qiu8310/serpent

View on GitHub
projects/serpent-common-cli/src/fs/project.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import path from 'path'
import assert from 'assert'
import findup from 'mora-scripts/libs/fs/findup'
import { existsFile, exists, existsDir } from './context'
import { toOSPath } from './toOSPath'

/** package.json 中的 name 的正则 */
export const PROJECT_NAME_REGEXP = /(?:@([\w-]+)\/)?([\w-]+)/
/** package.json 中的 name 的正则(匹配字符串的开始和结束) */
export const PROJECT_NAME_REGEXP_FULL = /^(?:@([\w-]+)\/)?([\w-]+)$/
/** package.json 中的 name 的正则(匹配字符串的开始) */
export const PROJECT_NAME_REGEXP_START = /^(?:@([\w-]+)\/)?([\w-]+)/
/** package.json 中的 name 的正则(匹配字符串的结束) */
export const PROJECT_NAME_REGEXP_END = /(?:@([\w-]+)\/)?([\w-]+)$/

/**
 * 获取项目的根目录
 * @param refAbsoluteFilePath 用于定位项目根目录的一个绝对路径,如果不传,默认使用 process.cwd
 */
export function tryGetProjectRootDir(refAbsoluteFilePath?: string): string | undefined {
  let refPath = refAbsoluteFilePath || process.cwd()
  try {
    // 移除后面 node_modules 目录
    const pkg = findup.pkg(refPath.replace(/[\\\/]node_modules(\b|[\\\/].*)$/, ''))
    return path.dirname(pkg)
  } catch (e) {
    return
  }
}

/**
 * 判断指定的路径是否是一个项目(是否包含 package.json 文件)的根目录
 */
export function isProjectRootDir(absDir: string) {
  return existsFile(path.join(absDir, 'package.json'))
}

/** 确保指定的路径是项目根目录 */
export function assertProjectRootDir(absDir: string, message?: string) {
  assert.ok(isProjectRootDir(absDir), message || `path ${absDir} is not a valid project root directory`)
}

/**
 * 根据相对路径,获取其绝对路径(只能获取文件的,无法获取文件夹的)
 *
 * @param relativeProjectFilePath 相对于项目根目录的路径
 * @param refAbsoluteFilePath 用于定位项目根目录的一个绝对路径,如果不传,默认使用 process.cwd
 *
 */
export function tryGetProjectFile(relativeProjectFilePath: string, refAbsoluteFilePath?: string) {
  return tryGetProjectPath(relativeProjectFilePath, refAbsoluteFilePath, existsFile)
}

/**
 * 根据相对路径,获取其绝对路径(只能获取文件夹的,无法获取文件的)
 *
 * @param relativeProjectFilePath 相对于项目根目录的路径
 * @param refAbsoluteFilePath 用于定位项目根目录的一个绝对路径,如果不传,默认使用 process.cwd
 *
 */
export function tryGetProjectDir(relativeProjectFilePath: string, refAbsoluteFilePath?: string) {
  return tryGetProjectPath(relativeProjectFilePath, refAbsoluteFilePath, existsDir)
}

function tryGetProjectPath(relativeProjectFilePath: string, refAbsoluteFilePath?: string, fn = exists) {
  const rootDir = tryGetProjectRootDir(refAbsoluteFilePath)
  if (rootDir) {
    const file = path.join(rootDir, toOSPath(relativeProjectFilePath))
    if (fn(file)) return file
  }
  return
}

/**
 * 解析项目安装时可能会用的名称
 * @param installName 项目名称,可以带有版本号,如:`vue`、`vue@^1`、`jquery@1.0.0`、`@serpent/foo@latest`
 * @returns
 * - `parseProjectInstallName("@serpent/foo@latest")  =>  { scope: 'serpent', name: 'foo', tag: 'latest', range: '' }`
 * - `parseProjectInstallName("foo@^1")               =>  { scope: '', name: 'foo', tag: '', range: '^1' }`
 * - `parseProjectInstallName("foo@*")                =>  { scope: '', name: 'foo', tag: '', range: '' }`
 */
export function parseProjectInstallName(installName: string) {
  let scope = ''
  let range = ''
  let tag = ''
  let name = ''

  let newName = installName
  let error = () => {
    throw new Error(`"${installName}" is not a valid project name`)
  }

  if (newName[0] === '@') {
    ;[scope, newName] = newName.substr(1).split('/')
    if (!scope || !newName) error()
  }

  ;[name, range = ''] = newName.split('@')
  if (!name || !PROJECT_NAME_REGEXP_FULL.test(name)) error()

  // range 还要支持 "*"
  if (range && range !== '*' && /^[a-zA-Z][-\w]*$/.test(range)) {
    tag = range
    range = ''
  }

  return {
    scope,
    range,
    tag,
    name,
  }
}

/**
 * 根据项目根目录,获取到 package.json 中指定的 bin 文件所在的绝对路径
 */
export function tryGetProjectBinFile(rootDir: string, cmdName?: string) {
  let bin = ''
  let file = ''

  try {
    const pkg = require(path.join(rootDir, 'package.json'))
    bin = pkg.bin
  } catch (e) {}

  if (bin && typeof bin === 'string') {
    file = path.resolve(rootDir, bin)
  } else if (typeof bin === 'object') {
    if (cmdName) {
      if (bin[cmdName]) {
        file = path.resolve(rootDir, bin[cmdName])
      }
    } else {
      const binKeys = Object.keys(bin)
      if (binKeys.length === 1) {
        file = path.resolve(rootDir, bin[binKeys[0]])
      }
    }
  }

  if (file) {
    try {
      return require.resolve(file)
    } catch (e) {}
  }
  return
}