#!/usr/bin/env bun // Emit Matrixify-format CSVs for the 4 new metaobject definition groups // produced by generate-pilot-content.ts: // - scientific_study // - timeline_phase + timeline_block (with phases as list.metaobject_reference) // - comparison_column + comparison_row + comparison_table // - faq_pair + faq_block // // Output: content_population_exports/pilot-extended-.csv // One CSV per definition, long format (one row per Handle x Field x Value). // // Usage: bun scripts/export-extended-metaobjects.ts import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs' import { join } from 'path' const ROOT = join(import.meta.dir, '..') const IN_DIR = join(ROOT, 'data', 'content', 'pilot-extended') const OUT_DIR = join(ROOT, 'content_population_exports') const META_COLS = ['Handle', 'Display Name', 'Status', 'Definition: Handle', 'Definition: Name', 'Field', 'Value'] as const const DEFINITIONS = { scientific_study: 'Scientific Study', timeline_phase: 'Timeline Phase', timeline_block: 'Timeline Block (Results)', comparison_column: 'Comparison Column', comparison_row: 'Comparison Row', comparison_table: 'Comparison Table', faq_pair: 'FAQ Pair', faq_block: 'FAQ Block' } as const type DefKey = keyof typeof DEFINITIONS function csvEscape(v: unknown): string { if (Array.isArray(v)) v = v.join(', ') if (v === undefined || v === null) v = '' const s = String(v) return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s } function toCsv(rows: (string | number | boolean | null | undefined)[][]): string { return rows.map(r => r.map(csvEscape).join(',')).join('\n') + '\n' } type RowBuckets = Record function newBuckets(): RowBuckets { return Object.fromEntries( (Object.keys(DEFINITIONS) as DefKey[]).map(k => [k, [META_COLS as unknown as string[]]]) ) as RowBuckets } function addField( buckets: RowBuckets, def: DefKey, handle: string, displayName: string, status: string, field: string, value: unknown ) { buckets[def].push([handle, displayName, status, def, DEFINITIONS[def], field, value as any]) } function emitScientificStudy(buckets: RowBuckets, ss: any) { if (!ss?.handle) return const def: DefKey = 'scientific_study' const h = ss.handle, n = ss.name_internal || ss.headline || h, st = ss.status || 'draft' addField(buckets, def, h, n, st, 'name_internal', ss.name_internal || '') addField(buckets, def, h, n, st, 'headline', ss.headline || '') addField(buckets, def, h, n, st, 'body_copy', ss.body_copy || '') addField(buckets, def, h, n, st, 'link_url', ss.link_url || '') addField(buckets, def, h, n, st, 'link_label', ss.link_label || '') const stats = (ss.stats as any[]) || [] for (let i = 0; i < 3; i++) { const s = stats[i] || {} addField(buckets, def, h, n, st, `stat_${i + 1}_value`, s.value ?? '') addField(buckets, def, h, n, st, `stat_${i + 1}_label`, s.label ?? '') } addField(buckets, def, h, n, st, 'image', ss.image || '') } function emitTimeline(buckets: RowBuckets, tb: any) { if (!tb?.handle) return const def: DefKey = 'timeline_block' const h = tb.handle, n = tb.name_internal || h, st = tb.status || 'draft' addField(buckets, def, h, n, st, 'headline', tb.headline || '') addField(buckets, def, h, n, st, 'subtitle', tb.subtitle || '') addField(buckets, def, h, n, st, 'intro_copy', tb.intro_copy || '') addField(buckets, def, h, n, st, 'study_link_label', tb.study_link_label || '') addField(buckets, def, h, n, st, 'study_link_url', tb.study_link_url || '') const phases = (tb.phases as any[]) || [] const phaseRefs = phases.map(p => `timeline_phase.${p.handle}`).join(', ') addField(buckets, def, h, n, st, 'phases', phaseRefs) // phase entries on their own sheet const pdef: DefKey = 'timeline_phase' for (const p of phases) { const ph = p.handle, pn = p.phase_label || ph, pst = p.status || 'draft' addField(buckets, pdef, ph, pn, pst, 'phase_label', p.phase_label || '') addField(buckets, pdef, ph, pn, pst, 'body_copy', p.body_copy || '') } } function emitComparison(buckets: RowBuckets, ct: any) { if (!ct?.handle) return const tdef: DefKey = 'comparison_table' const h = ct.handle, n = ct.name_internal || h, st = ct.status || 'draft' addField(buckets, tdef, h, n, st, 'section_title', ct.section_title || '') addField(buckets, tdef, h, n, st, 'subtitle', ct.subtitle || '') const cols = (ct.columns as any[]) || [] const rows = (ct.rows as any[]) || [] addField(buckets, tdef, h, n, st, 'columns', cols.map(c => `comparison_column.${c.handle}`).join(', ')) addField(buckets, tdef, h, n, st, 'rows', rows.map(r => `comparison_row.${r.handle}`).join(', ')) const cdef: DefKey = 'comparison_column' for (const c of cols) { const ch = c.handle, cn = c.name || ch addField(buckets, cdef, ch, cn, 'draft', 'name', c.name || '') addField(buckets, cdef, ch, cn, 'draft', 'is_us', c.is_us ? 'true' : 'false') addField(buckets, cdef, ch, cn, 'draft', 'accent_colour', c.accent_colour || '') addField(buckets, cdef, ch, cn, 'draft', 'image', c.image || '') } const rdef: DefKey = 'comparison_row' for (const r of rows) { const rh = r.handle, rn = r.feature_label || rh, rst = r.status || 'draft' addField(buckets, rdef, rh, rn, rst, 'feature_label', r.feature_label || '') addField(buckets, rdef, rh, rn, rst, 'values_json', JSON.stringify(r.values || {})) } } function emitFaq(buckets: RowBuckets, fb: any) { if (!fb?.handle) return const bdef: DefKey = 'faq_block' const h = fb.handle, n = fb.name_internal || h, st = fb.status || 'draft' addField(buckets, bdef, h, n, st, 'section_title', fb.section_title || '') addField(buckets, bdef, h, n, st, 'cta_label', fb.cta_label || '') addField(buckets, bdef, h, n, st, 'cta_url', fb.cta_url || '') const pairs = (fb.pairs as any[]) || [] addField(buckets, bdef, h, n, st, 'pairs', pairs.map(p => `faq_pair.${p.handle}`).join(', ')) const pdef: DefKey = 'faq_pair' for (const p of pairs) { const ph = p.handle, pn = p.question || ph, pst = p.status || 'draft' addField(buckets, pdef, ph, pn, pst, 'scope', p.scope || 'product') addField(buckets, pdef, ph, pn, pst, 'question', p.question || '') addField(buckets, pdef, ph, pn, pst, 'answer', p.answer || '') addField(buckets, pdef, ph, pn, pst, 'source_citation_id', p.source_citation_id || '') } } async function main() { if (!existsSync(IN_DIR)) { console.error(`Input dir missing: ${IN_DIR}. Run generate-pilot-content.ts first.`) process.exit(1) } mkdirSync(OUT_DIR, { recursive: true }) const buckets = newBuckets() const files = readdirSync(IN_DIR).filter(f => f.endsWith('.json')) if (files.length === 0) { console.error(`No pilot-extended JSONs in ${IN_DIR}`); process.exit(1) } for (const f of files) { const doc = JSON.parse(readFileSync(join(IN_DIR, f), 'utf8')) emitScientificStudy(buckets, doc.scientificStudy) emitTimeline(buckets, doc.timelineBlock) emitComparison(buckets, doc.comparisonTable) emitFaq(buckets, doc.faqBlock) } const summary: Record = {} for (const def of Object.keys(DEFINITIONS) as DefKey[]) { const out = toCsv(buckets[def]) const rowCount = buckets[def].length - 1 const fileName = `pilot-extended-${def}.csv` writeFileSync(join(OUT_DIR, fileName), out, 'utf8') summary[def] = rowCount console.log(` ${fileName.padEnd(45)} ${rowCount} rows`) } const manifestPath = join(OUT_DIR, 'pilot-extended-manifest.json') writeFileSync( manifestPath, JSON.stringify( { generatedAt: new Date().toISOString(), source: 'data/content/pilot-extended/', skus: files.map(f => f.replace(/\.json$/, '')), definitions: summary, notes: [ 'Long-format Matrixify CSVs per metaobject definition.', 'Import order must respect references: comparison_column + comparison_row + timeline_phase + faq_pair FIRST, then comparison_table + timeline_block + faq_block + scientific_study.', 'Confirm with Lewis that the Shopify template supports list.metaobject_reference for timeline_block.phases, comparison_table.columns/rows, faq_block.pairs.' ] }, null, 2 ) ) console.log(`\nManifest: ${manifestPath}`) } await main()