main
  1// Copyright 2022 Google LLC
  2//
  3// Licensed under the Apache License, Version 2.0 (the "License");
  4// you may not use this file except in compliance with the License.
  5// You may obtain a copy of the License at
  6//
  7//     https://www.apache.org/licenses/LICENSE-2.0
  8//
  9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15import path from 'node:path'
 16import { type Buffer } from 'node:buffer'
 17import process from 'node:process'
 18import { type TSpawnStore } from './vendor-core.ts'
 19
 20export { isStringLiteral } from './vendor-core.ts'
 21
 22export function noop() {}
 23
 24export function identity<T>(v: T): T {
 25  return v
 26}
 27
 28export function randomId() {
 29  return Math.random().toString(36).slice(2)
 30}
 31
 32export function isString(obj: any) {
 33  return typeof obj === 'string'
 34}
 35
 36const utf8Decoder = new TextDecoder('utf-8')
 37export const bufToString = (buf: Buffer | string): string =>
 38  isString(buf) ? buf : utf8Decoder.decode(buf)
 39
 40export const bufArrJoin = (arr: TSpawnStore[keyof TSpawnStore]): string =>
 41  arr.reduce((acc, buf) => acc + bufToString(buf), '')
 42
 43export const getLast = <T>(arr: { length: number; [i: number]: any }): T =>
 44  arr[arr.length - 1]
 45
 46export function preferLocalBin(
 47  env: NodeJS.ProcessEnv,
 48  ...dirs: (string | undefined)[]
 49) {
 50  const pathKey =
 51    process.platform === 'win32'
 52      ? Object.keys(env)
 53          .reverse()
 54          .find((key) => key.toUpperCase() === 'PATH') || 'Path'
 55      : 'PATH'
 56  const pathValue = dirs
 57    .map(
 58      (c) =>
 59        c && [
 60          path.resolve(c as string, 'node_modules', '.bin'),
 61          path.resolve(c as string),
 62        ]
 63    )
 64    .flat()
 65    .concat(env[pathKey])
 66    .filter(Boolean)
 67    .join(path.delimiter)
 68
 69  return {
 70    ...env,
 71    [pathKey]: pathValue,
 72  }
 73}
 74
 75export function quote(arg: string): string {
 76  if (arg === '') return `$''`
 77  if (/^[\w/.\-@:=]+$/.test(arg)) return arg
 78
 79  return (
 80    `$'` +
 81    arg
 82      .replace(/\\/g, '\\\\')
 83      .replace(/'/g, "\\'")
 84      .replace(/\f/g, '\\f')
 85      .replace(/\n/g, '\\n')
 86      .replace(/\r/g, '\\r')
 87      .replace(/\t/g, '\\t')
 88      .replace(/\v/g, '\\v')
 89      .replace(/\0/g, '\\0') +
 90    `'`
 91  )
 92}
 93
 94export function quotePowerShell(arg: string): string {
 95  if (arg === '') return `''`
 96  if (/^[\w/.\-]+$/.test(arg)) return arg
 97
 98  return `'` + arg.replace(/'/g, "''") + `'`
 99}
100
101export type Duration =
102  | number
103  | `${number}`
104  | `${number}m`
105  | `${number}s`
106  | `${number}ms`
107
108export function parseDuration(d: Duration) {
109  if (typeof d === 'number') {
110    if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`)
111    return d
112  }
113  const [m, v, u] = d.match(/^(\d+)(m?s?)$/) || []
114  if (!m) throw new Error(`Unknown duration: "${d}".`)
115
116  return +v * ({ s: 1000, ms: 1, m: 60_000 }[u] || 1)
117}
118
119export const once = <T extends (...args: any[]) => any>(fn: T) => {
120  let called = false
121  let result: ReturnType<T>
122
123  return (...args: Parameters<T>): ReturnType<T> =>
124    called ? result : ((called = true), (result = fn(...args)))
125}
126
127export const proxyOverride = <T extends object>(
128  origin: T,
129  ...fallbacks: any
130): T =>
131  new Proxy(origin, {
132    get(target: any, key) {
133      return (
134        fallbacks.find((f: any) => key in f)?.[key] ?? Reflect.get(target, key)
135      )
136    },
137  }) as T
138
139export const toCamelCase = (str: string) =>
140  str
141    .toLowerCase()
142    .replace(/([a-z])[_-]+([a-z])/g, (_, p1, p2) => p1 + p2.toUpperCase())
143
144export const parseBool = (v: string): boolean | string =>
145  v === 'true' || (v !== 'false' && v)
146
147export const getLines = (
148  chunk: Buffer | string,
149  next: (string | undefined)[],
150  delimiter: string | RegExp
151) => {
152  const lines = ((next.pop() || '') + bufToString(chunk)).split(delimiter)
153  next.push(lines.pop())
154  return lines
155}
156
157export const iteratorToArray = <T>(it: Iterator<T>): T[] => {
158  const arr = []
159  let entry
160  while (!(entry = it.next()).done) arr.push(entry.value)
161  return arr
162}