handoff-app
Version:
Automated documentation toolchain for building client side documentation from figma
363 lines (362 loc) • 19 kB
JavaScript
;
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.buildComponents = exports.zipAssets = exports.readPrevJSONFile = void 0;
const archiver_1 = __importDefault(require("archiver"));
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 stream = __importStar(require("node:stream"));
const path_1 = __importDefault(require("path"));
const app_1 = __importDefault(require("./app"));
const changelog_1 = __importDefault(require("./changelog"));
const documentation_object_1 = require("./documentation-object");
const component_1 = require("./transformers/preview/component");
const prompt_1 = require("./utils/prompt");
/**
* Read Previous Json File
* @param path
* @returns
*/
const readPrevJSONFile = (path) => __awaiter(void 0, void 0, void 0, function* () {
try {
return yield fs_extra_1.default.readJSON(path);
}
catch (e) {
return undefined;
}
});
exports.readPrevJSONFile = readPrevJSONFile;
/**
* Zips the contents of a directory and writes the resulting archive to a writable stream.
*
* @param dirPath - The path to the directory whose contents will be zipped.
* @param destination - A writable stream where the zip archive will be written.
* @returns A Promise that resolves with the destination stream when the archive has been finalized.
* @throws Will throw an error if the archiving process fails.
*/
const zip = (dirPath, destination) => __awaiter(void 0, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const archive = (0, archiver_1.default)('zip', {
zlib: { level: 9 },
});
// Set up event handlers
archive.on('error', reject);
destination.on('error', reject);
// When the destination closes, resolve the promise
destination.on('close', () => resolve(destination));
archive.pipe(destination);
fs_extra_1.default.readdir(dirPath)
.then((fontDir) => {
for (const file of fontDir) {
const filePath = path_1.default.join(dirPath, file);
archive.append(fs_extra_1.default.createReadStream(filePath), { name: path_1.default.basename(file) });
}
return archive.finalize();
})
.catch(reject);
});
});
const zipAssets = (assets, destination) => __awaiter(void 0, void 0, void 0, function* () {
const archive = (0, archiver_1.default)('zip', {
zlib: { level: 9 }, // Sets the compression level.
});
// good practice to catch this error explicitly
archive.on('error', function (err) {
throw err;
});
archive.pipe(destination);
assets.forEach((asset) => {
archive.append(asset.data, { name: asset.path });
});
yield archive.finalize();
return destination;
});
exports.zipAssets = zipAssets;
/**
* Build just the custom fonts
* @param documentationObject
* @returns
*/
const buildCustomFonts = (handoff, documentationObject) => __awaiter(void 0, void 0, void 0, function* () {
const { localStyles } = documentationObject;
const fontLocation = path_1.default.join(handoff === null || handoff === void 0 ? void 0 : handoff.workingPath, 'fonts');
const families = localStyles.typography.reduce((result, current) => {
return Object.assign(Object.assign({}, result), { [current.values.fontFamily]: result[current.values.fontFamily]
? // sorts and returns unique font weights
(0, lodash_1.sortedUniq)([...result[current.values.fontFamily], current.values.fontWeight].sort((a, b) => a - b))
: [current.values.fontWeight] });
}, {});
Object.keys(families).map((key) => __awaiter(void 0, void 0, void 0, function* () {
const name = key.replace(/\s/g, '');
const fontDirName = path_1.default.join(fontLocation, name);
if (fs_extra_1.default.existsSync(fontDirName)) {
const stream = fs_extra_1.default.createWriteStream(path_1.default.join(fontLocation, `${name}.zip`));
yield zip(fontDirName, stream);
const fontsFolder = path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.config.figma_project_id, 'fonts');
if (!fs_extra_1.default.existsSync(fontsFolder)) {
fs_extra_1.default.mkdirSync(fontsFolder);
}
fs_extra_1.default.copySync(fontDirName, fontsFolder);
}
}));
});
/**
* Build previews
* @param documentationObject
* @returns
*/
const buildComponents = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
yield Promise.all([(0, component_1.componentTransformer)(handoff)]);
});
exports.buildComponents = buildComponents;
/**
* Build only the styles pipeline
* @param documentationObject
*/
const buildStyles = (handoff, documentationObject) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d;
// Core transformers that should always be included
const coreTransformers = [
{
transformer: handoff_core_1.Transformers.ScssTransformer,
outDir: 'sass',
format: 'scss',
},
{
transformer: handoff_core_1.Transformers.ScssTypesTransformer,
outDir: 'types',
format: 'scss',
},
{
transformer: handoff_core_1.Transformers.CssTransformer,
outDir: 'css',
format: 'css',
},
];
// Get user-configured transformers
const userTransformers = ((_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.pipeline) === null || _b === void 0 ? void 0 : _b.transformers) || [];
// Merge core transformers with user transformers
// If a user transformer matches a core transformer, use user's outDir and format
const transformers = coreTransformers.map((coreTransformer) => {
const userTransformer = userTransformers.find((t) => t.transformer === coreTransformer.transformer);
return userTransformer ? Object.assign(Object.assign({}, coreTransformer), { outDir: userTransformer.outDir, format: userTransformer.format }) : coreTransformer;
});
// Add any additional user transformers that aren't core transformers
userTransformers.forEach((userTransformer) => {
if (!coreTransformers.some((core) => core.transformer === userTransformer.transformer)) {
transformers.push(userTransformer);
}
});
const baseDir = handoff.getVariablesFilePath();
const runner = yield handoff.getRunner();
// Create transformer instances and transform documentation object
const transformedFiles = transformers.map(({ transformer }) => {
var _a;
return ({
transformer,
files: runner.transform(transformer({ useVariables: (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.useVariables }), documentationObject),
});
});
// Ensure base directory exists
yield fs_extra_1.default.ensureDir(baseDir);
// Create all necessary subdirectories
const directories = transformers.map(({ outDir }) => path_1.default.join(baseDir, outDir));
yield Promise.all(directories.map((dir) => fs_extra_1.default.ensureDir(dir)));
// Special case for SD tokens components directory
const sdTransformer = transformers.find((t) => t.transformer === handoff_core_1.Transformers.StyleDictionaryTransformer);
if (sdTransformer) {
const sdFiles = (_c = transformedFiles.find((t) => t.transformer === handoff_core_1.Transformers.StyleDictionaryTransformer)) === null || _c === void 0 ? void 0 : _c.files;
if (sdFiles === null || sdFiles === void 0 ? void 0 : sdFiles.components) {
yield Promise.all(Object.keys(sdFiles.components).map((name) => fs_extra_1.default.ensureDir(path_1.default.join(baseDir, sdTransformer.outDir, name))));
}
}
// Write all files
const writePromises = transformedFiles.flatMap(({ transformer: TransformerClass, files }) => {
const { outDir, format } = transformers.find((t) => t.transformer === TransformerClass) || {};
if (!outDir || !files)
return [];
const componentPromises = Object.entries(files.components || {}).map(([name, content]) => {
const filePath = TransformerClass === handoff_core_1.Transformers.StyleDictionaryTransformer
? path_1.default.join(baseDir, outDir, name, `${name}.tokens.json`)
: path_1.default.join(baseDir, outDir, `${name}.${format}`);
return fs_extra_1.default.writeFile(filePath, content);
});
const designPromises = Object.entries(files.design || {}).map(([name, content]) => {
const filePath = TransformerClass === handoff_core_1.Transformers.StyleDictionaryTransformer
? path_1.default.join(baseDir, outDir, `${name}.tokens.json`)
: path_1.default.join(baseDir, outDir, `${name}.${format}`);
return fs_extra_1.default.writeFile(filePath, content);
});
return [...componentPromises, ...designPromises];
});
// Generate tokens-map.json
const mapFiles = (_d = transformedFiles.find((t) => t.transformer === handoff_core_1.Transformers.MapTransformer)) === null || _d === void 0 ? void 0 : _d.files;
if (mapFiles) {
const tokensMapContent = JSON.stringify(Object.entries(mapFiles.components || {}).reduce((acc, [_, data]) => (Object.assign(Object.assign({}, acc), JSON.parse(data))), Object.assign(Object.assign(Object.assign({}, JSON.parse(mapFiles.design.colors)), JSON.parse(mapFiles.design.typography)), JSON.parse(mapFiles.design.effects))), null, 2);
writePromises.push(fs_extra_1.default.writeFile(path_1.default.join(handoff.getOutputPath(), 'tokens-map.json'), tokensMapContent));
}
// Write all files
yield Promise.all(writePromises);
});
const validateHandoffRequirements = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
let requirements = false;
const result = process.versions;
if (result && result.node) {
if (parseInt(result.node) >= 16) {
requirements = true;
}
}
else {
// couldn't find the right version, but ...
}
if (!requirements) {
console.log(chalk_1.default.redBright('Handoff Installation failed'));
console.log(chalk_1.default.yellow('- Please update node to at least Node 16 https://nodejs.org/en/download. \n- You can read more about installing handoff at https://www.handoff.com/docs/'));
throw new Error('Could not run handoff');
}
});
/**
* Validate the figma auth tokens
* @param handoff
*/
const validateFigmaAuth = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
let DEV_ACCESS_TOKEN = handoff.config.dev_access_token;
let FIGMA_PROJECT_ID = handoff.config.figma_project_id;
if (DEV_ACCESS_TOKEN && FIGMA_PROJECT_ID) {
return;
}
let missingEnvVars = false;
if (!DEV_ACCESS_TOKEN) {
missingEnvVars = true;
console.log(chalk_1.default.yellow(`Figma developer access token not found. You can supply it as an environment variable or .env file at HANDOFF_DEV_ACCESS_TOKEN.
Use these instructions to generate them ${chalk_1.default.blue(`https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens`)}\n`));
DEV_ACCESS_TOKEN = yield (0, prompt_1.maskPrompt)(chalk_1.default.green('Figma Developer Key: '));
}
if (!FIGMA_PROJECT_ID) {
missingEnvVars = true;
console.log(chalk_1.default.yellow(`\n\nFigma project id not found. You can supply it as an environment variable or .env file at HANDOFF_FIGMA_PROJECT_ID.
You can find this by looking at the url of your Figma file. If the url is ${chalk_1.default.blue(`https://www.figma.com/file/IGYfyraLDa0BpVXkxHY2tE/Starter-%5BV2%5D`)}
your id would be IGYfyraLDa0BpVXkxHY2tE\n`));
FIGMA_PROJECT_ID = yield (0, prompt_1.maskPrompt)(chalk_1.default.green('Figma Project Id: '));
}
if (missingEnvVars) {
console.log(chalk_1.default.yellow(`\n\nYou supplied at least one required variable. We can write these variables to a local env file for you to make it easier to run the pipeline in the future.\n`));
const writeEnvFile = yield (0, prompt_1.prompt)(chalk_1.default.green('Write environment variables to .env file? (y/n): '));
if (writeEnvFile !== 'y') {
console.log(chalk_1.default.green(`Skipping .env file creation. You will need to supply these variables in the future.\n`));
}
else {
const envFilePath = path_1.default.resolve(handoff.workingPath, '.env');
const envFileContent = `
HANDOFF_DEV_ACCESS_TOKEN="${DEV_ACCESS_TOKEN}"
HANDOFF_FIGMA_PROJECT_ID="${FIGMA_PROJECT_ID}"
`;
try {
const fileExists = yield fs_extra_1.default
.access(envFilePath)
.then(() => true)
.catch(() => false);
if (fileExists) {
yield fs_extra_1.default.appendFile(envFilePath, envFileContent);
console.log(chalk_1.default.green(`\nThe .env file was found and updated with new content. Since these are sensitive variables, please do not commit this file.\n`));
}
else {
yield fs_extra_1.default.writeFile(envFilePath, envFileContent.replace(/^\s*[\r\n]/gm, ''));
console.log(chalk_1.default.green(`\nAn .env file was created in the root of your project. Since these are sensitive variables, please do not commit this file.\n`));
}
}
catch (error) {
console.error(chalk_1.default.red('Error handling the .env file:', error));
}
}
}
handoff.config.dev_access_token = DEV_ACCESS_TOKEN;
handoff.config.figma_project_id = FIGMA_PROJECT_ID;
});
const figmaExtract = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
console.log(chalk_1.default.green(`Starting Figma data extraction.`));
let prevDocumentationObject = yield handoff.getDocumentationObject();
let changelog = (yield (0, exports.readPrevJSONFile)(handoff.getChangelogFilePath())) || [];
yield fs_extra_1.default.emptyDir(handoff.getOutputPath());
const documentationObject = yield (0, documentation_object_1.createDocumentationObject)(handoff);
const changelogRecord = (0, changelog_1.default)(prevDocumentationObject, documentationObject);
if (changelogRecord) {
changelog = [changelogRecord, ...changelog];
}
yield Promise.all([
fs_extra_1.default.writeJSON(handoff.getTokensFilePath(), documentationObject, { spaces: 2 }),
fs_extra_1.default.writeJSON(handoff.getChangelogFilePath(), changelog, { spaces: 2 }),
...(!process.env.HANDOFF_CREATE_ASSETS_ZIP_FILES || process.env.HANDOFF_CREATE_ASSETS_ZIP_FILES !== 'false'
? [
(0, exports.zipAssets)(documentationObject.assets.icons, fs_extra_1.default.createWriteStream(handoff.getIconsZipFilePath())).then((writeStream) => stream.promises.finished(writeStream)),
(0, exports.zipAssets)(documentationObject.assets.logos, fs_extra_1.default.createWriteStream(handoff.getLogosZipFilePath())).then((writeStream) => stream.promises.finished(writeStream)),
]
: []),
]);
// define the output folder
const outputFolder = path_1.default.resolve(handoff.modulePath, '.handoff', `${handoff.config.figma_project_id}`, 'public');
// ensure output folder exists
if (!fs_extra_1.default.existsSync(outputFolder)) {
yield fs_extra_1.default.promises.mkdir(outputFolder, { recursive: true });
}
// copy assets to output folder
fs_extra_1.default.copyFileSync(handoff.getIconsZipFilePath(), path_1.default.join(handoff.modulePath, '.handoff', `${handoff.config.figma_project_id}`, 'public', 'icons.zip'));
fs_extra_1.default.copyFileSync(handoff.getLogosZipFilePath(), path_1.default.join(handoff.modulePath, '.handoff', `${handoff.config.figma_project_id}`, 'public', 'logos.zip'));
return documentationObject;
});
/**
* Run the entire pipeline
*/
const pipeline = (handoff, build) => __awaiter(void 0, void 0, void 0, function* () {
if (!handoff.config) {
throw new Error('Handoff config not found');
}
console.log(chalk_1.default.green(`Starting Handoff Figma data pipeline. Checking for environment and config.\n`));
yield validateHandoffRequirements(handoff);
yield validateFigmaAuth(handoff);
const documentationObject = yield figmaExtract(handoff);
yield buildCustomFonts(handoff, documentationObject);
yield buildStyles(handoff, documentationObject);
// await buildComponents(handoff);
if (build) {
yield (0, app_1.default)(handoff);
}
});
exports.default = pipeline;