common/scripts/generate-api-diff.mjs (162 lines of code) (raw):

#!/usr/bin/env node // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * This script is used to generate api.json files for beta features for uploading to apiview.dev for review. * This generates two files: * 1. baseline.api.json - the api.json file with the beta feature removed * 2. feature.api.json - the api.json file with the beta feature enabled * * @example * `node ./generate-api-diff.mjs --feature nameOfFeature` */ import { exit } from 'process'; import yargs from 'yargs/yargs'; import featureDefinitions from '../config/babel/features.js'; import { exec as execInternal } from './lib/exec.mjs'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { getBuildFlavor } from './lib/getBuildFlavor.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const destinationDir = path.join(__dirname, '../../apis'); // This process runs the build and api generate commands locally. // So this process backs up the files that will be modified by the build process // and restores them after the build process is complete. const filesToBackup = [ path.join(__dirname, '../../packages/communication-react/review/beta/communication-react.api.md'), path.join(__dirname, '../../packages/communication-react/review/stable/communication-react.api.md'), path.join(__dirname, '../config/babel/features.js') ]; // This process runs the build and api generate commands locally. // During this process, the development flavor is switched to beta-release and/or stable. // This variable is used to store the initial development flavor to restore to once the cmd finishes. let initialDevelopmentFlavor = undefined; // This variable is used to store the commands that were run during the process. // This is used to display the commands that were run in the logs in the event of an error // to allow the developer to easily reproduce the error. let commandsRun = [] async function main() { let result = undefined; await setup(); try { result = await generateApiJsons(); } catch (e) { console.error(e); } finally { await cleanup(); } // Ouput the result of the process _after_ the cleanup process has run to ensure the logs are not lost if (result) { console.log(`\nDone generating api.json files, files can be found at:`); console.log(` - ${result.baselineFilePath}`); console.log(` - ${result.featureFilePath}`); console.log(`Please upload these files to https://apiview.dev/ for review.`); } else { console.error(`\nScript failed to generate api.json files.\nInspect the logs above for more information on errors.\nAlternatively, this tool ran the following commands that you can run locally to reproduce the error: \n${commandsRun.map(c => ` - ${c}`).join('\n')}`); exit(1); } } /** To be called at the beginning of the script to backup the files that will be modified by the build process */ async function setup() { // Clean any existing results first if (fs.existsSync(destinationDir)) { fs.rmSync(destinationDir, { recursive: true }); } // Backup the files that will be modified by the build process backupFiles(filesToBackup); // Store the active development flavor to restore to once the cmd finishes initialDevelopmentFlavor = getBuildFlavor(); } /** To be called at the end of the script to restore the files that were modified by the build process */ async function cleanup() { // Restore the files that were modified by the build process restoreFiles(filesToBackup); // restore the initial development flavor used before the cmd started if (initialDevelopmentFlavor) { await execInternal(`rush switch-flavor:${initialDevelopmentFlavor}`); } } /** * Generate the baseline.api.json and feature.api.json files. */ async function generateApiJsons() { const { feature } = parseArgs(process.argv); const { alpha, beta, stable } = featureDefinitions; if (stable.includes(feature)) { console.error(`ERROR: Feature ${feature} is already stabilized. Please use a beta feature.`); exit(-1); } const allFeatures = [...alpha, ...beta, ...stable]; if (!allFeatures.includes(feature)) { console.error(`ERROR: Could not find feature "${feature}" in features.js file.`); exit(-1); } const isAlphaFeature = alpha.includes(feature); if (isAlphaFeature) { await exec('rush switch-flavor:beta-release'); } else { await exec('rush switch-flavor:stable'); } console.log(`Generating baseline.api.json file`); const baselineFilePath = await generateApiFile('baseline.api.json'); console.log(`Generating feature.api.json file`); exec(`rush stage-feature -f ${feature} -o ${isAlphaFeature ? 'alphaToBeta' : 'betaToStable'}`); const featureFilePath = await generateApiFile('feature.api.json'); return { baselineFilePath, featureFilePath }; } async function exec(cmd) { commandsRun.push(cmd); await execInternal(cmd); } /** * @returns {Promise<string>} The path to the generated api.json file */ async function generateApiFile(filename) { await exec('rush build -v -o @azure/communication-react'); if (!fs.existsSync(destinationDir)) { fs.mkdirSync(destinationDir, { recursive: true }); } const destinationFile = `${destinationDir}/${filename}`; fs.copyFileSync( path.join(__dirname, '../../packages/communication-react/temp/communication-react.api.json'), destinationFile ); return destinationFile; } /** Backup the files that will be modified by the build process */ function backupFiles(filepaths) { filepaths.forEach((file) => { fs.copyFileSync(file, `${file}.bak`); }); } /** Restore the files that were modified by the build process */ function restoreFiles(filepaths) { filepaths.forEach((file) => { fs.copyFileSync(`${file}.bak`, file); fs.rmSync(`${file}.bak`); }); } function parseArgs(argv) { const args = yargs(argv.slice(2)) .usage('$0 [options]', 'Use this script to generate api.json files for beta features for uploading to apiview.dev for review.') .example([ ['$0 --feature my-feature', 'Generate api.json files for the my-feature beta feature.'] ]) .options({ feature: { alias: 'f', type: 'string', describe: 'Feature name to generate api files for. This should match the name in the features.js file' } }) .parseSync(); if (!args.feature) { console.error('ERROR: Could not find feature name. Please provide a feature name using the `feature` argument.'); exit(-1); } return args; } main();