import path from 'path' ; import fs from 'fs' ; import npm from 'npm' ; import formatPkg from 'format-package' ; import prettier from 'prettier' ; import pkgUp from 'pkg-up' ; import { getConfigsBasePath , execCmdInProject , configToEvalString , serialPromises , } from '@alfred/helpers' ; import mergeConfigs from '@alfred/merge-configs' ; import { PkgJson , ConfigInterface , ProjectInterface , ValidationResult , SkillsList , SkillMap , NpmClient , DependencyType , Dependencies , PkgWithDeps , Env , ProjectEnum , Platform , Target , Skill , Entrypoint , LearnEvent , NewEvent , SkillConfig , RunEvent , HookEvent , } from '@alfred/types' ; import loadJsonFile from 'load-json-file' ; import Config from './config' ; import { PkgValidation } from './validation' ; import skillMapFromConfig , { requireSkill , skillSupportsTarget } from './skill' ; import run from './commands/run' ; import learn from './commands/learn' ; import skills from './commands/skills' ; import clean from './commands/clean' ; import { PKG_SORT_ORDER , RAW_ENTRYPOINTS } from './constants' ; import { EventEmitter } from 'events' ; process . on ( 'unhandledRejection' , ( err ) => { throw err ; } ) ; /**
 * Get the root of a project from the current working directory
 */ function findProjectRoot ( startingSearchDir = process . cwd ( ) ) { const pkgPath = pkgUp . sync ( { cwd : startingSearchDir , } ) ; if ( ! pkgPath ) { throw new Error ( `Alfred project root could not be found from "${ startingSearchDir }".

      Make sure you are inside an Alfred project.` ) ; } return path . dirname ( pkgPath ) ; } function getInstallCommmand ( project ) { const { root } = project ; const { npmClient } = project . config ; return npmClient . toLowerCase ( ) === 'npm' ? `npm install --prefix ${ root }` : 'yarn' ; } export function parseEntrypoint ( rawEntrypoint ) { const [ project , platform ] = rawEntrypoint . split ( '.' ) as [ ProjectEnum , Platform ] ; return { project , platform , filename : rawEntrypoint } ; } export function entrypointToTarget ( entrypoint , env ) { return { platform : entrypoint . platform , project : entrypoint . project , env , } ; } /**
 * Given an object with deps, return the deps as a list
 * Example:
 * pkgDepsToList({ react: 16 }) => ['react@16']
 */ export function pkgDepsToList ( deps ) { return Array . from ( Object . entries ( deps ) ) . map ( ( [ dependency , semver ] ) => `${ dependency }@${ semver }` ) ; } export function formatPkgJson ( pkg ) { return formatPkg ( pkg , { order : PKG_SORT_ORDER } ) ; } export default class Project extends EventEmitter implements ProjectInterface { config : ConfigInterface ; pkgPath : string ; pkg : PkgJson ; root : string ; entrypoints : Entrypoint [ ] ; targets : Target [ ] ; constructor ( projectRootOrSubDir = process . cwd ( ) ) { super ( ) ; const projectRoot = findProjectRoot ( projectRootOrSubDir ) ; this . root = projectRoot ; this . pkgPath = path . join ( projectRoot , 'package.json' ) ; this . pkg = loadJsonFile . sync ( this . pkgPath ) ; this . config = Config . initFromProjectRoot ( projectRoot ) ; this . entrypoints = this . getEntrypoints ( ) ; this . targets = this . getTargets ( ) ; } private getEntrypoints ( ) { return RAW_ENTRYPOINTS . filter ( ( entryPoint ) => fs . existsSync ( path . join ( this . root , 'src' , entryPoint ) ) ) . map ( parseEntrypoint ) ; } private getTargets ( ) { const envs = [ 'production' , 'development' , 'test' ] ; const env = envs . includes ( process . env . NODE_ENV || '' ) ? ( process . env . NODE_ENV as Env ) : 'development' ; return this . entrypoints . map ( ( entrypoint ) => entrypointToTarget ( entrypoint , env ) ) ; } async emitAsync ( eventName , eventData ? ) { const tasks = this . listeners ( eventName ) . map ( ( event ) => { return async ( ) : Promise < void > => { if ( eventData ) { await event ( eventData ) ; } else { await event ( ) ; } } ; } ) ; await serialPromises ( tasks ) ; } async init ( ) { this . checkProjectIsValid ( ) ; const skillMap = await this . getSkillMap ( ) ; skillMap . forEach ( ( skill ) => { Object . entries ( skill . hooks || { } ) . forEach ( ( [ hookName , hookFn ] ) => { this . on ( hookName , ( event = { } ) : Promise < void > | void => { if ( ! hookFn ) return ; hookFn ( { event , project : this , config : this . config , targets : this . targets , skill , skillMap , } ) ; } ) ; } ) ; } ) ; [ 'afterNew' , 'afterLearn' , 'beforeRun' ] . forEach ( ( hookName ) => { this . on ( hookName , async ( event ) => { await this . writeSkillConfigs ( skillMap ) ; if ( 'skillsPkgNames' in event ) { await this . writeSkillFiles ( event . skillsPkgNames . map ( requireSkill ) ) ; } } ) ; } ) ; return this ; } static validatePkgPath ( pkgPath ) { if ( ! fs . existsSync ( pkgPath ) ) { throw new Error ( `"${ pkgPath }" does not exist` ) ; } } setConfig ( config ) { this . config = config ; return this ; } /**
     * Find the dependencies that the user is missing in their package.json and write
     * them to the package.json
     */ private async writeMissingDeps ( ) { const { dependencies , devDependencies } = await this . findDepsToInstall ( ) ; await this . installDeps ( dependencies , 'dep' , 'writeOnly' ) ; await this . installDeps ( devDependencies , 'dev' , 'writeOnly' ) ; } TODO HACK private autoInstallDeps ( ) { const { config } = this ; const nodeModulesPath = `${ this . root }/node_modules` ; module . paths . push ( nodeModulesPath ) ; if ( config . autoInstall === true && ! fs . existsSync ( nodeModulesPath ) ) { const installCommand = getInstallCommmand ( this ) ; execCmdInProject ( this , installCommand ) ; } } /**
     * A list of things to do before running any command
     */ async beforeCommand ( ) { await this . writeMissingDeps ( ) ; this . autoInstallDeps ( ) ; } async run ( subcommand , args = [ ] ) { const capitalizedCmd = `${ subcommand [ 0 ] . toUpperCase ( ) }${ subcommand . slice ( 1 ) }` ; await this . emitAsync ( `before${ capitalizedCmd }` ) ; switch ( subcommand ) { case 'clean' : { await this . clean ( ) ; break ; } case 'skills' : { await this . skills ( ) ; break ; } case 'learn' : { await this . learn ( args ) ; break ; } default : { await this . beforeCommand ( ) ; await run ( this , subcommand , args ) ; break ; } } await this . emitAsync ( `after${ capitalizedCmd }` ) ; } learn ( skillPkgNames ) { return learn ( this , skillPkgNames ) ; } async clean ( ) { await this . beforeCommand ( ) ; return clean ( this ) ; } async skills ( ) { await this . beforeCommand ( ) ; return skills ( this ) ; } /**
     * Validate the package.json of the Alfred project
     */ validatePkgJson ( ) { const { pkgPath } = this ; const result = PkgValidation . validate ( fs . readFileSync ( pkgPath ) . toString ( ) ) ; if ( result . errors . length ) { console . warn ( `The following errors were found in the package.json path:
${ pkgPath }:

${ JSON . stringify ( result . errors ) }` ) ; } return result ; } private checkProjectIsValid ( ) { const srcPath = path . join ( this . root , 'src' ) ; const validationResult = this . validatePkgJson ( ) ; if ( ! fs . existsSync ( srcPath ) ) { throw new Error ( '"./src" directory does not exist. This does not seem to be an Alfred project' ) ; } const hasEntrypoint = this . entrypoints . length > 0 ; if ( ! hasEntrypoint ) { throw new Error ( `You might be in the wrong directory or this is not an Alfred project. The project must have at least one entrypoint. Here are some examples of entrypoints:\n\n${ RAW_ENTRYPOINTS . map ( ( e ) => `"./src/${ e }"` ) . join ( '\n' ) } \n\n Searching from ${ this . root }\n\n` ) ; } if ( this . entrypoints . some ( ( entrypoint ) => entrypoint . filename === 'app.browser.js' ) ) { const indexHtmlPath = path . join ( srcPath , 'index.html' ) ; if ( ! fs . existsSync ( indexHtmlPath ) ) { throw new Error ( 'An "./src/index.html" file must exist when using an "app.browser" entrypoint' ) ; } } return validationResult ; } async installDeps ( dependencies , dependenciesType , npmClient = this . config . npmClient ) { const normalizedDeps = Array . isArray ( dependencies ) ? dependencies : pkgDepsToList ( dependencies ) ; if ( ! normalizedDeps . length ) return ; await this . updatePkg ( ) ; const npmClientWithOverride = process . env . ALFRED_IGNORE_INSTALL === 'true' ? 'writeOnly' : npmClient ; switch ( npmClientWithOverride ) { case 'npm' : { await new Promise ( ( resolve , reject ) => { npm . load ( { save : true , dev : dependenciesType === 'dev' } , ( err ) => { if ( err ) reject ( err ) ; npm . commands . install ( normalizedDeps , ( _err , data ) => { if ( _err ) reject ( _err ) ; resolve ( data ) ; } ) ; npm . on ( 'log' , console . log ) ; } ) ; } ) ; break ; } case 'yarn' : { const devFlag = dependenciesType === 'dev' ? '--dev' : '' ; execCmdInProject ( this , [ 'yarn' , 'add' , devFlag , ... normalizedDeps ] ) ; break ; } case 'writeOnly' : { const newDependencies = normalizedDeps . map ( ( dependency ) => { if ( dependency [ 0 ] !== '@' ) { return dependency . split ( '@' ) ; } const pkgWithoutScope = dependency . slice ( 1 ) . split ( '@' ) ; return [ `@${ pkgWithoutScope [ 0 ] }` , pkgWithoutScope [ 1 ] ] ; } ) . map ( ( [ p , c ] ) => ( { [ p ] : c } ) ) . reduce ( ( p , c ) => ( { ... p , ... c } ) , { } ) ; await Config . writeObjToPkgJson ( this . pkgPath , mergeConfigs ( { } , this . pkg , { [ dependenciesType === 'dev' ? 'devDependencies' : 'dependencies' ] : newDependencies , } ) ) ; break ; } default : { throw new Error ( 'Unsupported npm client. Can only be "npm" or "yarn"' ) ; } } await this . updatePkg ( ) ; } async updatePkg ( ) { this . pkg = await loadJsonFile ( this . pkgPath ) ; } TODO /**
     * Get a skillMap that has all the skills used in all targets
     */ async getSkillMap ( ) { return skillMapFromConfig ( this ) ; } async writeSkillFiles ( skills ) { await Promise . all ( skills . filter ( ( skill ) => this . targets . some ( ( target ) => skillSupportsTarget ( skill , target ) ) ) . map ( ( skill ) => skill . files . writeAllFiles ( this ) ) ) ; } async writeSkillConfigs ( skillMap ) { const configsBasePath = getConfigsBasePath ( this ) ; if ( ! fs . existsSync ( configsBasePath ) ) { fs . mkdirSync ( configsBasePath ) ; } const skills = Array . from ( skillMap . values ( ) ) ; const pkgConfigEntries = [ ] ; await Promise . all ( skills . flatMap ( ( skill ) => Array . from ( skill . configs . values ( ) ) ) . map ( async ( config ) => { const filePath = path . join ( configsBasePath , config . filename ) ; switch ( config . write ) { case false : break ; case 'pkg' : { if ( typeof config . pkgProperty === 'string' ) { pkgConfigEntries . push ( [ config . pkgProperty , config . config ] ) ; if ( fs . existsSync ( filePath ) ) { await fs . promises . unlink ( filePath ) ; } } break ; } case 'file' : { if ( typeof config . pkgProperty === 'string' && config . pkgProperty in this . pkg ) { delete this . pkg [ config . pkgProperty ] ; } const stringifiedConfig = JSON . stringify ( config . config ) ; let parser = 'babel' ; const configWithExports = ( ( ) : string => { switch ( config . fileType ) { case 'commonjs' : parser = 'babel' ; return `module.exports = ${ stringifiedConfig }` ; case 'module' : parser = 'babel' ; return `export default ${ stringifiedConfig }` ; case 'json' : parser = 'json' ; return stringifiedConfig ; default : parser = 'babel' ; return `module.exports = ${ stringifiedConfig }` ; } } ) ( ) ; const formattedConfig = prettier . format ( [ ... ( config . imports || [ ] ) , '' , configToEvalString ( configWithExports ) , ] . join ( '\n' ) , { parser , } ) ; await fs . promises . writeFile ( filePath , formattedConfig ) ; break ; } } } ) ) ; if ( pkgConfigEntries . length ) { await this . updatePkg ( ) ; await Config . writeObjToPkgJson ( this . pkgPath , Object . fromEntries ( pkgConfigEntries ) , this . pkg ) ; } } async findDepsToInstall ( additionalSkills = [ ] ) { const skillMap = await this . getSkillMap ( ) ; const pkgDeps = [ ... skillMap . values ( ) , ... additionalSkills , ] . reduce ( ( prev , curr ) => ( { dependencies : { ... prev . dependencies , ... curr . dependencies } , devDependencies : { ... prev . devDependencies , ... curr . devDependencies } , } ) , { dependencies : { } , devDependencies : { } } ) ; const dependencies = Object . fromEntries ( Object . entries ( pkgDeps . dependencies ) . filter ( ( [ dep ] ) => ! ( dep in ( this . pkg . dependencies || { } ) ) ) ) ; const devDependencies = Object . fromEntries ( Object . entries ( pkgDeps . devDependencies ) . filter ( ( [ dep ] ) => ! ( dep in ( this . pkg . devDependencies || { } ) ) ) ) ; return { dependencies , devDependencies , } ; } }