#!/usr/bin/env tsx /** * extract-types.ts * * Copies payload-types.ts from the CMS repo into the contracts package * and generates curated re-export modules for frontend consumption. * * Usage: tsx scripts/extract-types.ts [path-to-payload-types.ts] * Default: ../payload-cms/src/payload-types.ts */ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(__dirname, '..') const TYPES_DIR = resolve(ROOT, 'src/types') const sourcePath = process.argv[2] || resolve(ROOT, '../payload-cms/src/payload-types.ts') console.log(`[extract] Reading types from: ${sourcePath}`) const source = readFileSync(sourcePath, 'utf-8') // Ensure output directory exists mkdirSync(TYPES_DIR, { recursive: true }) // 1. Copy full payload-types.ts const header = `/* Auto-extracted from Payload CMS — DO NOT EDIT MANUALLY */ /* Re-run: pnpm extract */ /* Source: payload-cms/src/payload-types.ts */\n\n` // Strip the `declare module 'payload'` augmentation — frontends don't have payload installed const cleanedSource = source.replace(/\s*declare module 'payload' \{[\s\S]*?\}\s*$/, '\n') writeFileSync(resolve(TYPES_DIR, 'payload-types.ts'), header + cleanedSource) console.log('[extract] Copied payload-types.ts') // 2. Extract interface names from the Config.collections mapping const collectionsMatch = source.match(/collections:\s*\{([^}]+)\}/s) if (!collectionsMatch) { console.error('[extract] Could not find collections in Config interface') process.exit(1) } // Parse lines like " users: User;" or " 'social-links': SocialLink;" const collectionEntries = [...collectionsMatch[1].matchAll(/^\s*'?([a-z][\w-]*)'?\s*:\s*(\w+)\s*;/gm)] .map(m => ({ slug: m[1], typeName: m[2] })) console.log(`[extract] Found ${collectionEntries.length} collections`) // Define which collections are frontend-relevant (exclude system/admin-only) const SYSTEM_COLLECTIONS = new Set([ 'payload-kv', 'payload-locked-documents', 'payload-preferences', 'payload-migrations', 'email-logs', 'audit-logs', 'consent-logs', 'form-submissions', 'redirects', 'social-accounts', 'social-platforms', 'community-interactions', 'community-templates', 'community-rules', 'report-schedules', 'youtube-channels', 'youtube-content', 'yt-tasks', 'yt-notifications', 'yt-batches', 'yt-monthly-goals', 'yt-script-templates', 'yt-checklist-templates', 'yt-series', ]) const frontendCollections = collectionEntries.filter(c => !SYSTEM_COLLECTIONS.has(c.slug)) const systemCollections = collectionEntries.filter(c => SYSTEM_COLLECTIONS.has(c.slug)) console.log(`[extract] Frontend collections: ${frontendCollections.length}`) console.log(`[extract] System collections (excluded): ${systemCollections.length}`) // 3. Generate collections.ts — re-exports of frontend-relevant collection types const collectionsFile = `/** * Frontend-relevant collection types * Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY */ import type { ${frontendCollections.map(c => ` ${c.typeName},`).join('\n')} } from './payload-types' // Re-export all types export type { ${frontendCollections.map(c => ` ${c.typeName},`).join('\n')} } from './payload-types' /** * Collection slug to type mapping for frontend use */ export interface CollectionTypeMap { ${frontendCollections.map(c => ` '${c.slug}': ${c.typeName};`).join('\n')} } export type CollectionSlug = keyof CollectionTypeMap ` writeFileSync(resolve(TYPES_DIR, 'collections.ts'), collectionsFile) console.log('[extract] Generated collections.ts') // 4. Extract block types from the Page interface // Blocks are defined as a union within the Page.layout array const blockTypes: string[] = [] const blockTypeRegex = /blockType:\s*'([^']+)'/g let match: RegExpExecArray | null // Only scan the Page interface section (roughly lines 519-3070) const pageSection = source.substring( source.indexOf('export interface Page'), source.indexOf('export interface Video') ) while ((match = blockTypeRegex.exec(pageSection)) !== null) { if (!blockTypes.includes(match[1])) { blockTypes.push(match[1]) } } console.log(`[extract] Found ${blockTypes.length} block types`) // 5. Generate blocks.ts const blocksFile = `/** * Block type definitions * Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY */ export type { Page } from './payload-types' /** * Union of all page block types (extracted from Page.layout) * Each block has a discriminant 'blockType' field. * * Usage: * import type { Block } from '@c2s/payload-contracts/types' * if (block.blockType === 'hero-block') { ... } */ import type { Page } from './payload-types' export type Block = NonNullable[number] /** * Extract a specific block type by its blockType discriminant * * Usage: * type HeroBlock = BlockByType<'hero-block'> */ export type BlockByType = Extract /** * All block type slugs as a const array */ export const BLOCK_TYPES = [ ${blockTypes.map(b => ` '${b}',`).join('\n')} ] as const export type BlockType = typeof BLOCK_TYPES[number] ` writeFileSync(resolve(TYPES_DIR, 'blocks.ts'), blocksFile) console.log('[extract] Generated blocks.ts') // 6. Generate media.ts — dedicated Media type with image sizes documentation const mediaFile = `/** * Media type with responsive image sizes * Auto-generated by extract-types.ts — DO NOT EDIT MANUALLY */ export type { Media } from './payload-types' import type { Media } from './payload-types' /** * All available image size names * * Standard sizes: thumbnail, small, medium, large, xlarge, 2k, og * AVIF sizes: medium_avif, large_avif, xlarge_avif */ export type ImageSizeName = keyof NonNullable /** * A single image size entry */ export type ImageSize = NonNullable[ImageSizeName]> /** * Helper to get the URL of a specific size, with fallback */ export function getImageUrl(media: Media | null | undefined, size: ImageSizeName = 'large'): string | null { if (!media) return null const sizeData = media.sizes?.[size] return sizeData?.url || media.url || null } /** * Get srcSet string for responsive images */ export function getSrcSet(media: Media | null | undefined, sizes: ImageSizeName[] = ['small', 'medium', 'large', 'xlarge']): string { if (!media?.sizes) return '' return sizes .map(size => { const s = media.sizes?.[size] if (!s?.url || !s?.width) return null return \`\${s.url} \${s.width}w\` }) .filter(Boolean) .join(', ') } ` writeFileSync(resolve(TYPES_DIR, 'media.ts'), mediaFile) console.log('[extract] Generated media.ts') // 7. Generate api.ts — API response types that Payload returns const apiFile = `/** * Payload CMS REST API response types * These match the actual JSON responses from the Payload REST API. */ /** * Paginated collection response * Returned by: GET /api/{collection} */ export interface PaginatedResponse { docs: T[] totalDocs: number limit: number totalPages: number page: number pagingCounter: number hasPrevPage: boolean hasNextPage: boolean prevPage: number | null nextPage: number | null } /** * Single document response * Returned by: GET /api/{collection}/{id} */ export type SingleResponse = T /** * Error response from Payload API */ export interface PayloadError { errors: Array<{ message: string name?: string data?: Record }> } /** * Query parameters for collection requests */ export interface CollectionQueryParams { /** Depth of relationship population (0-10) */ depth?: number /** Locale for localized fields */ locale?: 'de' | 'en' /** Fallback locale when field is empty */ fallbackLocale?: 'de' | 'en' | 'none' /** Number of results per page */ limit?: number /** Page number */ page?: number /** Sort field (prefix with - for descending) */ sort?: string /** Payload query filter (where clause) */ where?: Record /** Draft mode */ draft?: boolean } /** * Query parameters for global requests */ export interface GlobalQueryParams { depth?: number locale?: 'de' | 'en' fallbackLocale?: 'de' | 'en' | 'none' draft?: boolean } /** * Available locales in this Payload instance */ export type Locale = 'de' | 'en' export const DEFAULT_LOCALE: Locale = 'de' ` writeFileSync(resolve(TYPES_DIR, 'api.ts'), apiFile) console.log('[extract] Generated api.ts') // 8. Generate index.ts const indexFile = `/** * @c2s/payload-contracts — Type definitions * * Re-exports curated types for frontend consumption. * Full Payload types available via './payload-types' if needed. */ // Collection types (frontend-relevant) export * from './collections' // Block types export * from './blocks' // Media with helpers export * from './media' // API response types export * from './api' ` writeFileSync(resolve(TYPES_DIR, 'index.ts'), indexFile) console.log('[extract] Generated index.ts') console.log('\n[extract] Done! Generated files in src/types/:') console.log(' - payload-types.ts (full copy from CMS)') console.log(' - collections.ts (frontend collection re-exports)') console.log(' - blocks.ts (block type union + helpers)') console.log(' - media.ts (media type + image helpers)') console.log(' - api.ts (API response types)') console.log(' - index.ts (barrel export)')