KnodesCommunity/typedoc-plugins

View on GitHub
tools/utils.js

Summary

Maintainability
A
2 hrs
Test Coverage
B
81%
const { exec: _exec, spawn: _spawn } = require( 'child_process' );
const { resolve, relative } = require( 'path' );
const { Writable, Stream, Readable } = require( 'stream' );
const { promisify } = require( 'util' );

const glob = require( 'glob' );
const { once, isArray, omitBy, isNil } = require( 'lodash' );
const { normalizePath } = require( 'typedoc' );

const exec = promisify( _exec );
module.exports.exec = exec;

/**
 * @param {string} cmd
 * @param {string[]=} args
 * @param {import('child_process').SpawnOptionsWithoutStdio=} opts
 */
const spawn = ( cmd, args, opts = {} ) => new Promise( ( res, rej ) => {
    const { stdio } = opts;
    if( !opts.stdio ){
        opts.stdio = [ null, process.stdout, process.stderr ];
    }
    if( stdio && isArray( stdio ) ){
        opts.stdio = stdio.map( ( s, i ) => i <= 2 && s instanceof Stream ? 'pipe' : s );
    }
    const p = _spawn( cmd, args, opts );
    if( stdio && isArray( stdio ) ){
        stdio[0] instanceof Readable && stdio[0].pipe( p.stdin );
        stdio[1] instanceof Writable && p.stdout.pipe( stdio[1] );
        stdio[2] instanceof Writable && p.stderr.pipe( stdio[2] );
    }
    p.on( 'close', code => {
        const out = omitBy( {
            stdout: stdio?.[1]?.CAPTURE === true ? stdio[1].read() : undefined,
            stderr: stdio?.[2]?.CAPTURE === true ? stdio[2].read() : undefined,
            code,
        }, isNil );
        if( code !== 0 ) {
            const err = new Error( `Exit code ${code}: ${JSON.stringify( {
                cmd: [ cmd, ...args ],
                cwd: opts.cwd,
            } )}` );
            return rej( Object.assign( err, out ) );
        } else {
            return res( out );
        }
    } );
} );
module.exports.spawn = spawn;

/**
 * @returns {Writable & {read: () => string}}
 */
const captureStream = () => {
    const stream = new Writable();
    const data = [];
    // eslint-disable-next-line no-underscore-dangle -- Expected
    stream._write = ( chunk, encoding, next ) => {
        data.push( chunk.toString() );
        next();
    };
    stream.read = () => {
        stream.end();
        return data.join( '\n' );
    };
    stream.CAPTURE = true;
    return stream;
};
module.exports.captureStream = captureStream;

/**
 * @typedef {{
 *     id: string,
 *     name: string,
 *     path: string,
 *     absPath: string,
 *     pkgName: string,
 *     pkgJson: import('type-fest').PackageJson.PackageJsonStandard,
 *     pkgJsonPath: string
 * }} Project
 */
/**
 * @returns {Project[]}
 */
const getProjects = once( () => {
    const packages = require( '../package.json' ).workspaces
        .map( w => glob.sync( w, { ignore: 'node_modules/**' } ) )
        .flat();
    let names = packages.slice();
    while( names.every( ( n => n[0] === names[0][0] ) ) ){
        names = names.map( n => n.slice( 1 ) );
    }
    return packages
        .map( ( p, i ) => {
            const pkgJsonPath = resolve( __dirname, '..', p, 'package.json' );
            const pkgJson = require( pkgJsonPath );
            return {
                id: relative( './packages', p ),
                path: p,
                absPath: this.resolveRoot( p ),
                name: names[i],
                pkgJson,
                pkgName: pkgJson.name,
                pkgJsonPath,
            };
        } );
} );
module.exports.getProjects = getProjects;
/**
 * @param {string} label
 */
module.exports.createStash = async label => {
    const message = `REPO SNAPSHOT '${label}'`;
    await spawn( 'git', [ 'stash', 'push', '--all', '--message', message ], { stdio: [ null, null, process.stderr ] } );
    console.log( `Created a stash "${message}"` );
    await spawn( 'git', [ 'stash', 'apply', 'stash@{0}' ] );
};

module.exports.commonPath = ( input, sep = '/' ) => {
    if( input.length <= 1 ){
        return input[0];
    }
    /**
     * Given an array of strings, return an array of arrays, containing the
     * strings split at the given separator
     *
     * @param {Array<string>} a - A
     * @returns {Array<Array<string>>} B
     */
    const splitStrings = a => a.map( i => i.split( sep ) );

    /**
     * Given an index number, return a function that takes an array and returns the
     * element at the given index
     *
     * @param {number} i - The index.
     * @returns {function(Array<*>): *} - FP helper taking an array.
     */
    const elAt = i => a => a[i];

    /**
     * Transpose an array of arrays:
     * Example:
     * [['a', 'b', 'c'], ['A', 'B', 'C'], [1, 2, 3]] ->
     * [['a', 'A', 1], ['b', 'B', 2], ['c', 'C', 3]]
     *
     * @param {Array<Array<*>>} a - A
     * @returns {Array<Array<*>>} B
     */
    const rotate = a => a[0].map( ( e, i ) => a.map( elAt( i ) ) );

    /**
     * Checks of all the elements in the array are the same.
     *
     * @param {Array<*>} arr - Arr
     * @returns {boolean} `true` if arr[0] === arr[1] === arr[2]...
     */
    const allElementsEqual = arr => arr.every( e => e === arr[0] );

    return rotate( splitStrings( input, sep ) )
        .filter( allElementsEqual ).map( elAt( 0 ) ).join( sep );
};

module.exports.selectProjects = explicitProjects => {
    const allProjects = getProjects();
    const nonExistingProjects = explicitProjects.filter( p => !allProjects.find( pp => pp.name === p ) );
    if( nonExistingProjects.length > 0 ){
        throw new Error( `Specified missing projects ${JSON.stringify( nonExistingProjects )}` );
    }
    return explicitProjects.length === 0 ?
        allProjects :
        allProjects.filter( p => explicitProjects.includes( p.name ) );
};

const formatPackagesBin = process.platform === 'win32' ? '.\\node_modules\\.bin\\format-package.cmd' : './node_modules/.bin/format-package';
/**
 * @param {string[]} packages
 */
module.exports.formatPackages = ( ...packages ) => spawn( formatPackagesBin, [ '--write', ...packages.map( p => normalizePath( p ) ) ] );
/**
 * @param {string[]} packages
 */
module.exports.checkFormatPackages = ( ...packages ) => spawn( formatPackagesBin, [ '--check', ...packages.map( p => normalizePath( p ) ) ] );

/**
 * @param {string[]} filesList
 */
module.exports.getStagedFiles = async ( ...filesList ) => {
    const stagedPatchesOutput = captureStream();
    if( filesList && filesList.length > 0 ){
        filesList.unshift( '--' );
    }
    await spawn( 'git', [ 'diff', '--name-only', '--cached', ...filesList ], { stdio: [ null, stagedPatchesOutput, null ] } );
    return stagedPatchesOutput.read().split( /\r?\n/ ).filter( v => v );
};

module.exports.resolveRoot = ( ...paths ) => resolve( __dirname, '..', ...paths );
module.exports.relativeToRoot = path => relative( resolve( __dirname, '..' ), path );