import fs from "fs"; import path from "path"; import { execSync } from "child_process"; const projectRoot = process.cwd(); const templateLibPath = path.join(projectRoot, "apps", "portfolio", "template-library"); const registryFilePath = path.join(templateLibPath, "registry.ts"); if (fs.existsSync(registryFilePath) && !process.env.FORCE_MOCK) { console.log("Template library already exists. Skipping mock setup."); process.exit(0); } console.log("Template library not found. Setting up mock templates for CI build..."); // Create directories fs.mkdirSync(templateLibPath, { recursive: true }); fs.mkdirSync(path.join(templateLibPath, "atelier"), { recursive: true }); fs.mkdirSync(path.join(templateLibPath, "signal"), { recursive: true }); fs.mkdirSync(path.join(templateLibPath, "nimbus"), { recursive: true }); fs.mkdirSync(path.join(templateLibPath, "cipher"), { recursive: true }); // Types const typesContent = `export interface PortfolioSection { id: string; type: string; title: string; subtitle?: string; visible: boolean; items: Array>; settings?: Record; } export interface PortfolioProject { schemaVersion: 1; templateId: string; identity: { name: string; headline: string; bio: string; location: string; email: string; availability: string; avatar: { id: string; url: string } | null; }; seo: { title: string; description: string; socialImage: { id: string; url: string } | null }; socialLinks: Array<{ id: string; label: string; url: string }>; sections: PortfolioSection[]; } export interface ProjectItem { id: string; name: string; role?: string; link?: string; linkLabel?: string; showLinkAsText?: boolean; summary: string; highlights?: string[]; skills?: string[]; coverImage?: { id: string; url: string } | null; } export interface ExperienceItem { id: string; company: string; role: string; location?: string; startDate?: string; endDate?: string; current?: boolean; summary: string; highlights?: string[]; coverImage?: { id: string; url: string } | null; } export interface EducationItem { id: string; school: string; degree?: string; field?: string; startDate?: string; endDate?: string; current?: boolean; summary?: string; coverImage?: { id: string; url: string } | null; } export interface SkillGroupItem { id: string; name: string; keywords: string[]; } export interface AdditionalSectionItem { id: string; name: string; issuer?: string; date?: string; link?: string; referenceId?: string; description?: string; details?: string[]; coverImage?: { id: string; url: string } | null; } export function itemText(item: Record, key: string, fallback = "") { const val = item[key]; if (key === "id" && (typeof val !== "string" || !val.trim())) { return fallback; } return typeof val === "string" ? val : fallback; } export function itemFirst(item: Record, keys: string[], fallback = "") { for (const key of keys) { const val = item[key]; if (typeof val === "string" && val.trim()) return val; } return fallback; } export function itemProse(item: Record, fallback = "") { return itemFirst(item, ["summary", "description"], fallback); } export function itemLabel(item: Record, fallback = "") { return itemFirst(item, ["title", "name"], fallback); } export function itemList(item: Record, ...keys: string[]): string[] { for (const key of keys) { const val = item[key]; if (Array.isArray(val)) { const list = val.filter( (entry): entry is string => typeof entry === "string" && !!entry.trim(), ); if (list.length) return list; } if (typeof val === "string" && val.trim()) { return val .split(/\\r?\\n|,/) .map((entry) => entry.trim()) .filter(Boolean); } } return []; } export function itemTags(item: Record) { return Array.isArray(item.tags) ? item.tags.filter((tag): tag is string => typeof tag === "string") : []; } export function itemAssetUrl(item: Record, key: string) { const value = item[key]; return value && typeof value === "object" && typeof (value as { url?: unknown }).url === "string" ? (value as { url: string }).url : ""; } export function safeExternalUrl(value: string) { try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : ""; } catch { return ""; } } export function itemLinkUrl(item: Record) { const val = item.link ?? item.url; return typeof val === "string" ? safeExternalUrl(val) : ""; } export function formatPeriod(start?: string, end?: string, current?: boolean) { if (!start && !end) return ""; const startStr = start || ""; const endStr = current ? "Present" : end || ""; if (startStr && endStr) return \`\${startStr} — \${endStr}\`; return startStr || endStr; } export function visibleSection(project: PortfolioProject, type: string) { return project.sections.find((section) => section.type === type && section.visible); } export function initials(name: string) { const words = name.trim().split(/\\s+/).filter(Boolean); if (words.length === 0) return ""; if (words.length === 1) return Array.from(words[0]).slice(0, 2).join("").toUpperCase(); return (Array.from(words[0])[0] + Array.from(words[words.length - 1])[0]).toUpperCase(); } `; fs.writeFileSync(path.join(templateLibPath, "types.ts"), typesContent); // Registry const registryContent = `import type { ComponentType } from "react"; import type { PortfolioProject } from "./types"; import type { TemplateDetails } from "../features/templates/data/template-details"; import { design as signalDesign } from "./signal/design"; import { design as atelierDesign } from "./atelier/design"; import { design as nimbusDesign } from "./nimbus/design"; import { design as cipherDesign } from "./cipher/design"; export type TemplateComponent = ComponentType<{ project: PortfolioProject }>; export interface TemplateRegistryEntry { name: string; note: string; mood: string; audience: string; strengths: string[]; image: string; loader: () => Promise<{ default: TemplateComponent }>; design: TemplateDetails; isPremium?: boolean; } export const templatesRegistry = { signal: { name: "Signal", note: "Mock signal note", mood: "Mock signal mood", audience: "Mock signal audience", strengths: ["Mock signal strengths"], image: "/templates/signal-template-preview.png", isPremium: false, loader: () => import("./signal/SignalTemplate"), design: signalDesign, }, atelier: { name: "Atelier", note: "Mock atelier note", mood: "Mock atelier mood", audience: "Mock atelier audience", strengths: ["Mock atelier strengths"], image: "/templates/atelier-template-preview.png", isPremium: false, loader: () => import("./atelier/AtelierTemplate"), design: atelierDesign, }, nimbus: { name: "Nimbus", note: "Mock nimbus note", mood: "Mock nimbus mood", audience: "Mock nimbus audience", strengths: ["Mock nimbus strengths"], image: "/templates/nimbus-template-preview.png", isPremium: true, loader: () => import("./nimbus/NimbusTemplate"), design: nimbusDesign, }, cipher: { name: "Cipher", note: "Mock cipher note", mood: "Mock cipher mood", audience: "Mock cipher audience", strengths: ["Mock cipher strengths"], image: "/templates/cipher-template-preview.png", isPremium: true, loader: () => import("./cipher/CipherTemplate"), design: cipherDesign, }, } satisfies Record; export type PrivateTemplateId = keyof typeof templatesRegistry; export const templateLoaders = Object.fromEntries( Object.entries(templatesRegistry).map(([id, entry]) => [id, entry.loader]) ) as Record Promise<{ default: TemplateComponent }>>; export function hasPrivateTemplate(id: string): id is PrivateTemplateId { return id in templatesRegistry; } `; fs.writeFileSync(registryFilePath, registryContent); // Helper template body generator function createTemplateComponentCode(templateName, isSignal = false) { return `import React from "react"; import { safeExternalUrl, itemText, type PortfolioProject } from "../types"; import "./styles.css"; export default function ${templateName}Template({ project }: { project: PortfolioProject }) { const visibleSections = project.sections.filter((s) => s.visible); ${isSignal ? `const projectsSection = visibleSections.find((s) => s.type === "projects");` : ""} return (
Mock ${templateName} Template: {project.identity.name}
${isSignal ? `{projectsSection && Explore work}` : ""} {visibleSections.map((section) => { if (section.type === "contact") { return (

{section.title}

{section.subtitle ?

{section.subtitle}

: null} {project.socialLinks.map((link) => { const href = safeExternalUrl(link.url); return href && link.label.trim() ? ( {link.label} ) : null; })}
); } return (

{section.title}

{section.subtitle ?

{section.subtitle}

: null} ${isSignal ? '{section.type === "experience" &&
}' : ""} ${isSignal ? '{section.type === "testimonials" &&
}' : ""} ${templateName === "Atelier" ? '{section.type === "services" &&
}' : ""} ${templateName === "Atelier" ? '{section.type === "testimonials" &&
}' : ""} {section.items.map((item, idx) => { const indexNum = idx + 1; const indexLabel = indexNum >= 10 ? String(indexNum) : "0" + indexNum; const degree = itemText(item, "degree"); const field = itemText(item, "field"); const degreeField = degree && field ? degree + " in " + field : ""; return (
{indexLabel} {degreeField ?

{degreeField}

: null} {Object.entries(item).map(([k, v]) => { if (k === "id" || k === "visible") return null; if (k === "link") { const href = typeof v === "string" ? safeExternalUrl(v) : ""; return href ? Link : null; } if (Array.isArray(v)) { return

{v.join(" ")}

; } if (typeof v === "string" || typeof v === "number") { return

{String(v)}

; } return null; })}
); })}
); })}
); } `; } // AtelierTemplate fs.writeFileSync( path.join(templateLibPath, "atelier", "AtelierTemplate.tsx"), createTemplateComponentCode("Atelier"), ); fs.writeFileSync(path.join(templateLibPath, "atelier", "styles.css"), "/* Mock styles */"); const atelierDesignContent = `export const design = { positioning: "Mock atelier positioning", fonts: "Mock atelier fonts", motion: "Mock atelier motion", palette: "Mock atelier palette", layout: "Mock atelier layout", componentLanguage: "Mock atelier componentLanguage", contentModel: ["Mock atelier contentModel"], colorScheme: [{ name: "Mock", value: "#000", className: "bg-black" }], bestFor: ["Mock atelier bestFor"], designNotes: ["Mock atelier designNotes"], }; `; fs.writeFileSync(path.join(templateLibPath, "atelier", "design.ts"), atelierDesignContent); // SignalTemplate fs.writeFileSync( path.join(templateLibPath, "signal", "SignalTemplate.tsx"), createTemplateComponentCode("Signal", true), ); fs.writeFileSync(path.join(templateLibPath, "signal", "styles.css"), "/* Mock styles */"); // SignalDesign const signalDesignContent = `export const design = { positioning: "Mock signal positioning", fonts: "Mock signal fonts", motion: "Mock signal motion", palette: "Mock signal palette", layout: "Mock signal layout", componentLanguage: "Mock signal componentLanguage", contentModel: ["Mock signal contentModel"], colorScheme: [{ name: "Mock", value: "#000", className: "bg-black" }], bestFor: ["Mock signal bestFor"], designNotes: ["Mock signal designNotes"], }; `; fs.writeFileSync(path.join(templateLibPath, "signal", "design.ts"), signalDesignContent); // NimbusTemplate fs.writeFileSync( path.join(templateLibPath, "nimbus", "NimbusTemplate.tsx"), createTemplateComponentCode("Nimbus"), ); fs.writeFileSync(path.join(templateLibPath, "nimbus", "styles.css"), "/* Mock styles */"); // NimbusDesign const nimbusDesignContent = `export const design = { positioning: "Mock nimbus positioning", fonts: "Mock nimbus fonts", motion: "Mock nimbus motion", palette: "Mock nimbus palette", layout: "Mock nimbus layout", componentLanguage: "Mock nimbus componentLanguage", contentModel: ["Mock nimbus contentModel"], colorScheme: [{ name: "Mock", value: "#000", className: "bg-black" }], bestFor: ["Mock nimbus bestFor"], designNotes: ["Mock nimbus designNotes"], }; `; fs.writeFileSync(path.join(templateLibPath, "nimbus", "design.ts"), nimbusDesignContent); // CipherTemplate const cipherContent = `import React from "react"; import { safeExternalUrl, type PortfolioProject } from "../types"; import "./styles.css"; export default function CipherTemplate({ project }: { project: PortfolioProject }) { const visibleSections = project.sections.filter((s) => s.visible); return (
Mock Cipher Template: {project.identity.name}
{visibleSections.map((section) => (

{section.title}

{section.items.map((item, idx) => (
{Object.entries(item).map(([k, v]) => { if (k === "id" || k === "visible") return null; if (k === "link") { const href = typeof v === "string" ? safeExternalUrl(v) : ""; return href ? Link : null; } if (Array.isArray(v)) return

{v.join(" ")}

; if (typeof v === "string" || typeof v === "number") return

{String(v)}

; return null; })}
))}
))}
); } `; fs.writeFileSync(path.join(templateLibPath, "cipher", "CipherTemplate.tsx"), cipherContent); fs.writeFileSync(path.join(templateLibPath, "cipher", "styles.css"), "/* Mock styles */"); // CipherDesign const cipherDesignContent = `export const design = { positioning: "Mock cipher positioning", fonts: "Mock cipher fonts", motion: "Mock cipher motion", palette: "Mock cipher palette", layout: "Mock cipher layout", componentLanguage: "Mock cipher componentLanguage", contentModel: ["Mock cipher contentModel"], colorScheme: [{ name: "Mock", value: "#000", className: "bg-black" }], bestFor: ["Mock cipher bestFor"], designNotes: ["Mock cipher designNotes"], }; `; fs.writeFileSync(path.join(templateLibPath, "cipher", "design.ts"), cipherDesignContent); console.log("Mock templates set up successfully."); try { console.log("Formatting mocked files..."); execSync("npx prettier --write apps/portfolio/template-library", { stdio: "inherit" }); console.log("Formatting completed."); } catch (err) { console.error("Failed to run prettier on mocked files:", err.message); }