UNPKG

handoff-app

Version:

Automated documentation toolchain for building client side documentation from figma

591 lines (590 loc) 25.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CoreTypes = exports.CoreTransformerUtils = exports.CoreTransformers = exports.initIntegrationObject = void 0; const chalk_1 = __importDefault(require("chalk")); require("dotenv/config"); const fs_extra_1 = __importDefault(require("fs-extra")); const handoff_core_1 = require("handoff-core"); const lodash_1 = require("lodash"); const path_1 = __importDefault(require("path")); const semver_1 = __importDefault(require("semver")); const app_1 = __importStar(require("./app")); const eject_1 = require("./cli/eject"); const make_1 = require("./cli/make"); const config_1 = require("./config"); const pipeline_1 = __importStar(require("./pipeline")); const component_1 = require("./transformers/preview/component"); const builder_1 = __importStar(require("./transformers/preview/component/builder")); const css_1 = require("./transformers/preview/component/css"); const javascript_1 = require("./transformers/preview/component/javascript"); const utils_1 = require("./utils"); const fs_1 = require("./utils/fs"); class Handoff { constructor(debug, force, config) { this.debug = false; this.force = false; this.modulePath = path_1.default.resolve(__filename, '../..'); this.workingPath = process.cwd(); this.exportsDirectory = 'exported'; this.sitesDirectory = 'out'; this._initialArgs = {}; this._configFilePaths = []; this._initialArgs = { debug, force, config }; this.construct(debug, force, config); } construct(debug, force, config) { this.config = null; this.debug = debug !== null && debug !== void 0 ? debug : false; this.force = force !== null && force !== void 0 ? force : false; this.init(config); global.handoff = this; } init(configOverride) { var _a, _b; const config = initConfig(configOverride !== null && configOverride !== void 0 ? configOverride : {}); this.config = config; this.exportsDirectory = (_a = config.exportsOutputDirectory) !== null && _a !== void 0 ? _a : this.exportsDirectory; this.sitesDirectory = (_b = config.sitesOutputDirectory) !== null && _b !== void 0 ? _b : this.exportsDirectory; [this.integrationObject, this._configFilePaths] = (0, exports.initIntegrationObject)(this); return this; } reload() { this.construct(this._initialArgs.debug, this._initialArgs.force, this._initialArgs.config); return this; } preRunner(validate) { if (!this.config) { throw Error('Handoff not initialized'); } if (validate) { this.config = validateConfig(this.config); } return this; } fetch() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, pipeline_1.default)(this); return this; }); } component(name) { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); if (name) { name = name.replace('.hbs', ''); yield (0, builder_1.default)(this, name); } else { yield (0, pipeline_1.buildComponents)(this); } return this; }); } build() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, app_1.default)(this); return this; }); } ejectConfig() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, eject_1.ejectConfig)(this); return this; }); } ejectExportables() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, eject_1.ejectExportables)(this); return this; }); } ejectPages() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, eject_1.ejectPages)(this); return this; }); } ejectTheme() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, eject_1.ejectTheme)(this); return this; }); } makeExportable(type, name) { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, make_1.makeExportable)(this, type, name); return this; }); } makeTemplate(component, state) { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, make_1.makeTemplate)(this, component, state); return this; }); } makePage(name, parent) { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, make_1.makePage)(this, name, parent); return this; }); } makeComponent(name) { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, make_1.makeComponent)(this, name); return this; }); } makeIntegrationStyles() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, javascript_1.buildMainJS)(this); yield (0, css_1.buildMainCss)(this); return this; }); } start() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, app_1.watchApp)(this); return this; }); } dev() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, app_1.devApp)(this); return this; }); } validateComponents() { return __awaiter(this, void 0, void 0, function* () { this.preRunner(); yield (0, builder_1.default)(this, undefined, builder_1.ComponentSegment.Validation); return this; }); } /** * Retrieves the documentation object, using cached version if available * @returns {Promise<CoreTypes.IDocumentationObject | undefined>} The documentation object or undefined if not found */ getDocumentationObject() { return __awaiter(this, void 0, void 0, function* () { if (this._documentationObjectCache) { return this._documentationObjectCache; } const documentationObject = yield this.readJsonFile(this.getTokensFilePath()); this._documentationObjectCache = documentationObject; return documentationObject; }); } /** * Retrieves shared styles, using cached version if available * @returns {Promise<string | null>} The shared styles string or null if not found */ getSharedStyles() { return __awaiter(this, void 0, void 0, function* () { if (this._sharedStylesCache !== undefined) { return this._sharedStylesCache; } const sharedStyles = yield (0, component_1.processSharedStyles)(this); this._sharedStylesCache = sharedStyles; return sharedStyles; }); } getRunner() { return __awaiter(this, void 0, void 0, function* () { if (!!this._handoffRunner) { return this._handoffRunner; } const apiCredentials = { projectId: this.config.figma_project_id, accessToken: this.config.dev_access_token, }; const legacyDefinitions = yield this.getLegacyDefinitions(); const useLegacyDefintions = !!legacyDefinitions; // Initialize the provider const provider = useLegacyDefintions ? handoff_core_1.Providers.RestApiLegacyDefinitionsProvider(apiCredentials, legacyDefinitions) : handoff_core_1.Providers.RestApiProvider(apiCredentials); this._handoffRunner = (0, handoff_core_1.Handoff)(provider, { options: { transformer: this.integrationObject.options, }, }, { log: (msg) => { console.log(msg); }, err: (msg) => { console.log(chalk_1.default.red(msg)); }, warn: (msg) => { console.log(chalk_1.default.yellow(msg)); }, success: (msg) => { console.log(chalk_1.default.green(msg)); }, }); return this._handoffRunner; }); } /** * Returns configured legacy component definitions in array form. * @deprecated Will be removed before 1.0.0 release. */ getLegacyDefinitions() { return __awaiter(this, void 0, void 0, function* () { try { const sourcePath = path_1.default.resolve(this.workingPath, 'exportables'); if (!fs_extra_1.default.existsSync(sourcePath)) { return null; } const definitionPaths = (0, fs_1.findFilesByExtension)(sourcePath, '.json'); const exportables = definitionPaths .map((definitionPath) => { const defBuffer = fs_extra_1.default.readFileSync(definitionPath); const exportable = JSON.parse(defBuffer.toString()); const exportableOptions = {}; (0, lodash_1.merge)(exportableOptions, exportable.options); exportable.options = exportableOptions; return exportable; }) .filter(utils_1.filterOutNull); return exportables ? exportables : null; } catch (e) { return []; } }); } /** * Gets the output path for the current project * @returns {string} The absolute path to the output directory */ getOutputPath() { return path_1.default.resolve(this.workingPath, this.exportsDirectory, this.config.figma_project_id); } /** * Gets the path to the tokens.json file * @returns {string} The absolute path to the tokens.json file */ getTokensFilePath() { return path_1.default.join(this.getOutputPath(), 'tokens.json'); } /** * Gets the path to the preview.json file * @returns {string} The absolute path to the preview.json file */ getPreviewFilePath() { return path_1.default.join(this.getOutputPath(), 'preview.json'); } /** * Gets the path to the changelog.json file * @returns {string} The absolute path to the changelog.json file */ getChangelogFilePath() { return path_1.default.join(this.getOutputPath(), 'changelog.json'); } /** * Gets the path to the tokens directory * @returns {string} The absolute path to the tokens directory */ getVariablesFilePath() { return path_1.default.join(this.getOutputPath(), 'tokens'); } /** * Gets the path to the icons.zip file * @returns {string} The absolute path to the icons.zip file */ getIconsZipFilePath() { return path_1.default.join(this.getOutputPath(), 'icons.zip'); } /** * Gets the path to the logos.zip file * @returns {string} The absolute path to the logos.zip file */ getLogosZipFilePath() { return path_1.default.join(this.getOutputPath(), 'logos.zip'); } /** * Gets the list of config file paths * @returns {string[]} Array of absolute paths to config files */ getConfigFilePaths() { return this._configFilePaths; } /** * Clears all cached data * @returns {void} */ clearCaches() { this._documentationObjectCache = undefined; this._sharedStylesCache = undefined; } /** * Reads and parses a JSON file * @param {string} path - Path to the JSON file * @returns {Promise<any>} The parsed JSON content or undefined if file cannot be read */ readJsonFile(path) { return __awaiter(this, void 0, void 0, function* () { try { return yield fs_extra_1.default.readJSON(path); } catch (e) { return undefined; } }); } } const initConfig = (configOverride) => { let config = {}; const possibleConfigFiles = ['handoff.config.json', 'handoff.config.js', 'handoff.config.cjs']; // Find the first existing config file const configFile = possibleConfigFiles.find((file) => fs_extra_1.default.existsSync(path_1.default.resolve(process.cwd(), file))); if (configFile) { const configPath = path_1.default.resolve(process.cwd(), configFile); if (configFile.endsWith('.json')) { const defBuffer = fs_extra_1.default.readFileSync(configPath); config = JSON.parse(defBuffer.toString()); } else if (configFile.endsWith('.js') || configFile.endsWith('.cjs')) { // Invalidate require cache to ensure fresh read delete require.cache[require.resolve(configPath)]; const importedConfig = require(configPath); config = importedConfig.default || importedConfig; } } // Apply overrides if provided if (configOverride) { Object.keys(configOverride).forEach((key) => { const value = configOverride[key]; if (value !== undefined) { config[key] = value; } }); } const returnConfig = Object.assign(Object.assign({}, (0, config_1.defaultConfig)()), config); return returnConfig; }; const initIntegrationObject = (handoff) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j; var _k; const configFiles = []; const result = { options: {}, entries: { integration: undefined, // scss bundle: undefined, // js components: {}, }, }; if (!!((_a = handoff.config.entries) === null || _a === void 0 ? void 0 : _a.scss)) { result.entries.integration = path_1.default.resolve(handoff.workingPath, (_b = handoff.config.entries) === null || _b === void 0 ? void 0 : _b.scss); } //console.log('result.entries.integration', handoff.config.entries, path.resolve(handoff.workingPath, handoff.config.entries?.js)); if (!!((_c = handoff.config.entries) === null || _c === void 0 ? void 0 : _c.js)) { result.entries.bundle = path_1.default.resolve(handoff.workingPath, (_d = handoff.config.entries) === null || _d === void 0 ? void 0 : _d.js); } else { console.log(chalk_1.default.red('No js entry found in config'), handoff.debug ? `Path: ${path_1.default.resolve(handoff.workingPath, (_e = handoff.config.entries) === null || _e === void 0 ? void 0 : _e.js)}` : ''); } if ((_g = (_f = handoff.config.entries) === null || _f === void 0 ? void 0 : _f.components) === null || _g === void 0 ? void 0 : _g.length) { const componentPaths = handoff.config.entries.components.flatMap(getComponentsForPath); for (const componentPath of componentPaths) { const resolvedComponentPath = path_1.default.resolve(handoff.workingPath, componentPath); const componentBaseName = path_1.default.basename(resolvedComponentPath); const versions = getVersionsForComponent(resolvedComponentPath); if (!versions.length) { console.warn(`No versions found for component at: ${resolvedComponentPath}`); continue; } const latest = getLatestVersionForComponent(versions); for (const componentVersion of versions) { const resolvedComponentVersionPath = path_1.default.resolve(resolvedComponentPath, componentVersion); const possibleConfigFiles = [`${componentBaseName}.json`, `${componentBaseName}.js`, `${componentBaseName}.cjs`]; const configFileName = possibleConfigFiles.find((file) => fs_extra_1.default.existsSync(path_1.default.resolve(resolvedComponentVersionPath, file))); if (!configFileName) { console.warn(`Missing config: ${path_1.default.resolve(resolvedComponentVersionPath, possibleConfigFiles.join(' or '))}`); continue; } const resolvedComponentVersionConfigPath = path_1.default.resolve(resolvedComponentVersionPath, configFileName); configFiles.push(resolvedComponentVersionConfigPath); let component; try { if (configFileName.endsWith('.json')) { const componentJson = fs_extra_1.default.readFileSync(resolvedComponentVersionConfigPath, 'utf8'); component = JSON.parse(componentJson); } else { // Invalidate require cache to ensure fresh read delete require.cache[require.resolve(resolvedComponentVersionConfigPath)]; const importedComponent = require(resolvedComponentVersionConfigPath); component = importedComponent.default || importedComponent; } } catch (err) { console.error(`Failed to read or parse config: ${resolvedComponentVersionConfigPath}`, err); continue; } // Use component basename as the id component.id = componentBaseName; // Resolve entry paths relative to component version directory if (component.entries) { for (const entryType in component.entries) { if (component.entries[entryType]) { component.entries[entryType] = path_1.default.resolve(resolvedComponentVersionPath, component.entries[entryType]); } } } // Initialize options with safe defaults component.options || (component.options = { transformer: { defaults: {}, replace: {} }, }); (_k = component.options).transformer || (_k.transformer = { defaults: {}, replace: {} }); const transformer = component.options.transformer; (_h = transformer.cssRootClass) !== null && _h !== void 0 ? _h : (transformer.cssRootClass = null); (_j = transformer.tokenNameSegments) !== null && _j !== void 0 ? _j : (transformer.tokenNameSegments = null); // Normalize keys and values to lowercase transformer.defaults = toLowerCaseKeysAndValues(Object.assign({}, transformer.defaults)); transformer.replace = toLowerCaseKeysAndValues(Object.assign({}, transformer.replace)); // Save transformer config for latest version if (componentVersion === latest) { result.options[component.id] = transformer; } // Save full component entry under its version result.entries.components[component.id] = Object.assign(Object.assign({}, result.entries.components[component.id]), { [componentVersion]: component }); } } } return [result, Array.from(configFiles)]; }; exports.initIntegrationObject = initIntegrationObject; /** * Returns a list of component directories for a given path. * * This function inspects the immediate subdirectories of the provided `searchPath`. * - If **any** subdirectory is **not** a valid semantic version (e.g. "header", "button"), * the function assumes `searchPath` contains multiple component directories, and returns their full paths. * - If **all** subdirectories are valid semantic versions (e.g. "1.0.0", "2.1.3"), * the function assumes `searchPath` itself is a component directory, and returns it as a single-element array. * * @param searchPath - The absolute path to check for components or versioned directories. * @returns An array of string paths to component directories. */ const getComponentsForPath = (searchPath) => { // Read all entries in the given path and keep only directories const components = fs_extra_1.default .readdirSync(searchPath, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name); // If there's any non-semver-named directory, this is a directory full of components const containsComponents = components.some((name) => !semver_1.default.valid(name)); if (containsComponents) { // Return full paths to each component directory return components.map((component) => path_1.default.join(searchPath, component)); } // All subdirectories are semver versions – treat this as a single component directory return [searchPath]; }; const validateConfig = (config) => { // TODO: Check to see if the exported folder exists before we run start if (!config.figma_project_id && !process.env.HANDOFF_FIGMA_PROJECT_ID) { // check to see if we can get this from the env console.error(chalk_1.default.red('Figma project id not found in config or env. Please run `handoff-app fetch` first.')); throw new Error('Cannot initialize configuration'); } if (!config.dev_access_token && !process.env.HANDOFF_DEV_ACCESS_TOKEN) { // check to see if we can get this from the env console.error(chalk_1.default.red('Dev access token not found in config or env. Please run `handoff-app fetch` first.')); throw new Error('Cannot initialize configuration'); } return config; }; const getVersionsForComponent = (componentPath) => { const versionDirectories = fs_extra_1.default.readdirSync(componentPath); const versions = []; // The directory name must be a semver if (fs_extra_1.default.lstatSync(componentPath).isDirectory()) { // this is a directory structure. this should be the component name, // and each directory inside should be a version for (const versionDirectory of versionDirectories) { if (semver_1.default.valid(versionDirectory)) { versions.push(versionDirectory); } else { console.error(`Invalid version directory ${versionDirectory}`); } } } versions.sort(semver_1.default.rcompare); return versions; }; const getLatestVersionForComponent = (versions) => versions.sort(semver_1.default.rcompare)[0]; const toLowerCaseKeysAndValues = (obj) => { const loweredObj = {}; for (const key in obj) { const lowerKey = key.toLowerCase(); const value = obj[key]; if (typeof value === 'string') { loweredObj[lowerKey] = value.toLowerCase(); } else if (typeof value === 'object' && value !== null) { loweredObj[lowerKey] = toLowerCaseKeysAndValues(value); } else { loweredObj[lowerKey] = value; // For non-string values } } return loweredObj; }; // Export transformers and types from handoff-core var handoff_core_2 = require("handoff-core"); Object.defineProperty(exports, "CoreTransformers", { enumerable: true, get: function () { return handoff_core_2.Transformers; } }); Object.defineProperty(exports, "CoreTransformerUtils", { enumerable: true, get: function () { return handoff_core_2.TransformerUtils; } }); Object.defineProperty(exports, "CoreTypes", { enumerable: true, get: function () { return handoff_core_2.Types; } }); exports.default = Handoff;