UNPKG

handoff-app

Version:

Automated documentation toolchain for building client side documentation from figma

184 lines (160 loc) 6.51 kB
import fs from 'fs-extra'; import path from 'path'; import Handoff from '../../../index'; import { ComponentListObject, TransformComponentTokensResult } from '../types'; /** * Merges values from a source object into a target object, returning a new object. * For each key present in either object: * - If the key is listed in preserveKeys and the source value is undefined, null, or an empty string, * the target's value is preserved. * - Otherwise, the value from the source is used (even if undefined, null, or empty string). * This is useful for partial updates where some properties should not be overwritten unless explicitly set. * * @param target - The original object to merge into * @param source - The object containing new values * @param preserveKeys - Keys for which the target's value should be preserved if the source value is undefined, null, or empty string * @returns A new object with merged values */ function updateObject<T extends TransformComponentTokensResult>(target: T, source: Partial<T>, preserveKeys: string[] = []): T { // Collect all unique keys from both target and source const allKeys = Array.from(new Set([...Object.keys(target), ...Object.keys(source)])); return allKeys.reduce( (acc, key) => { const sourceValue = source[key as keyof T]; const targetValue = target[key as keyof T]; // Preserve existing values for specified keys when source value is undefined if (preserveKeys.includes(key) && (sourceValue === undefined || sourceValue === null || sourceValue === '')) { acc[key as keyof T] = targetValue; } else { acc[key as keyof T] = sourceValue; } return acc; }, { ...target } ); } export const getAPIPath = (handoff: Handoff) => { const apiPath = path.resolve(handoff.workingPath, `public/api`); const componentPath = path.resolve(handoff.workingPath, `public/api/component`); // Ensure the public API path exists if (!fs.existsSync(componentPath)) { fs.mkdirSync(componentPath, { recursive: true }); } return apiPath; }; /** * Build the preview API from the component data * @param handoff * @param componentData */ const writeComponentSummaryAPI = async (handoff: Handoff, componentData: ComponentListObject[]) => { componentData.sort((a, b) => a.title.localeCompare(b.title)); await fs.writeFile(path.resolve(getAPIPath(handoff), 'components.json'), JSON.stringify(componentData, null, 2)); }; export const writeComponentApi = async ( id: string, component: TransformComponentTokensResult, handoff: Handoff, preserveKeys: string[] = [] ) => { const outputDirPath = path.resolve(getAPIPath(handoff), 'component'); const outputFilePath = path.resolve(outputDirPath, `${id}.json`); if (fs.existsSync(outputFilePath)) { const existingJson = await fs.readFile(outputFilePath, 'utf8'); if (existingJson) { try { const existingData = JSON.parse(existingJson) as TransformComponentTokensResult; // Special case: always allow page to be cleared when undefined // This handles the case where page slices are removed const finalPreserveKeys = component.page === undefined ? preserveKeys.filter((key) => key !== 'page') : preserveKeys; const mergedData = updateObject(existingData, component, finalPreserveKeys); await fs.writeFile(outputFilePath, JSON.stringify(mergedData, null, 2)); return; } catch (_) { // Unable to parse existing file } } } if (!fs.existsSync(outputDirPath)) { fs.mkdirSync(outputDirPath, { recursive: true }); } await fs.writeFile(outputFilePath, JSON.stringify(component, null, 2)); }; /** * Update the main component summary API with the new component data * @param handoff * @param componentData */ export const updateComponentSummaryApi = async (handoff: Handoff, componentData: ComponentListObject[], isFullRebuild: boolean = false) => { if (isFullRebuild) { // Full rebuild: replace the entire file await writeComponentSummaryAPI(handoff, componentData); return; } // Partial update: merge with existing data const apiPath = path.resolve(handoff.workingPath, 'public/api/components.json'); let existingData: ComponentListObject[] = []; if (fs.existsSync(apiPath)) { try { const existing = await fs.readFile(apiPath, 'utf8'); existingData = JSON.parse(existing); } catch { // Corrupt or missing JSON — treat as empty existingData = []; } } // Replace existing entries with same ID const incomingIds = new Set(componentData.map((c) => c.id)); const merged = [...componentData, ...existingData.filter((c) => !incomingIds.has(c.id))]; // Always write the file (even if merged is empty) await writeComponentSummaryAPI(handoff, merged); }; /** * Read the component API data * @param handoff * @param id * @returns */ export const readComponentApi = async (handoff: Handoff, id: string): Promise<TransformComponentTokensResult | null> => { const outputFilePath = path.resolve(getAPIPath(handoff), 'component', `${id}.json`); if (fs.existsSync(outputFilePath)) { try { const existingJson = await fs.readFile(outputFilePath, 'utf8'); if (existingJson) { return JSON.parse(existingJson) as TransformComponentTokensResult; } } catch (_) { // Unable to parse existing file } } return null; }; /** * Read the component metadata/summary from the component JSON file * @param handoff * @param id * @returns The component summary or null if not found */ export const readComponentMetadataApi = async (handoff: Handoff, id: string): Promise<ComponentListObject | null> => { const componentData = await readComponentApi(handoff, id); if (!componentData) { return null; } // Construct the summary from the full component data return { id, title: componentData.title, description: componentData.description, type: componentData.type, group: componentData.group, image: componentData.image ? componentData.image : '', figma: componentData.figma ? componentData.figma : '', figmaComponentId: componentData.figmaComponentId, categories: componentData.categories ? componentData.categories : [], tags: componentData.tags ? componentData.tags : [], properties: componentData.properties, previews: componentData.previews, path: `/api/component/${id}.json`, }; }; export default writeComponentSummaryAPI;