All files / lib options.ts

99.03% Statements 102/103
97.87% Branches 46/47
100% Functions 19/19
100% Lines 73/73

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 1406x                 6x 6x 6x                     6x                   25x 25x   25x 25x   25x 25x 25x   22x 22x 22x 3x     22x     20x 20x 2x 2x     18x 2x 2x     16x 7x 7x     9x 2x 2x     7x 6x 10x 6x   4x 4x     1x     6x 3x 3x     6x 84x 6x 20x 20x     26x 24x 16x 9x 13x   6x 6x   6x 20x 20x   20x 20x   10x 10x   10x 10x   10x 10x 10x   4x     6x 25x 25x   74x   20x 20x     25x     9x  
import { InvalidArgumentError, InvalidNewlineTypeError, MissingArgumentValueError } from "./env/error"
 
type RawArgument = string
type ArgumentName = string
type ArgumentValue = string | boolean
type Argument = [ArgumentName, ArgumentValue]
type ArgumentMap = Record<ArgumentName, ArgumentValue>
type ProcessEnvVariable = [keyof NodeJS.ProcessEnv, NodeJS.ProcessEnv[keyof NodeJS.ProcessEnv]]
 
export enum NewlineType {
    unix = 'unix',
    windows = 'windows'
}
 
export interface Options {
    distFilePath: string
    localFilePath: string
    prompts: boolean
    allowDuplicates: boolean
    newlineType: NewlineType
}
 
const defaultOptions: Options = {
    distFilePath: '.env.dist',
    localFilePath: '.env',
    prompts: true,
    allowDuplicates: false,
    newlineType: NewlineType.unix
}
 
export type ProcessDependencies = Pick<NodeJS.Process, 'argv' | 'env' | 'platform'>
 
export const getOptionsFromEnvironment = ({ argv, env, platform }: ProcessDependencies): Options => {
    const argumentMap = parseArguments(argv)
 
    const isWindows = platform === 'win32'
    const newlineType: NewlineType = isWindows ? NewlineType.windows : NewlineType.unix
 
    const cliOptions: Partial<Options> = {}
    const argumentList = Object.entries(argumentMap)
    argumentList.forEach((argument) => mapArgumentToOptions(argument, cliOptions))
 
    const processEnvOptions: Partial<Options> = {}
    const processEnvVariableList = Object.entries(env)
    processEnvVariableList.forEach(
        (variable) => mapProcessEnvVariableToOptions(variable, processEnvOptions)
    )
 
    return { ...defaultOptions, newlineType, ...processEnvOptions, ...cliOptions }
}
 
const mapArgumentToOptions = ([name, value]: Argument, options: Partial<Options>) => {
    if (isDistFileArgument(name)) {
        options.distFilePath = String(value)
        return
    }
 
    if (isLocalFileArgument(name)) {
        options.localFilePath = String(value)
        return
    }
 
    if (isPromptsArgument(name)) {
        options.prompts = getBooleanFromArgumentValue(value)
        return
    }
 
    if (isAllowDuplicatesArgument(name)) {
        options.allowDuplicates = getBooleanFromArgumentValue(value)
        return
    }
 
    if (isNewlineTypeArgument(name)) {
        const validTypes = Object.values(NewlineType)
        const isValid = validTypes.find(type => type === value)
        if (!isValid) throw new InvalidNewlineTypeError()
 
        options.newlineType = value as NewlineType
        return
    }
 
    throw new InvalidArgumentError().setArgumentName(name)
}
 
const mapProcessEnvVariableToOptions = ([name, value]: ProcessEnvVariable, options: Partial<Options>) => {
    const isContinuousIntegration = name === 'CI' && value === 'true'
    if (isContinuousIntegration) options.prompts = false
}
 
const argumentNameAndInlineValueExpression = /^(--?\w+)(?:=(.*))?/
const isArgumentName = (value: RawArgument): boolean => argumentNameAndInlineValueExpression.test(value)
const getArgumentNameAndInlineValue = (rawArgument: string): [string, string?] => {
    const [_, name, inlineValue] = argumentNameAndInlineValueExpression.exec(rawArgument)
    return [name, inlineValue]
}
 
const isDistFileArgument = (name: string): boolean => name === '-d' || name === '--distFile'
const isLocalFileArgument = (name: string): boolean => name === '-l' || name === '--localFile'
const isPromptsArgument = (name: string): boolean => name === '-p' || name === '--prompts'
const isAllowDuplicatesArgument = (name: string): boolean => name === '-a' || name === '--allowDuplicates'
const isNewlineTypeArgument = (name: string): boolean => name === '-n' || name === '--newlineType'
 
const isArgumentValueRequired = (name: string): boolean =>
    isDistFileArgument(name) || isLocalFileArgument(name) || isNewlineTypeArgument(name)
 
const getArgumentNameAndValue = (rawArguments: RawArgument[], i: number): Argument => {
    const rawArgument = rawArguments[i]
    const [name, inlineValue] = getArgumentNameAndInlineValue(rawArgument)
 
    const hasInlineValue = typeof inlineValue !== 'undefined'
    if (hasInlineValue) return [name, inlineValue]
    
    const valueIndex = i + 1
    const value = rawArguments[valueIndex]
 
    const hasValue = !!value
    Iif (!hasValue && isArgumentValueRequired(name)) throw new MissingArgumentValueError().setName(name)
 
    const isEndOfRawArguments = rawArguments.length === valueIndex
    const isValueAnArgumentName = isArgumentName(value)
    if (isEndOfRawArguments || isValueAnArgumentName) return [name, true]
 
    return [name, value]
}
 
const parseArguments = (rawArguments: RawArgument[]): ArgumentMap => {
    const argumentMap: ArgumentMap = {}
    rawArguments
        .forEach((rawArgument: RawArgument, i: number) => {
            if (!isArgumentName(rawArgument)) return
            
            const [argumentName, argumentValue] = getArgumentNameAndValue(rawArguments, i)
            argumentMap[argumentName] = argumentValue
        })
 
    return argumentMap
}
 
const getBooleanFromArgumentValue = (value: ArgumentValue): boolean => value === 'false' ? false : true