import { readdirSync , readFileSync , statSync , writeFileSync , existsSync , mkdirSync , symlinkSync , } from 'fs' ; import { fork , spawnSync , ChildProcess } from 'child_process' ; import { on } from 'events' ; import { join , relative } from 'path' ; import type { Readable } from 'stream' ; interface Step { msg : string ; action ? : ( ) => void | { sleep ? : number ; } ; } interface ProcessCliOutputParams { dirPath : string ; args : string [ ] ; steps : Step [ ] ; timeout ? : number ; } export function createCliTester ( ) { const processes = [ ] ; async function processCliOutput ( { dirPath , args , steps , timeout = Number ( process . env . CLI_WATCH_TEST_TIMEOUT ) || 10000 , } ) { const process = runCli ( [ '--rootDir' , dirPath , '--log' , ... args ] , dirPath ) ; const lines = [ ] ; const output = ( ) => lines . join ( '\n' ) ; processes . push ( process ) ; if ( ! process . stdout ) { throw new Error ( 'no stdout on cli process' ) ; } process . stdout . on ( 'data' , ( e ) => lines . push ( e . toString ( ) ) ) ; const found = [ ] ; const startTime = Date . now ( ) ; return Promise . race ( [ onTimeout ( timeout , ( ) => new Error ( `${ JSON . stringify ( found , null , 3 ) }\n\n${ output ( ) }` ) ) , runSteps ( ) , ] ) ; function onTimeout ( ms , rejectWith ? ) { return new Promise < { output ( ) : string ; } > ( ( resolve , reject ) => setTimeout ( ( ) => ( rejectWith ? reject ( rejectWith ( ) ) : resolve ( { output } ) ) , ms ) ) ; } async function runSteps ( ) { for await ( const line of readLines ( process . stdout ! ) ) { const step = steps [ found . length ] ; if ( line . includes ( step . msg ) ) { found . push ( { message : step . msg , time : Date . now ( ) - startTime , } ) ; if ( step . action ) { const { sleep } = step . action ( ) || { } ; if ( typeof sleep === 'number' ) { await onTimeout ( sleep ) ; } } if ( steps . length === found . length ) { return { output } ; } } } return { output } ; } } return { run : processCliOutput , cleanup ( ) { for ( const process of processes ) { process . kill ( ) ; } processes . length = 0 ; } , } ; } async function * readLines ( readable ) { let buffer = '' ; for await ( const e of on ( readable , 'data' ) ) { for ( const char of e . toString ( ) ) { if ( char === '\n' ) { yield buffer ; buffer = '' ; } else { buffer += char ; } } } yield buffer ; } export function writeToExistingFile ( filePath , content ) { if ( existsSync ( filePath ) ) { writeFileSync ( filePath , content ) ; } else { throw new Error ( `file ${ filePath } does not exist` ) ; } } const stcPath = require . resolve ( '@stylable/cli/bin/stc.js' ) ; const formatPath = require . resolve ( '@stylable/cli/bin/stc-format.js' ) ; const codeModPath = require . resolve ( '@stylable/cli/bin/stc-codemod.js' ) ; export function runCli ( cliArgs = [ ] , cwd ) { return fork ( stcPath , cliArgs , { cwd , stdio : 'pipe' } ) ; } export function runCliSync ( cliArgs = [ ] ) { return spawnSync ( 'node' , [ stcPath , ... cliArgs ] , { encoding : 'utf8' } ) ; } export function runFormatCliSync ( cliArgs = [ ] ) { return spawnSync ( 'node' , [ formatPath , ... cliArgs ] , { encoding : 'utf8' } ) ; } export function runCliCodeMod ( cliArgs = [ ] ) { return spawnSync ( 'node' , [ codeModPath , ... cliArgs ] , { encoding : 'utf8' } ) ; } export interface Files { [ filepath ] : string ; } export const smlinkSymbol = Symbol ( 'smlink' ) ; export interface LinkedDirectory { type : typeof smlinkSymbol ; path : string ; } export interface FilesStructure { [ filepath ] : string | FilesStructure | LinkedDirectory ; } export function loadDirSync ( rootPath , dirPath = rootPath ) { return readdirSync ( dirPath ) . reduce < Files > ( ( acc , entry ) => { const fullPath = join ( dirPath , entry ) ; const key = relative ( rootPath , fullPath ) . replace ( /\\/g , '/' ) ; const stat = statSync ( fullPath ) ; if ( stat . isFile ( ) ) { acc [ key ] = readFileSync ( fullPath , 'utf8' ) ; } else if ( stat . isDirectory ( ) ) { return { ... acc , ... loadDirSync ( rootPath , fullPath ) , } ; } else { throw new Error ( 'Not Implemented' ) ; } return acc ; } , { } ) ; } export function populateDirectorySync ( rootDir , files , context = { smlinks : new Map ( ) } ) { for ( const [ filePath , content ] of Object . entries ( files ) ) { const path = join ( rootDir , filePath ) ; if ( typeof content === 'object' ) { if ( content . type === smlinkSymbol ) { const existingPath = join ( path , content . path as string ) ; try { symlinkSync ( existingPath , path , 'junction' ) ; } catch { if ( ! context . smlinks . has ( existingPath ) ) { context . smlinks . set ( existingPath , new Set ( ) ) ; } context . smlinks . get ( existingPath ) ! . add ( path ) ; } } else { mkdirSync ( path ) ; if ( context . smlinks . has ( path ) ) { for ( const linkedPath of context . smlinks . get ( path ) ! ) { symlinkSync ( path , linkedPath , 'junction' ) ; } context . smlinks . delete ( path ) ; } populateDirectorySync ( path , content as FilesStructure , context ) ; } } else { writeFileSync ( path , content ) ; if ( context . smlinks . has ( path ) ) { for ( const linkedPath of context . smlinks . get ( path ) ! ) { symlinkSync ( path , linkedPath , 'junction' ) ; } context . smlinks . delete ( path ) ; } } } } export function escapeRegExp ( str ) { return str . replace ( /[.*+?^${}()|[\]\\]/g , '\\$&' ) ; }