UNPKG

handoff-app

Version:

Automated documentation toolchain for building client side documentation from figma

326 lines (325 loc) 17.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComponentSegment = void 0; exports.processComponents = processComponents; const cloneDeep_1 = __importDefault(require("lodash/cloneDeep")); const cache_1 = require("../../../cache"); const logger_1 = require("../../../utils/logger"); const schema_1 = require("../../utils/schema"); const types_1 = require("../types"); const api_1 = require("./api"); const css_1 = __importDefault(require("./css")); const html_1 = __importDefault(require("./html")); const javascript_1 = __importDefault(require("./javascript")); const defaultComponent = { id: '', title: 'Untitled', figma: '', image: '', description: 'No description provided', preview: 'No preview available', type: types_1.ComponentType.Element, group: 'default', should_do: [], should_not_do: [], categories: [], tags: [], previews: {}, properties: {}, code: '', html: '', format: 'html', js: null, css: null, sass: null, }; /** * Types of component segments that can be updated */ var ComponentSegment; (function (ComponentSegment) { ComponentSegment["JavaScript"] = "javascript"; ComponentSegment["Style"] = "style"; ComponentSegment["Previews"] = "previews"; ComponentSegment["Validation"] = "validation"; })(ComponentSegment || (exports.ComponentSegment = ComponentSegment = {})); /** * Returns a normalized build plan describing which component segments need rebuilding. * * The plan consolidates the conditional logic for: * - Full builds (no segment specified) where every segment should be regenerated * - Targeted rebuilds where only the requested segment runs * - Validation sweeps that only rebuild segments with missing artifacts * * @param segmentToProcess Optional segment identifier coming from the caller * @param existingData Previously persisted component output (if any) */ const createComponentBuildPlan = (segmentToProcess, existingData) => { const isValidationMode = segmentToProcess === ComponentSegment.Validation; const isFullBuild = !segmentToProcess; const previewsMissing = !(existingData === null || existingData === void 0 ? void 0 : existingData.code) || Object.values((existingData === null || existingData === void 0 ? void 0 : existingData.previews) || {}).some((preview) => !(preview === null || preview === void 0 ? void 0 : preview.url)); return { js: isFullBuild || segmentToProcess === ComponentSegment.JavaScript || (isValidationMode && !(existingData === null || existingData === void 0 ? void 0 : existingData.js)), css: isFullBuild || segmentToProcess === ComponentSegment.Style || (isValidationMode && !(existingData === null || existingData === void 0 ? void 0 : existingData.css)), previews: isFullBuild || segmentToProcess === ComponentSegment.Previews || (isValidationMode && previewsMissing), validationMode: isValidationMode, }; }; /** * Process components and generate their code, styles, and previews * @param handoff - The Handoff instance containing configuration and state * @param id - Optional component ID to process a specific component * @param segmentToProcess - Optional segment to update * @param options - Optional processing options including cache settings * @returns Promise resolving to an array of processed components */ function processComponents(handoff, id, segmentToProcess, options) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f, _g; const result = []; const documentationObject = yield handoff.getDocumentationObject(); const components = (_a = documentationObject === null || documentationObject === void 0 ? void 0 : documentationObject.components) !== null && _a !== void 0 ? _a : {}; const sharedStyles = yield handoff.getSharedStyles(); const runtimeComponents = (_d = (_c = (_b = handoff.runtimeConfig) === null || _b === void 0 ? void 0 : _b.entries) === null || _c === void 0 ? void 0 : _c.components) !== null && _d !== void 0 ? _d : {}; const allComponentIds = Object.keys(runtimeComponents); // Determine which components need building based on cache (when enabled) let componentsToBuild; let cache = null; let currentGlobalDeps = {}; const componentFileStatesMap = new Map(); // Only use caching when: // - useCache option is enabled // - No specific component ID is requested (full build scenario) // - No specific segment is requested (full build scenario) // - Force flag is not set const shouldUseCache = (options === null || options === void 0 ? void 0 : options.useCache) && !id && !segmentToProcess && !handoff.force; if (shouldUseCache) { logger_1.Logger.debug('Loading build cache...'); cache = yield (0, cache_1.loadBuildCache)(handoff); currentGlobalDeps = yield (0, cache_1.computeGlobalDepsState)(handoff); const globalDepsChanged = (0, cache_1.haveGlobalDepsChanged)(cache === null || cache === void 0 ? void 0 : cache.globalDeps, currentGlobalDeps); if (globalDepsChanged) { logger_1.Logger.info('Global dependencies changed, rebuilding all components'); componentsToBuild = new Set(allComponentIds); } else { logger_1.Logger.debug('Global dependencies unchanged'); componentsToBuild = new Set(); // Evaluate each component independently for (const componentId of allComponentIds) { const currentFileStates = yield (0, cache_1.computeComponentFileStates)(handoff, componentId); componentFileStatesMap.set(componentId, currentFileStates); const cachedEntry = (_e = cache === null || cache === void 0 ? void 0 : cache.components) === null || _e === void 0 ? void 0 : _e[componentId]; if (!cachedEntry) { logger_1.Logger.info(`Component '${componentId}': new component, will build`); componentsToBuild.add(componentId); } else if ((0, cache_1.hasComponentChanged)(cachedEntry, currentFileStates)) { logger_1.Logger.info(`Component '${componentId}': source files changed, will rebuild`); componentsToBuild.add(componentId); } else if (!(yield (0, cache_1.checkOutputExists)(handoff, componentId))) { logger_1.Logger.info(`Component '${componentId}': output missing, will rebuild`); componentsToBuild.add(componentId); } else { logger_1.Logger.info(`Component '${componentId}': unchanged, skipping`); } } } // Prune removed components from cache if (cache) { (0, cache_1.pruneRemovedComponents)(cache, allComponentIds); } const skippedCount = allComponentIds.length - componentsToBuild.size; if (skippedCount > 0) { logger_1.Logger.info(`Building ${componentsToBuild.size} of ${allComponentIds.length} components (${skippedCount} unchanged)`); } else if (componentsToBuild.size > 0) { logger_1.Logger.info(`Building all ${componentsToBuild.size} components`); } else { logger_1.Logger.info('All components up to date, nothing to build'); } } else { // No caching - build all requested components componentsToBuild = new Set(allComponentIds); } for (const runtimeComponentId of allComponentIds) { // Skip if specific ID requested and doesn't match if (!!id && runtimeComponentId !== id) { continue; } // Skip if caching is enabled and this component doesn't need building if (shouldUseCache && !componentsToBuild.has(runtimeComponentId)) { // Even though we're skipping the build, we need to include this component's // existing summary in the result to prevent data loss in components.json const existingSummary = yield (0, api_1.readComponentMetadataApi)(handoff, runtimeComponentId); if (existingSummary) { result.push(existingSummary); } continue; } // Select the current component metadata from the runtime config. // Separate out `type` to enforce/rewrite it during build. const runtimeComponent = runtimeComponents[runtimeComponentId]; const { type } = runtimeComponent, restMetadata = __rest(runtimeComponent, ["type"]); // Attempt to load any existing persisted component output (previous build). // This is used for incremental/partial rebuilds to retain previously generated segments when not rebuilding all. const existingData = yield (0, api_1.readComponentApi)(handoff, runtimeComponentId); // Compose the base in-memory data for building this component: // - Start from a deep clone of the defaultComponent (to avoid mutation bugs) // - Merge in metadata from the current runtime configuration (from config/docs) // - Explicitly set `type` (defaults to Element if not provided) const componentDefaults = (0, cloneDeep_1.default)(defaultComponent); // If this is NOT a figma component, add the default generic preview. // We add it here (before merge) so that if the user explicitly provided previews in 'restMetadata', // those will override this default (standard "config overrides defaults" behavior). if (!restMetadata.figmaComponentId) { componentDefaults.previews = { generic: { title: 'Default', values: {}, url: '', }, }; } let data = Object.assign(Object.assign(Object.assign({}, componentDefaults), restMetadata), { type: type || types_1.ComponentType.Element }); // buildPlan captures which segments need work for this run. const buildPlan = createComponentBuildPlan(segmentToProcess, existingData); /** * Merge segment data from existing component if this segment is *not* being rebuilt. * This ensures that when only one segment (e.g., Javascript, CSS, Previews) is being updated, * other fields retain their previous values. This avoids unnecessary overwrites or data loss * when doing segmented or partial builds. */ if (existingData) { // If we're not building JS, carry forward the previous JS output. if (!buildPlan.js) { data.js = existingData.js; } // If we're not building CSS/Sass, keep the earlier CSS and Sass outputs. if (!buildPlan.css) { data.css = existingData.css; data.sass = existingData.sass; } // If we're not building previews, preserve pre-existing HTML, code snippet, and previews. if (!buildPlan.previews) { data.html = existingData.html; data.code = existingData.code; data.previews = existingData.previews; } /** * Always keep validation results from the previous data, * unless this run is specifically doing a validation update. * This keeps validations current without unnecessary recomputation or accidental removal. */ if (!buildPlan.validationMode) { data.validations = existingData.validations; } } // Build JS if needed (new build, validation missing, or explicit segment request). if (buildPlan.js) { data = yield (0, javascript_1.default)(data, handoff); } // Build CSS if needed. if (buildPlan.css) { data = yield (0, css_1.default)(data, handoff, sharedStyles); } // Build previews (HTML, snapshots, etc) if needed. if (buildPlan.previews) { data = yield (0, html_1.default)(data, handoff, components); } /** * Run validation if explicitly requested and a hook is configured. * This allows custom logic to assess the validity of the generated component data. */ if (buildPlan.validationMode && ((_g = (_f = handoff.config) === null || _f === void 0 ? void 0 : _f.hooks) === null || _g === void 0 ? void 0 : _g.validateComponent)) { const validationResults = yield handoff.config.hooks.validateComponent(data); data.validations = validationResults; } // Attach the resolved sharedStyles to the component data for persistence and downstream usage. data.sharedStyles = sharedStyles; // Ensure that every property within the properties array/object contains an 'id' field. // This guarantees unique identification for property entries, which is useful for updates and API consumers. data.properties = (0, schema_1.ensureIds)(data.properties); // Write the updated component data to the API file for external access and caching. yield (0, api_1.writeComponentApi)(runtimeComponentId, data, handoff, []); // Build the summary metadata for this component. const summary = buildComponentSummary(runtimeComponentId, data); // Add to the cumulative results, to later update the global components summary file. result.push(summary); // Update cache entry for this component after successful build if (shouldUseCache) { if (!cache) { cache = (0, cache_1.createEmptyCache)(); } const fileStates = componentFileStatesMap.get(runtimeComponentId); if (fileStates) { (0, cache_1.updateComponentCacheEntry)(cache, runtimeComponentId, fileStates); } else { // Compute file states if not already computed (e.g., when global deps changed) const computedFileStates = yield (0, cache_1.computeComponentFileStates)(handoff, runtimeComponentId); (0, cache_1.updateComponentCacheEntry)(cache, runtimeComponentId, computedFileStates); } } } // Save the updated cache if (shouldUseCache && cache) { cache.globalDeps = currentGlobalDeps; yield (0, cache_1.saveBuildCache)(handoff, cache); } // Always merge and write summary file, even if no components processed const isFullRebuild = !id; yield (0, api_1.updateComponentSummaryApi)(handoff, result, isFullRebuild); return result; }); } /** * Build a summary for the component list * @param id * @param data * @returns */ const buildComponentSummary = (id, data) => { return { id, title: data.title, description: data.description, type: data.type, group: data.group, image: data.image ? data.image : '', figma: data.figma ? data.figma : '', categories: data.categories ? data.categories : [], tags: data.tags ? data.tags : [], properties: data.properties, previews: data.previews, path: `/api/component/${id}.json`, }; }; exports.default = processComponents;