import { types } from "recast" ; import childProcess from "child_process" ; import path from "path" ; import { promises as fsPromises , constants } from "fs" ; import pMap from "p-map" ; import partition from "lodash.partition" ; import debug from "debug" ; import { assertNonNull } from "./assertNonNull" ; import { parse , print } from "recast" ; import { getWorkspaceInfo } from "workspace-info" ; const debugFunc = debug ( "typescript-monorepo-toolkit" ) ; export function wrapAsyncCommand ( command ) { command . catch ( ( error ) => { console . error ( "command exited with an error" , error ) ; process . exit ( 1 ) ; } ) ; } export function execFile ( file , args , cwd ) { return new Promise < string > ( ( res , rej ) => { childProcess . execFile ( file , args , { cwd , } , ( error , stdOut ) => { if ( error ) { rej ( error ) ; } else { res ( stdOut ) ; } } ) ; } ) ; } export interface PackageInfo { location : string ; mismatchedWorkspaceDependencies : string [ ] ; workspaceDependencies : string [ ] ; } export interface WorkspaceInfo { [ packageName ] : PackageInfo ; } export async function partitionNoneTs ( root , input , tsconfigPath ) { return pMap ( input , async ( [ packageName , packageInfo ] ) => { const is = await isTsPackage ( root , packageName , packageInfo , tsconfigPath ) ; return { is , packageName , packageInfo , } ; } , { concurrency : 4 , } ) . then ( ( all ) => { const [ ts , noneTs ] = partition ( all , ( e ) => e . is ) ; return [ ts . map ( ( e ) => [ e . packageName , e . packageInfo ] ) , noneTs . map ( ( e ) => [ e . packageName , e . packageInfo ] ) , ] ; } ) ; } async function isTsPackage ( root , packageName , packageInfo , tsconfigPath ) { return await fsPromises . access ( path . resolve ( root , packageInfo . location , tsconfigPath ) , constants . F_OK ) . then ( ( ) => true , ( ) => false ) ; } export async function applyOnPackage ( root , packageName , packagesMap , tsconfigPath ) { const packageInfo = packagesMap . get ( packageName ) ; assertNonNull ( packageInfo ) ; const absolutePath = path . resolve ( root , packageInfo . location ) ; const tsconfigContent = await fsPromises . readFile ( path . resolve ( root , packageInfo . location , tsconfigPath ) , "utf-8" ) ; const asJs = `[${ tsconfigContent }]` ; const ast = parse ( asJs , { } ) ; const deps = packageInfo . workspaceDependencies . map ( ( depPackageName ) => packagesMap . get ( depPackageName ) ) . filter ( ( v ) : v is NonNullable < typeof v > => v !== undefined ) ; const paths = deps . map ( ( d ) => path . relative ( absolutePath , path . resolve ( root , d . location ) ) ) ; ensureCompositeProject ( ast ) ; setProjectReferences ( ast , paths ) ; await fsPromises . writeFile ( path . resolve ( root , packageInfo . location , tsconfigPath ) , print ( ast ) . code . substring ( 1 ) . slice ( 0 , - 1 ) ) ; } export async function applyTransformationOnTSConfig ( root , packageName , packagesMap , tsconfigPath , transformation ) { const packageInfo = packagesMap . get ( packageName ) ; assertNonNull ( packageInfo ) ; const tsconfigContent = await fsPromises . readFile ( path . resolve ( root , packageInfo . location , tsconfigPath ) , "utf-8" ) ; const asJs = `[${ tsconfigContent }]` ; const ast = parse ( asJs , { } ) ; await transformation ( ast , root , packageName , packageInfo ) ; await fsPromises . writeFile ( path . resolve ( root , packageInfo . location , tsconfigPath ) , print ( ast ) . code . substring ( 1 ) . slice ( 0 , - 1 ) ) ; } export function setProjectReferences ( ast , pathsToAdd ) { const refsNode = findEnsureProjectReferences ( ast ) ; refsNode . value . elements = [ ] ; for ( const path of pathsToAdd ) { refsNode . value . elements . push ( types . builders . objectExpression ( [ types . builders . property ( "init" , types . builders . literal ( "path" ) , types . builders . literal ( path ) ) , ] ) ) ; } } export function findEnsureCompilerOptions ( ast ) { let compilerOptions = ast . program . body [ 0 ] . expression . elements [ 0 ] . properties . find ( ( p ) => p . key . value === "compilerOptions" ) ; if ( ! compilerOptions ) { compilerOptions = types . builders . objectProperty ( types . builders . literal ( "compilerOptions" ) , types . builders . objectExpression ( [ ] ) ) ; ast . program . body [ 0 ] . expression . elements [ 0 ] . properties . unshift ( compilerOptions ) ; } return compilerOptions ; } export function findEnsureProjectReferences ( ast ) { let references = ast . program . body [ 0 ] . expression . elements [ 0 ] . properties . find ( ( p ) => p . key . value === "references" ) ; if ( ! references ) { references = types . builders . objectProperty ( types . builders . literal ( "references" ) , types . builders . arrayExpression ( [ ] ) ) ; ast . program . body [ 0 ] . expression . elements [ 0 ] . properties . push ( references ) ; } return references ; } export function ensureCompositeProject ( ast ) { const compilerOptions = findEnsureCompilerOptions ( ast ) ; let compositeProp = compilerOptions . value . properties . find ( ( p ) => p . key . value === "composite" ) ; if ( ! compositeProp ) { compositeProp = types . builders . objectProperty ( types . builders . literal ( "composite" ) , types . builders . booleanLiteral ( true ) ) ; compilerOptions . value . properties . push ( compositeProp ) ; } compositeProp . value = types . builders . booleanLiteral ( true ) ; } export function setRootStringProp ( ast , propName , propValue ) { const root = ast . program . body [ 0 ] . expression . elements [ 0 ] ; let propAst = root . properties . find ( ( p ) => p . key . value === propName ) ; if ( ! propAst && propValue ) { propAst = types . builders . objectProperty ( types . builders . literal ( propName ) , types . builders . stringLiteral ( propValue ) ) ; root . properties . push ( propAst ) ; } if ( ! propValue ) { if ( propAst ) { const index = root . properties . indexOf ( propAst ) ; root . properties . splice ( index , 1 ) ; } } else { propAst . value = types . builders . stringLiteral ( propValue ) ; } } export function setCompilerOptionsStringProp ( ast , propName , propValue ) { const compilerOptions = findEnsureCompilerOptions ( ast ) ; let propAst = compilerOptions . value . properties . find ( ( p ) => p . key . value === propName ) ; if ( ! propAst && propValue ) { propAst = types . builders . objectProperty ( types . builders . literal ( propName ) , types . builders . stringLiteral ( propValue ) ) ; compilerOptions . value . properties . push ( propAst ) ; } if ( ! propValue ) { if ( propAst ) { const index = compilerOptions . value . properties . indexOf ( propAst ) ; compilerOptions . value . properties . splice ( index , 1 ) ; } } else { propAst . value = types . builders . stringLiteral ( propValue ) ; } } export async function applyTransformationOnAllPackages ( projectRoot , tsconfigPathInsidePackages , transformation ) { const workspaceInfoObject = await readWorkspaceInfoObject ( projectRoot ) ; const workspaceInfoEntries = Object . entries ( workspaceInfoObject ) ; const [ tsPackages , noneTsPackages ] = await partitionNoneTs ( projectRoot , workspaceInfoEntries , tsconfigPathInsidePackages ) ; debugFunc ( "found %d none-ts packages(?), ignoring them" , noneTsPackages . length ) ; debugFunc ( "none ts: %j" , noneTsPackages . map ( ( p ) => p [ 0 ] ) ) ; const map = new Map ( tsPackages ) ; await pMap ( tsPackages , ( p ) => { applyTransformationOnTSConfig ( projectRoot , p [ 0 ] , map , tsconfigPathInsidePackages , transformation ) ; } , { concurrency : 4 , } ) ; } export async function readWorkspaceInfoObject ( projectRoot ) { return await getWorkspaceInfo ( { cwd : projectRoot , } ) ; }