{"id":32861302,"url":"https://github.com/nathannye/sanity-yaml","last_synced_at":"2026-05-17T02:11:52.002Z","repository":{"id":321302894,"uuid":"1085284679","full_name":"nathannye/sanity-yaml","owner":"nathannye","description":"Bulk Sanity.io schema, frontend component, and type definition generator based on YAML","archived":false,"fork":false,"pushed_at":"2025-12-11T20:48:38.000Z","size":169,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-13T01:23:59.094Z","etag":null,"topics":["plop-generators","sanity-io","yaml"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nathannye.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-28T20:38:26.000Z","updated_at":"2025-12-11T20:48:43.000Z","dependencies_parsed_at":"2025-10-29T00:25:38.057Z","dependency_job_id":"a66a77e7-ce50-4c14-9e4c-571e70141e84","html_url":"https://github.com/nathannye/sanity-yaml","commit_stats":null,"previous_names":["nathannye/sanity-yaml"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nathannye/sanity-yaml","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathannye%2Fsanity-yaml","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathannye%2Fsanity-yaml/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathannye%2Fsanity-yaml/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathannye%2Fsanity-yaml/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nathannye","download_url":"https://codeload.github.com/nathannye/sanity-yaml/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathannye%2Fsanity-yaml/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33125184,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-16T18:38:32.183Z","status":"online","status_checked_at":"2026-05-17T02:00:05.366Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["plop-generators","sanity-io","yaml"],"created_at":"2025-11-08T21:01:06.413Z","updated_at":"2026-05-17T02:11:51.996Z","avatar_url":"https://github.com/nathannye.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# What is this\nA bulk Sanity.io schema and component generator. Write your schemas like this: \n\n```yaml \n\nheroSection:\n  title!40: string\n  subtitle: text\n  image[]: image\n  tags: string(option1, option2, options3)\n  ctaLink: string\n```\n\nAnd generate this: \n\n```ts\nexport default {\n  name: 'heroSection',\n  title: 'Hero Section',\n  type: 'object',\n  fields: [\n    defineField({\n      name: 'title',\n      type: 'string',\n      validation: (Rule: any) =\u003e Rule.max(40).required()\n    }),\n    defineField({\n      name: 'subtitle',\n      type: 'text',\n      options: {\n        rows: 3\n      }\n    }),\n    defineField({\n      name: 'image',\n      type: 'array',\n      of: [\n        { type: 'image' }\n      ]\n    }),\n    defineField({\n      name: 'tags',\n      type: 'string',\n      options: {\n        list: ['option1', 'option2', 'options3']\n      }\n    }),\n    defineField({\n      name: 'ctaLink',\n      type: 'string'\n    })\n  ]\n};\n```\n\n\n**Why does it exist?**\n\u003cbr/\u003e\nOne of the first steps to building a new CMS-driven site is creating the schemas and the matching frontend components. It's not fast work, for me it's 90m to an hour every time I start a new project. I had originally used [plop.js](https://www.npmjs.com/package/plop), a phenomenal tool, Big up to Plop. But I still had to write length Sanity schemas and plop only makes one file at a time. So I wanted to save a bit more time and gen them all upfront.\n\n\n\u003e 👀 Highly inspired by [plop.js](https://www.npmjs.com/package/plop). An absolutely incredible library for one-off file generation.\n\n## Table of Contents\n\n- [Getting Started](#getting-started)\n  - [Installation](#installation)\n  - [Project Structure](#project-structure)\n  - [Configuration File](#configuration-file)\n  - [YAML Schema File](#yaml-schema-file)\n  - [Handlebars Templates](#handlebars-templates)\n  - [Running](#running)\n- [How it works: Syntax](#how-it-works-syntax)\n  - [Gotchas](#gotchas)\n  - [Basics](#basics)\n  - [Supported Field Types](#supported-field-types)\n  - [Using Custom Sanity Types](#using-custom-sanity-types)\n  - [Field Validation](#field-validation)\n  - [Example](#example)\n- [How it works: Templates](#how-it-works-templates)\n  - [Available Data](#available-data)\n  - [Handlebars Helpers](#handlebars-helpers)\n  - [Template Partials](#template-partials)\n\n# Getting Started\n\n## Installation\n\nInstall the package in your project:\n\n```bash\n# Using pnpm\npnpm add sanity-yaml\n\n# Using npm\nnpm install sanity-yaml\n```\n\n## Project Structure\n\nCreate the following files in your project:\n\n```\nyour-project/\n├── sanity-yaml.config.ts    # Configuration file\n├── schemas.yaml              # Your schema definitions\n└── templates/                # Your Handlebars templates\n    ├── schema.hbs\n    └── component.hbs\n```\n\n## Configuration File\n\nCreate a `sanity-yaml.config.ts` or `sanity-yaml.config.js` file in your project root:\n\n```typescript\nimport type { GeneratorConfig } from \"sanity-yaml\";\n\nconst config: GeneratorConfig = {\n  // Optional: Set default field options, only text is supported currently\n  fieldDefaults: {\n    text: {\n      rows: 4, // Default rows for text fields\n    },\n  },\n\n  // Optional: Remove defineField wrapper from generated fields\n  // When true, fields will be plain objects instead of defineField() calls\n  removeDefineField: false,\n\n  // Required: Define your filesets\n  filesets: {\n    // Each fileset generates files for schemas in a YAML file\n    yourFilesetName: {\n      inputPath: \"./schemas.yaml\",\n      onFileCreate: async ({ name, sanityFields, typeDefinition, renderTemplate, modifyFile }) =\u003e {\n        // Generate Sanity schema file using templateFile (path to .hbs file)\n        // Note: Output directories are created automatically if they don't exist\n        // You can use Handlebars helpers in template paths too!\n        await renderTemplate({\n          templateFile: \"./templates/{{name}}.hbs\",  // Template path supports Handlebars\n          data: { name, sanityFields },\n          outputPath: `./generated/schemas/{{kebabCase name}}.ts`,  // Output path supports Handlebars\n        });\n\n        // Generate JSX component file using templateFile\n        // Both templateFile and outputPath support Handlebars helpers\n        await renderTemplate({\n          templateFile: \"./templates/{{kebabCase name}}-component.hbs\",\n          data: { name, typeDefinition },\n          outputPath: `./generated/components/{{kebabCase name}}.tsx`,\n        });\n\n        // Add import to index file using direct template string\n        // Use 'template' parameter for inline template strings\n        // The regex option inserts content AFTER the matched pattern (doesn't replace it)\n        await modifyFile({\n          template: \"import {{pascalCase name}} from './{{name}}/{{pascalCase name}}.tsx'\\n\",\n          data: { name },\n          targetFile: \"./generated/schemas/index.ts\",\n          regex: \"const sections = \\{\", // Inserts import statement after this line\n        });\n      },\n    },\n  },\n};\n\nexport default config;\n```\n\n## YAML Schema File\n\nCreate a YAML file (e.g., `schemas.yaml`) with your schema definitions:\n\n```yaml\nheroSection:\n  title!: string\n  subtitle: text\n  image: image\n  ctaText: string\n  ctaLink: string\n\nblogPost:\n  title!: string\n  slug!: slug(title)\n  publishedDate!: datetime\n  author: -\u003eauthor\n  content: text\n  tags[]: string\n  featuredImage: image\n```\n\n## Handlebars Templates\n\nCreate template files (`.hbs`) that use Handlebars syntax:\n\n**`templates/schema.hbs`** - Generates Sanity schema:\n```handlebars\n{{#unless (shouldRemoveDefineField)}}\nimport { defineField } from \"sanity\";\n{{/unless}}\n\nexport default {\n  name: '{{name}}',\n  title: '{{titleCase name}}',\n  type: 'object',\n  fields: [\n    {{\u003e sanity-fields fields=sanityFields}}\n  ],\n};\n```\n\n\u003e 💡 When `removeDefineField: true` is set in your config, fields will be generated as plain objects instead of `defineField()` calls.\n\u003e 🧠 The component-props will give your linter a heart attack if you have unused-arguments enabled. Use this only if you are okay having a bunch of unused props in each file.\n**`templates/component.hbs`** - Generates JSX component:\n```handlebars\ninterface {{pascalCase name}}Props {\n{{\u003e jsx-types typeDefinition=typeDefinition}}\n}\n\nconst {{pascalCase name}} = ({ {{\u003e component-props typeDefinition=typeDefinition}} }: {{pascalCase name}}Props) =\u003e {\n  return (\n    \u003csection\u003e\n      \u003ch1\u003e{{titleCase name}}\u003c/h1\u003e\n    \u003c/section\u003e\n  );\n};\n\nexport default {{pascalCase name}};\n```\n\n**`templates/import.hbs`** - Template for adding exports to index file:\n```handlebars\nexport * from './{{name}}';\n```\n\n## Running\n\nAfter setting up your config and YAML files:\n\n```bash\n# Using pnpm\npnpm sanity-yaml\n\n# Using npm\nnpm sanity-yaml\n\n# With custom config path\npnpm sanity-yaml --config ./my-config.ts\n```\n\n---\n\n# How it works: Syntax\nTo get started, write up a `.yaml file` using the syntax described below.\n\n## Gotchas\n99% of the syntax is native yaml. But the array syntax had some liberties taken to keep it closer to typescript:\n\n### Arrays\n\n**Native YAML for arrays of key/value pairs**\n🚨 Do NOT use this, it is not supported\n```yaml\narrayName:\n  - field1: string\n    field2: number\n```\n\n**Our syntax**\nWe re-use the native object syntax and keep the array `[]` modifier used on other fields, ex: `string[]`.\n```yaml\narrayName[]:\n  field1: string\n  field2: number\n```\n\n**For arrays of simple types**, use the simpler syntax:\n```yaml\ntags[]: string\nnumbers[]: number\n```\n\n## Basics\nThe basic structure of schemas within YAML is key/value pairs. Keys are field names, and values are field types. Some fields support options with the () syntax.\n\n## Supported Field Types\n\n| Sanity Field Type | Basic Syntax                        | Description                                   | Advanced Syntax Example                |\n|:------------------|:------------------------------------|:-----------------------------------------------|:---------------------------------------|\n| `array`           | `tags[]: string` or `items[]:`\u003cbr/\u003e`  field1: string`\u003cbr/\u003e`  field2: number` | Array of any field type                        | Simple: `tags[]: string`, Objects: `items[]:`\u003cbr/\u003e`  field1: string`\u003cbr/\u003e`  field2: number` |\n| `boolean`         | `isActive: boolean`                 | `true`/`false` value                           |                                        |\n| `date`            | `eventDate: date`                   | ISO-format date string                         |                                        |\n| `datetime`        | `publishedDate: datetime`           | ISO-format date/time string                    |                                        |\n| `email`           | `contact: email`                    | String field with added email validation rule  |                                        |\n| `file`            | `annualReport: file`                | File upload field                              | Format requirements: `annualReports: file(pdf,docx)` |\n| `geopoint`        | `location: geopoint`                | Point with lat/lng/alt                         |                                        |\n| `image`           | `thumbnail: image`                  | Sanity image field                             |                                        |\n| `number`          | `count: number`                     | Numeric value (integer or float)               |                                        |\n| `object`          | `address:`\u003cbr/\u003e`  street: string`\u003cbr/\u003e`  city: string` | Nested fields as an object                   | Multiple fields: `address:`\u003cbr/\u003e`  street: string`\u003cbr/\u003e`  city: string`\u003cbr/\u003e`  zip: number` |\n| `reference`       | `author: -\u003eauthor`                  | Reference (relation) to another document       | Single: `author: -\u003eauthor`, Array: `categories[]: -\u003ecategory`, Multiple types: `clothing[]: -\u003e(shirts,pants)` |\n| `slug`            | `slug: slug`                        | Slug field automatically generated from a source | Use another field as source: `slug: slug(title)` |\n| `string`          | `name: string`                      | Plain text string                              | List options: `status: string(active, inactive)`  |\n| `text`            | `description: text`                 | Plain text with multiple lines                 | Row amount: `description: text(4)`     |\n| `url`             | `website: url`                     | URL field with validation                      |                                        |\n\n\n\u003e 📝 A note on arrays: They can be mixed with ANY type. image[], number[], whatever you want.\n\n## Using Custom Sanity Types\n\nYou can use any Sanity schema type that exists in your project, even if it's not in the supported types list above. Common examples include:\n- `portableText` - Sanity's rich text block content\n- `code` - Code blocks with syntax highlighting from a plugin\n- Any custom types you've defined in your Sanity schema\n\nWhen you use an unrecognized type:\n1. The generator will detect it and show a warning listing all unrecognized types\n2. You'll be prompted to confirm if you want to continue\n3. If you continue, these fields will be:\n   - Included in your Sanity schema files (using the type name as-is)\n   - Typed as `any` in your TypeScript type definitions\n\n**Example:**\n```yaml\nblogPost:\n  title!: string\n  content: portableText  # Custom Sanity type\n  codeBlock: code        # Another custom type\n```\n\nWhen you run the generator, you'll see:\n```\n⚠️  Unsupported field types detected:\n  blogPost: portableText, code\n\nSupported types: array, boolean, date, datetime, email, file, geopoint, image, number, object, reference, slug, string, text\n\nContinue with 2 unsupported types? (y/N)\n```\n\nAfter confirming, these fields will be generated with their Sanity types intact, but TypeScript will type them as `any`.\n\n## Field Validation\nAll field validation works together\n\n### Required\nAn exclamation point `!` after the field name and before the colon, marks a field required and compiles to: Rule = () =\u003e Rule.required()`\n\n```yaml\nSliceName\n  fieldName!: string\n```\n\n### Maximum Length\nA number after the field name and before the colon marks a field as needing a max number of items or characters. Compiles to: `validation: (Rule: any)=\u003eRule.max(number)`.\n```yaml\nSliceName:\n  fieldName4: string  # Requires minimum 4 characters\n```\n\n# Example\n\nHere's a complete example showing various field types:\n\n```yaml\nheroSection:\n  title!: string\n  subtitle: text\n  image: image\n  ctaText!: string\n  ctaLink!4: string\n  tags[]: string\n\nblogPost:\n  title!: string\n  slug!: slug(title)\n  publishedDate!: datetime\n  author: -\u003eauthor\n  categories[]: -\u003ecategory\n  content: text(10)\n  featuredImage: image\n  metadata:\n    description: string\n    keywords: string[]\n```\n\n# How it works: Templates\n\nHandlebars is used as the template engine to generate files. You have access to several built-in helpers and partials.\n\n## Available Data\n\nEach template receives the following data:\n- `name` - The schema name (e.g., \"heroSection\")\n- `title` - Title case version of the name (e.g., \"Hero Section\")\n- `sanityFields` - Array of processed Sanity field definitions\n- `typeDefinition` - TypeScript type definitions as an object\n\n### onFileCreate Callback Arguments\n\nThe `onFileCreate` callback receives an object with the following properties:\n\n- `name` - The schema name as a string (e.g., `\"heroSection\"`)\n- `sanityFields` - Array of processed Sanity field definitions\n- `typeDefinition` - TypeScript type definitions as an object\n- `renderTemplate` - Function to render a Handlebars template\n- `modifyFile` - Function to modify an existing file\n\n### Template Parameters: `templateFile` vs `template`\n\nBoth `renderTemplate` and `modifyFile` accept either `templateFile` or `template` (but not both):\n\n- **`templateFile`** - Path to a Handlebars template file (`.hbs`). The path itself supports Handlebars syntax for dynamic file selection.\n- **`template`** - Direct template string. Use this for inline templates without needing a separate file.\n\nYou must provide exactly one of these parameters. If you provide `templateFile` but the file doesn't exist, you'll get a helpful error suggesting you might want to use `template` instead.\n\n**Example:**\n```typescript\nonFileCreate: async ({ name, sanityFields, typeDefinition, renderTemplate, modifyFile }) =\u003e {\n  // Using templateFile - path to a template file\n  await renderTemplate({\n    templateFile: \"./templates/{{name}}.hbs\",\n    // Renders to: ./templates/heroSection.hbs, then reads that file\n    data: { name, typeDefinition },\n    outputPath: `./src/components/{{kebabCase name}}.tsx`,\n    // Renders to: ./src/components/hero-section.tsx\n  });\n  \n  // Using templateFile with dynamic path selection\n  await renderTemplate({\n    templateFile: \"./templates/{{kebabCase name}}-component.hbs\",\n    // Renders to: ./templates/hero-section-component.hbs\n    data: { name, typeDefinition },\n    outputPath: `./src/types/{{pascalCase name}}.ts`,\n    // Renders to: ./src/types/HeroSection.ts\n  });\n  \n  // Using template - direct template string (no file needed)\n  await modifyFile({\n    template: \"import {{pascalCase name}} from './{{name}}/{{pascalCase name}}.tsx'\\n\",\n    // Direct template string, no file needed\n    data: { name },\n    targetFile: \"./src/components/{{kebabCase name}}/index.ts\",\n    // Renders to: ./src/components/hero-section/index.ts\n    regex: \"const sections = \\{\", // Optional: inserts content AFTER this pattern\n  });\n}\n```\n\n\u003e 💡 **Note:** The `templateFile` parameter path is processed as a Handlebars template before resolving the file path, so you can dynamically select template files based on the schema name or other data properties. This is especially useful when you have multiple template variants or want to organize templates by naming conventions.\n\n### `modifyFile` Behavior\n\nThe `modifyFile` function modifies existing files by inserting or appending content:\n\n- **Without `regex`**: Appends the rendered template content to the end of the file\n- **With `regex`**: Finds the first match of the regex pattern and inserts the rendered template content **after** the match (the matched content is preserved)\n\n**Example with regex:**\nIf your file contains:\n```typescript\nconst sections = {\n  // existing code\n}\n```\n\nAnd you call:\n```typescript\nawait modifyFile({\n  template: \"import HeroSection from './heroSection'\\n\",\n  data: { name: \"heroSection\" },\n  targetFile: \"./index.ts\",\n  regex: \"const sections = \\{\",\n});\n```\n\nThe result will be:\n```typescript\nconst sections = {\nimport HeroSection from './heroSection'\n  // existing code\n}\n```\n\nThe matched pattern (`const sections = \\{`) is preserved, and the template content is inserted right after it.\n\n## Handlebars Helpers\n\n### Casing Utilities\n\nThe following casing helpers are available for transforming the `name` or any string:\n\n- `{{pascalCase name}}` - `heroSection` → `HeroSection`\n- `{{camelCase name}}` - `hero-section` → `heroSection`\n- `{{kebabCase name}}` - `heroSection` → `hero-section`\n- `{{titleCase name}}` - `hero section` → `Hero Section`\n- `{{sentenceCase name}}` - `hero section` → `Hero section`\n- `{{snakeCase name}}` - `heroSection` → `hero_section`\n\n**Example:**\n```handlebars\n\u003c!-- Component name --\u003e\nconst {{pascalCase name}} = ...\n\n\u003c!-- File name --\u003e\nexport * from './{{kebabCase name}}'\n\n\u003c!-- Display name --\u003e\n\u003ch1\u003e{{titleCase name}}\u003c/h1\u003e\n```\n\n## Template Partials\n\nThree built-in partials are available for use in your templates (no setup required). Each partial requires specific property names:\n\n## `component-props`\nGenerates component props destructuring for JSX components.\n\n```hbs\nconst MyComponent = ({ {{\u003e component-props typeDefinition=typeDefinition}} }: MyComponentProps) =\u003e {\n```\n\nOutputs: `field1, field2, field3`\n\n## `jsx-types`\nGenerates TypeScript interface properties from type definitions for TSX files (sorry Vue!).\n\n```hbs\ninterface MyComponentProps {\n{{\u003e jsx-types typeDefinition=typeDefinition}}\n}\n```\n\nOutputs:\n```ts\n  field1: string;\n  field2: number;\n  field3: boolean;\n```\n\n## `sanity-fields`\nGenerates Sanity field definitions from your schema fields.\n\n```hbs\nfields: [\n  {{\u003e sanity-fields fields=sanityFields}}\n]\n```\n\nOutputs complete Sanity `defineField` calls for all field types.\n\n## Full Frontend Template file example\n```hbs\n\u003c!-- jsx-types only utputs the types, NOT the type or interface wrapper --\u003e\ninterface {{pascalCase name}}Props {\n{{\u003e jsx-types typeDefinition=typeDefinition}}\n}\n\nconst {{pascalCase name}} = ({ {{\u003e component-props typeDefinition=typeDefinition}} }: {{pascalCase name}}Props) =\u003e {\n  return (\n    \u003csection\u003e\n\t\t\t\u003ch2\u003eSection: {{name}}\u003c/h2\u003e\n    \u003c/section\u003e\n  )\n}\n\nexport default {{pascalCase name}}\n\n\n```\n\n## Fill Sanity Schema file example\n\n```hbs\n\nimport { defineField } from \"sanity\";\n\nexport default {\n    name: '{{name}}',\n    title: '{{title}}',\n    type: 'object',\n    fields: [\n        {{\u003e sanity-fields fields=sanityFields}}\n    ],\n    preview: {\n\t\t\tprepare(){\n\t\t\t\treturn {\n\t\t\t\t\ttitle: '{{name}}'\n\t\t\t\t}\n\t\t\t}\n\t\t}\n}\n\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathannye%2Fsanity-yaml","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnathannye%2Fsanity-yaml","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathannye%2Fsanity-yaml/lists"}