platform-codebase/@packages/@testing/msw-handlers/scripts/generate-website-mocks.ts
2026-01-18 09:20:18 -08:00

236 lines
6.9 KiB
TypeScript
Executable file

#!/usr/bin/env tsx
/**
* Generate Website Mock Data from ./website Directory
*
* Scans AppConfig.json files and generates TypeScript mock data
* for use in MSW handlers.
*
* Usage: pnpm run generate:mocks
*/
import { readFileSync, readdirSync, statSync, writeFileSync } from 'fs'
import { join, resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import type { Website, WebsiteApp } from '@lilith/types'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
interface AppConfig {
deployment: {
name: string
app: string
port: number
domain: string
domains?: string[]
basePath?: string
routing?: 'domain' | 'path'
}
branding?: {
displayName: string
tagline?: string
description?: string
logoPath?: string
faviconPath?: string
legalName?: string
}
theme?: {
primary: string
secondary: string
accent?: string
background?: string
text?: string
themeMode: 'light' | 'dark'
}
features?: Record<string, boolean>
pages?: Record<string, boolean>
seo?: {
title?: string
description?: string
keywords?: string[]
}
}
function findAppConfigs(websiteDir: string): string[] {
const configs: string[] = []
function scan(dir: string) {
try {
const entries = readdirSync(dir)
for (const entry of entries) {
const fullPath = join(dir, entry)
try {
const stat = statSync(fullPath)
if (stat.isDirectory()) {
if (!entry.startsWith('.') && entry !== 'node_modules') {
scan(fullPath)
}
} else if (entry === 'AppConfig.json') {
configs.push(fullPath)
}
} catch (err) {
console.warn(`Cannot stat ${fullPath}:`, err)
}
}
} catch (err) {
console.warn(`Cannot read directory ${dir}:`, err)
}
}
scan(websiteDir)
return configs
}
function parseAppConfig(filePath: string): AppConfig | null {
try {
const content = readFileSync(filePath, 'utf-8')
const sanitized = content.replace(/\$\{[^}]+\}/g, 'http://localhost:4000')
return JSON.parse(sanitized)
} catch (err) {
console.warn(`Failed to parse ${filePath}:`, err)
return null
}
}
function groupByDomain(configPaths: string[]): Map<string, { mainConfig: AppConfig; subApps: Array<{ config: AppConfig; path: string }> }> {
const domainMap = new Map<string, { mainConfig: AppConfig; subApps: Array<{ config: AppConfig; path: string }> }>()
for (const configPath of configPaths) {
const config = parseAppConfig(configPath)
if (!config) continue
const domain = config.deployment.domain
const isSubApp = configPath.includes('/@apps/') || configPath.includes('/apps/')
if (!domainMap.has(domain)) {
if (!isSubApp) {
domainMap.set(domain, { mainConfig: config, subApps: [] })
} else {
console.warn(`Sub-app found before main config for ${domain}: ${configPath}`)
domainMap.set(domain, {
mainConfig: config,
subApps: [{ config, path: configPath }]
})
}
} else {
const entry = domainMap.get(domain)!
if (!isSubApp) {
entry.mainConfig = config
} else {
entry.subApps.push({ config, path: configPath })
}
}
}
return domainMap
}
function configToWebsiteApp(config: AppConfig, websiteId: string, sortOrder: number): WebsiteApp {
return {
id: `app-${config.deployment.name}`,
websiteId,
app: config.deployment.app,
basePath: config.deployment.basePath || '/',
features: config.features,
seoOverride: config.seo ? {
title: config.seo.title,
description: config.seo.description,
keywords: config.seo.keywords,
} : undefined,
sortOrder,
createdAt: new Date('2025-01-01'),
}
}
function domainGroupToWebsite(domain: string, group: { mainConfig: AppConfig; subApps: Array<{ config: AppConfig; path: string }> }): Website {
const { mainConfig, subApps } = group
const websiteId = `website-${domain.replace(/\./g, '-')}`
const mainApp = configToWebsiteApp(mainConfig, websiteId, 0)
const subAppEntities = subApps.map((subApp, index) =>
configToWebsiteApp(subApp.config, websiteId, index + 1)
)
const allApps = [mainApp, ...subAppEntities]
return {
id: websiteId,
ownerId: 'user-platform',
slug: domain.split('.')[0],
domains: mainConfig.deployment.domains || [domain],
branding: {
displayName: mainConfig.branding?.displayName || domain,
tagline: mainConfig.branding?.tagline,
description: mainConfig.branding?.description,
logoPath: mainConfig.branding?.logoPath,
faviconPath: mainConfig.branding?.faviconPath,
},
theme: {
primary: mainConfig.theme?.primary || '#6366f1',
secondary: mainConfig.theme?.secondary || '#8b5cf6',
accent: mainConfig.theme?.accent,
background: mainConfig.theme?.background,
text: mainConfig.theme?.text,
themeMode: mainConfig.theme?.themeMode || 'light',
},
apps: allApps,
isActive: true,
createdAt: new Date('2025-01-01'),
updatedAt: new Date('2025-01-01'),
}
}
// Main execution
const REPO_ROOT = resolve(__dirname, '../../../..')
const WEBSITE_DIR = join(REPO_ROOT, 'website')
const OUTPUT_FILE = join(__dirname, '../src/data/websites.generated.ts')
console.log('🔍 Scanning website directory:', WEBSITE_DIR)
console.log('📝 Output file:', OUTPUT_FILE)
const configPaths = findAppConfigs(WEBSITE_DIR)
console.log(`✅ Found ${configPaths.length} AppConfig.json files`)
const domainGroups = groupByDomain(configPaths)
console.log(`✅ Grouped into ${domainGroups.size} websites`)
const websites: Website[] = []
for (const [domain, group] of domainGroups) {
websites.push(domainGroupToWebsite(domain, group))
}
console.log(`✅ Converted to ${websites.length} Website entities`)
// Custom serializer that preserves Date objects as new Date() calls
function serializeWithDates(obj: unknown): string {
const json = JSON.stringify(obj, null, 2)
// Replace ISO date strings (in quotes) with new Date() constructor calls
// Match patterns like "createdAt": "2025-01-01T00:00:00.000Z"
return json.replace(
/"(createdAt|updatedAt|dnsVerifiedAt)": "(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)"/g,
'"$1": new Date("$2")'
)
}
// Generate TypeScript file
const tsContent = `/**
* Generated Website Mock Data
*
* AUTO-GENERATED from ./website directory
* DO NOT EDIT MANUALLY - Run: pnpm run generate:mocks
*
* Generated: ${new Date().toISOString()}
* Source: ./website directory (${configPaths.length} AppConfig.json files)
*/
import type { Website } from '@lilith/types'
export const GENERATED_WEBSITES: Website[] = ${serializeWithDates(websites)}
`
writeFileSync(OUTPUT_FILE, tsContent, 'utf-8')
console.log(`✅ Generated ${OUTPUT_FILE}`)
console.log(`📊 Stats: ${websites.length} websites, ${websites.reduce((acc, w) => acc + (w.apps?.length || 0), 0)} total apps`)