mirror of
https://github.com/complexcaresolutions/payload-contracts.git
synced 2026-03-17 18:43:48 +00:00
Shared TypeScript types, API client, and block registry for coordinated CMS-to-frontend development across all tenants. - Type extraction script from payload-types.ts (12,782 lines) - 39 frontend collection types, 42 block types - createPayloadClient() with tenant isolation - createBlockRenderer() for type-safe block mapping - Media helpers (getImageUrl, getSrcSet) - Work order system for cross-server coordination - Block catalog documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
316 lines
9.4 KiB
TypeScript
316 lines
9.4 KiB
TypeScript
#!/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<Page['layout']>[number]
|
|
|
|
/**
|
|
* Extract a specific block type by its blockType discriminant
|
|
*
|
|
* Usage:
|
|
* type HeroBlock = BlockByType<'hero-block'>
|
|
*/
|
|
export type BlockByType<T extends Block['blockType']> = Extract<Block, { blockType: T }>
|
|
|
|
/**
|
|
* 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<Media['sizes']>
|
|
|
|
/**
|
|
* A single image size entry
|
|
*/
|
|
export type ImageSize = NonNullable<NonNullable<Media['sizes']>[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<T> {
|
|
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> = T
|
|
|
|
/**
|
|
* Error response from Payload API
|
|
*/
|
|
export interface PayloadError {
|
|
errors: Array<{
|
|
message: string
|
|
name?: string
|
|
data?: Record<string, unknown>
|
|
}>
|
|
}
|
|
|
|
/**
|
|
* 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<string, unknown>
|
|
/** 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)')
|