keep keep keep keep replace keep keep keep

const index = internalValue.findIndex(val => this.valueComparator(val, value))

// Styles
import './VItemGroup.sass'

// Mixins
import Comparable from '../../mixins/comparable'
import Groupable from '../../mixins/groupable'
import Proxyable from '../../mixins/proxyable'
import Themeable from '../../mixins/themeable'

// Utilities
import mixins from '../../util/mixins'
import { consoleWarn } from '../../util/console'

// Types
import { VNode } from 'vue/types'

export type GroupableInstance = InstanceType<typeof Groupable> & {
  id?: string
  to?: any
  value?: any
 }

export const BaseItemGroup = mixins(
  Comparable,
  Proxyable,
  Themeable
).extend({
  name: 'base-item-group',

  props: {
    activeClass: {
      type: String,
      default: 'v-item--active',
    },
    mandatory: Boolean,
    max: {
      type: [Number, String],
      default: null,
    },
    multiple: Boolean,
    tag: {
      type: String,
      default: 'div',
    },
  },

  data () {
    return {
      // As long as a value is defined, show it
      // Otherwise, check if multiple
      // to determine which default to provide
      internalLazyValue: this.value !== undefined
        ? this.value
        : this.multiple ? [] : undefined,
      items: [] as GroupableInstance[],
    }
  },

  computed: {
    classes (): Record<string, boolean> {
      return {
        'v-item-group': true,
        ...this.themeClasses,
      }
    },
    selectedIndex (): number {
      return (this.selectedItem && this.items.indexOf(this.selectedItem)) || -1
    },
    selectedItem (): GroupableInstance | undefined {
      if (this.multiple) return undefined

      return this.selectedItems[0]
    },
    selectedItems (): GroupableInstance[] {
      return this.items.filter((item, index) => {
        return this.toggleMethod(this.getValue(item, index))
      })
    },
    selectedValues (): any[] {
      if (this.internalValue == null) return []

      return Array.isArray(this.internalValue)
        ? this.internalValue
        : [this.internalValue]
    },
    toggleMethod (): (v: any) => boolean {
      if (!this.multiple) {
        return (v: any) => this.valueComparator(this.internalValue, v)
      }

      const internalValue = this.internalValue
      if (Array.isArray(internalValue)) {
        return (v: any) => internalValue.some(intern => this.valueComparator(intern, v))
      }

      return () => false
    },
  },

  watch: {
    internalValue: 'updateItemsState',
    items: 'updateItemsState',
  },

  created () {
    if (this.multiple && !Array.isArray(this.internalValue)) {
      consoleWarn('Model must be bound to an array if the multiple property is true.', this)
    }
  },

  methods: {

    genData (): object {
      return {
        class: this.classes,
      }
    },
    getValue (item: GroupableInstance, i: number): unknown {
      return item.value === undefined
        ? i
        : item.value
    },
    onClick (item: GroupableInstance) {
      this.updateInternalValue(
        this.getValue(item, this.items.indexOf(item))
      )
    },
    register (item: GroupableInstance) {
      const index = this.items.push(item) - 1

      item.$on('change', () => this.onClick(item))

      // If no value provided and mandatory,
      // assign first registered item
      if (this.mandatory && !this.selectedValues.length) {
        this.updateMandatory()
      }

      this.updateItem(item, index)
    },
    unregister (item: GroupableInstance) {
      if (this._isDestroyed) return

      const index = this.items.indexOf(item)
      const value = this.getValue(item, index)

      this.items.splice(index, 1)

      const valueIndex = this.selectedValues.indexOf(value)

      // Items is not selected, do nothing
      if (valueIndex < 0) return

      // If not mandatory, use regular update process
      if (!this.mandatory) {
        return this.updateInternalValue(value)
      }

      // Remove the value
      if (this.multiple && Array.isArray(this.internalValue)) {
        this.internalValue = this.internalValue.filter(v => v !== value)
      } else {
        this.internalValue = undefined
      }

      // If mandatory and we have no selection
      // add the last item as value
      /* istanbul ignore else */
      if (!this.selectedItems.length) {
        this.updateMandatory(true)
      }
    },
    updateItem (item: GroupableInstance, index: number) {
      const value = this.getValue(item, index)

      item.isActive = this.toggleMethod(value)
    },
    // https://github.com/vuetifyjs/vuetify/issues/5352
    updateItemsState () {
      this.$nextTick(() => {
        if (this.mandatory &&
          !this.selectedItems.length
        ) {
          return this.updateMandatory()
        }

        // TODO: Make this smarter so it
        // doesn't have to iterate every
        // child in an update
        this.items.forEach(this.updateItem)
      })
    },
    updateInternalValue (value: any) {
      this.multiple
        ? this.updateMultiple(value)
        : this.updateSingle(value)
    },
    updateMandatory (last?: boolean) {
      if (!this.items.length) return

      const items = this.items.slice()

      if (last) items.reverse()

      const item = items.find(item => !item.disabled)

      // If no tabs are available
      // aborts mandatory value
      if (!item) return

      const index = this.items.indexOf(item)

      this.updateInternalValue(
        this.getValue(item, index)
      )
    },
    updateMultiple (value: any) {
      const defaultValue = Array.isArray(this.internalValue)
        ? this.internalValue
        : []
      const internalValue = defaultValue.slice()
      const index = internalValue.findIndex(val => val === value)

      if (
        this.mandatory &&
        // Item already exists
        index > -1 &&
        // value would be reduced below min
        internalValue.length - 1 < 1
      ) return

      if (
        // Max is set
        this.max != null &&
        // Item doesn't exist
        index < 0 &&
        // value would be increased above max
        internalValue.length + 1 > this.max
      ) return

      index > -1
        ? internalValue.splice(index, 1)
        : internalValue.push(value)

      this.internalValue = internalValue
    },
    updateSingle (value: any) {
      const isSame = value === this.internalValue

      if (this.mandatory && isSame) return

      this.internalValue = isSame ? undefined : value
    },
  },

  render (h): VNode {
    return h(this.tag, this.genData(), this.$slots.default)
  },
})

export default BaseItemGroup.extend({
  name: 'v-item-group',

  provide (): object {
    return {
      itemGroup: this,
    }
  },
})


{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "es2015",                       /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": [                                  /* Specify library files to be included in the compilation */
      "es2015",
      "es2016",
      "es2017",
      "es2018",
      "dom",
      "dom.iterable"
    ],
    "allowJs": false,                         /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    "sourceMap": true,                        /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    "outDir": "./dist",                       /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    "downlevelIteration": true,               /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": true,                   /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    "noImplicitReturns": true,                /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {                             /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // },
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "./",                    /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "./",                       /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true             /* Enables experimental support for emitting type metadata for decorators. */
  },
  "exclude": [
    "**/*.spec.ts",
    "**/*.spec.js",
    "node_modules"
  ]
}


import { CreateElement, VNode } from 'vue'

export default function rebuildFunctionalSlots (slots: { [key: string]: VNode[] | undefined }, h: CreateElement) {
  const children: VNode[] = []

  for (const slot in slots) {
    if (slots.hasOwnProperty(slot)) {
      children.push(h('template', { slot }, slots[slot]))
    }
  }

  return children
}


import { VNodeDirective, VNode } from 'vue/types/vnode'
import { keys } from '../../util/helpers'
import { TouchHandlers, TouchValue, TouchWrapper } from 'vuetify/types'

export interface TouchStoredHandlers {
  touchstart: (e: TouchEvent) => void
  touchend: (e: TouchEvent) => void
  touchmove: (e: TouchEvent) => void
}

interface TouchVNodeDirective extends VNodeDirective {
  value?: TouchValue
}

const handleGesture = (wrapper: TouchWrapper) => {
  const { touchstartX, touchendX, touchstartY, touchendY } = wrapper
  const dirRatio = 0.5
  const minDistance = 16
  wrapper.offsetX = touchendX - touchstartX
  wrapper.offsetY = touchendY - touchstartY

  if (Math.abs(wrapper.offsetY) < dirRatio * Math.abs(wrapper.offsetX)) {
    wrapper.left && (touchendX < touchstartX - minDistance) && wrapper.left(wrapper)
    wrapper.right && (touchendX > touchstartX + minDistance) && wrapper.right(wrapper)
  }

  if (Math.abs(wrapper.offsetX) < dirRatio * Math.abs(wrapper.offsetY)) {
    wrapper.up && (touchendY < touchstartY - minDistance) && wrapper.up(wrapper)
    wrapper.down && (touchendY > touchstartY + minDistance) && wrapper.down(wrapper)
  }
}

function touchstart (event: TouchEvent, wrapper: TouchWrapper) {
  const touch = event.changedTouches[0]
  wrapper.touchstartX = touch.clientX
  wrapper.touchstartY = touch.clientY

  wrapper.start &&
    wrapper.start(Object.assign(event, wrapper))
}

function touchend (event: TouchEvent, wrapper: TouchWrapper) {
  const touch = event.changedTouches[0]
  wrapper.touchendX = touch.clientX
  wrapper.touchendY = touch.clientY

  wrapper.end &&
    wrapper.end(Object.assign(event, wrapper))

  handleGesture(wrapper)
}

function touchmove (event: TouchEvent, wrapper: TouchWrapper) {
  const touch = event.changedTouches[0]
  wrapper.touchmoveX = touch.clientX
  wrapper.touchmoveY = touch.clientY

  wrapper.move && wrapper.move(Object.assign(event, wrapper))
}

function createHandlers (value: TouchHandlers): TouchStoredHandlers {
  const wrapper = {
    touchstartX: 0,
    touchstartY: 0,
    touchendX: 0,
    touchendY: 0,
    touchmoveX: 0,
    touchmoveY: 0,
    offsetX: 0,
    offsetY: 0,
    left: value.left,
    right: value.right,
    up: value.up,
    down: value.down,
    start: value.start,
    move: value.move,
    end: value.end,
  }

  return {
    touchstart: (e: TouchEvent) => touchstart(e, wrapper),
    touchend: (e: TouchEvent) => touchend(e, wrapper),
    touchmove: (e: TouchEvent) => touchmove(e, wrapper),
  }
}

function inserted (el: HTMLElement, binding: TouchVNodeDirective, vnode: VNode) {
  const value = binding.value!
  const target = value.parent ? el.parentElement : el
  const options = value.options || { passive: true }

  // Needed to pass unit tests
  if (!target) return

  const handlers = createHandlers(binding.value!)
  target._touchHandlers = Object(target._touchHandlers)
  target._touchHandlers![vnode.context!._uid] = handlers

  keys(handlers).forEach(eventName => {
    target.addEventListener(eventName, handlers[eventName] as EventListener, options)
  })
}

function unbind (el: HTMLElement, binding: TouchVNodeDirective, vnode: VNode) {
  const target = binding.value!.parent ? el.parentElement : el
  if (!target || !target._touchHandlers) return

  const handlers = target._touchHandlers[vnode.context!._uid]
  keys(handlers).forEach(eventName => {
    target.removeEventListener(eventName, handlers[eventName])
  })
  delete target._touchHandlers[vnode.context!._uid]
}

export const Touch = {
  inserted,
  unbind,
}

export default Touch


const isSame = value === this.internalValue

keep keep keep keep keep replace keep keep keep

const isSame = this.valueComparator(this.internalValue, value)


<template>
  <app-tooltip-btn
    :to="{
      name: 'Documentation',
      params: {
        category: 'resources',
        page: 'jobs-for-vue'
      }
    }"
    class="jobs-link"
    path="jobs"
    @click="onClick"
  >
    <template #icon>
      <v-badge
        :value="showBadge"
        color="#ED561B"
        dot
        left
        overlap
      >
        <v-icon
          class="mx-1"
          v-text="icon"
        />
      </v-badge>
    </template>
  </app-tooltip-btn>
</template>

<script>
  // Utilities
  import { call, get } from 'vuex-pathify'
  import { differenceInDays, parseISO } from 'date-fns'

  export default {
    name: 'JobsLink',

    computed: {
      name: get('route/name'),
      newJobs: get('jobs/newJobs'),
      notification: get('user/last@jobs'),
      page: get('route/params@page'),
      icon () {
        return this.page === 'jobs-for-vue'
          ? '$mdiBriefcaseVariant'
          : '$mdiBriefcaseVariantOutline'
      },
      showBadge () {
        return (
          this.newJobs.length &&
          (
            !this.notification ||
            differenceInDays(Date.now(), parseISO(this.notification)) < 3
          )
        )
      },
    },

    async mounted () {
      this.fetch()
    },

    methods: {
      fetch: call('jobs/fetch'),
      onClick () {
        this.$gtag.event('click', {
          event_category: 'toolbar',
          event_label: 'jobs',
          value: this.name,
        })
      },
    },
  }
</script>

<style lang="sass">
  .jobs-link .v-btn:not(:hover)::before
    display: none
</style>


module.exports = {
  'v-virtual-scroll': {
    slots: [
      {
        name: 'default',
        props: {
          index: 'number',
          item: 'any',
        },
      },
    ],
  },
}


---
meta:
  title: CSS Elevation helpers
  description: Elevation helper classes allow you to control relative depth, or distance, between two surfaces along the z-axis.
  keywords: elevation helper classes, elevation classes, vuetify elevation
related:
  - /components/cards/
  - /components/sheets/
  - /components/bottom-sheets/
---

# Elevation

The elevation helpers allow you to control relative depth, or distance, between two surfaces along the **z-axis**. There is a total of 25 elevation levels. You can set an element's elevation by using the class `elevation-{n}`, where `n` is a integer between 0-24 corresponding to the desired elevation.

<entry-ad />

## Usage

The `elevation` helper classes allow you to assign a custom **z-depth** to any element.

<example file="elevation/usage" />

## Examples

### Props

#### Dynamic elevation

Numerous components utilize the **elevatable** mixin and are given an **elevation** prop. For components that are not supported, you can dynamically change the class

<example file="elevation/prop-dynamic" />

<backmatter />


// Toggling on and off object references

expect(wrapper.vm.internalValue).toEqual({ foo: 'foo' })

// Toggling on and off with custom comparator

wrapper.setProps({ valueComparator: (a: any, b: any) => a?.startsWith(b?.[0]), value: 'foo' })


<template>
  <v-card flat>
    <v-card-text>
      <v-container fluid>
        <v-row>
          <v-col
            cols="12"
            sm="6"
            md="6"
          >
            <v-radio-group
              v-model="ex7"
              column
            >
              <v-radio
                label="red"
                color="red"
                value="red"
              ></v-radio>
              <v-radio
                label="red darken-3"
                color="red darken-3"
                value="red darken-3"
              ></v-radio>
              <v-radio
                label="indigo"
                color="indigo"
                value="indigo"
              ></v-radio>
              <v-radio
                label="indigo darken-3"
                color="indigo darken-3"
                value="indigo darken-3"
              ></v-radio>
              <v-radio
                label="orange"
                color="orange"
                value="orange"
              ></v-radio>
              <v-radio
                label="orange darken-3"
                color="orange darken-3"
                value="orange darken-3"
              ></v-radio>
            </v-radio-group>
          </v-col>
          <v-col
            cols="12"
            sm="6"
            md="6"
          >
            <v-radio-group
              v-model="ex8"
              column
            >
              <v-radio
                label="primary"
                color="primary"
                value="primary"
              ></v-radio>
              <v-radio
                label="secondary"
                color="secondary"
                value="secondary"
              ></v-radio>
              <v-radio
                label="success"
                color="success"
                value="success"
              ></v-radio>
              <v-radio
                label="info"
                color="info"
                value="info"
              ></v-radio>
              <v-radio
                label="warning"
                color="warning"
                value="warning"
              ></v-radio>
              <v-radio
                label="error"
                color="error"
                value="error"
              ></v-radio>
            </v-radio-group>
          </v-col>
        </v-row>
      </v-container>
    </v-card-text>
  </v-card>
</template>

<script>
  export default {
    data () {
      return {
        ex7: 'red',
        ex8: 'primary',
      }
    },
  }
</script>


module.exports = {
  'v-sparkline': {
    slots: [
      {
        name: 'label',
        value: undefined,
      },
    ],
  },
}


node scripts/warn-npm-install.js && yarn run lint --since HEAD && yarn lerna run test --since HEAD

lerna run start --scope vuetifyjs.com --stream


keep keep keep add keep keep keep keep keep

wrapper.vm.updateMultiple({ foo: 'foo' })

expect(wrapper.vm.internalValue).toEqual([{ foo: 'foo' }])

// Libraries
import Vue from 'vue'

// Components
import VItem from '../VItem'
import VItemGroup from '../VItemGroup'

// Utilities
import {
  createLocalVue,
  mount,
  Wrapper,
} from '@vue/test-utils'
import { ExtractVue } from './../../../util/mixins'

const vm = new Vue()
const defaultSlot = ({ toggle }) => vm.$createElement('div', { on: { click: toggle } }, 'foobar')

const Mock = {
  name: 'test',

  render: h => h(VItem, {
    scopedSlots: {
      default: defaultSlot,
    },
  }),
}

describe('VItemGroup', () => {
  type Instance = ExtractVue<typeof VItemGroup>
  let mountFunction: (options?: object) => Wrapper<Instance>
  let localVue: typeof Vue

  beforeEach(() => {
    localVue = createLocalVue()

    mountFunction = (options = {}) => {
      return mount(VItemGroup, {
        localVue,
        ...options,
      })
    }
  })

  it('should warn if using multiple prop without an array value', () => {
    mountFunction({
      propsData: {
        multiple: true,
        value: '',
      },
    })

    expect('Model must be bound to an array if the multiple property is true').toHaveBeenTipped()
  })

  it('should return the correct value', () => {
    const wrapper = mountFunction()

    const getValue = wrapper.vm.getValue

    expect(getValue({ value: null }, 0)).toBeNull()
    expect(getValue({ value: undefined }, 1)).toBe(1)
    expect(getValue({ value: '' }, 2)).toBe('')
    expect(getValue({ value: 'foo' }, 'foo')).toBe('foo')
  })

  it('should register elements', () => {
    const wrapper = mountFunction({
      slots: {
        default: [Mock],
      },
    })

    expect(wrapper.vm.items).toHaveLength(1)

    const item = wrapper.find(Mock)

    item.destroy()

    expect(wrapper.vm.items).toHaveLength(0)
  })

  it('should register and activate elements', () => {
    const wrapper = mountFunction({
      propsData: { value: 0 },
      slots: { default: [Mock] },
    })

    expect(wrapper.vm.items).toHaveLength(1)

    // Avoriaz doesn't like
    // components without
    // a render function
    const item = wrapper.find({
      name: 'v-item',
      render: () => null,
    })

    expect(item.vm.isActive).toBe(true)
  })

  it('should update state from child clicks', () => {
    const change = jest.fn()
    const wrapper = mountFunction({
      slots: {
        default: [
          Mock,
          Mock,
        ],
      },
    })

    wrapper.vm.$on('change', change)

    expect(wrapper.vm.items).toHaveLength(2)

    const [child1, child2] = wrapper.vm.$el.children

    child1.click()
    expect(change).toHaveBeenCalledWith(0)
    expect(wrapper.vm.internalValue).toBe(0)

    child2.click()
    expect(change).toHaveBeenCalledWith(1)
    expect(wrapper.vm.internalValue).toBe(1)

    child2.click()
    expect(wrapper.vm.internalValue).toBeUndefined()

    wrapper.setProps({
      value: [],
      multiple: true,
    })

    child1.click()
    expect(change).toHaveBeenCalledWith([0])

    child2.click()
    expect(change).toHaveBeenCalledWith([0, 1])

    child1.click()
    expect(change).toHaveBeenCalledWith([1])
  })

  it('should have a conditional method for toggling items', () => {
    const wrapper = mountFunction()

    expect(wrapper.vm.toggleMethod(0)).toBe(false)

    wrapper.setProps({ value: 0 })

    expect(wrapper.vm.toggleMethod(0)).toBe(true)

    wrapper.setProps({
      multiple: true,
      value: [],
    })

    expect(wrapper.vm.toggleMethod(0)).toBe(false)

    wrapper.setProps({ value: [0] })

    expect(wrapper.vm.toggleMethod(0)).toBe(true)

    wrapper.setProps({ value: 0 })

    expect(wrapper.vm.toggleMethod(0)).toBe(false)
  })

  it('should correctly be active with objects having different references', () => {
    const wrapper = mountFunction()

    wrapper.setProps({ value: { a: 1 } })
    expect(wrapper.vm.toggleMethod({ a: 1 })).toBe(true)
    expect(wrapper.vm.toggleMethod({ a: 2 })).toBe(false)
  })

  it('should have a customizable comparator function', () => {
    const wrapper = mountFunction()

    wrapper.setProps({ valueComparator: (a: any, b: any) => a === b + 1, value: 0 })

    expect(wrapper.vm.toggleMethod(0)).toBe(false)
    expect(wrapper.vm.toggleMethod(-1)).toBe(true)
  })

  it('should select the first item if mandatory and no value', async () => {
    const wrapper = mountFunction({
      propsData: { mandatory: true },
      slots: {
        default: [Mock],
      },
    })

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedItems).toHaveLength(1)
    expect(wrapper.vm.internalValue).toBe(0)

    wrapper.setProps({ multiple: true })

    // Manually update selected items
    wrapper.vm.updateItemsState()

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.selectedItems).toHaveLength(1)
    expect(wrapper.vm.internalValue).toEqual([0])
  })

  it('should update a single item group', () => {
    const wrapper = mountFunction()

    // Toggling on and off
    wrapper.vm.updateSingle('foo')
    expect(wrapper.vm.internalValue).toBe('foo')
    wrapper.vm.updateSingle('foo')
    expect(wrapper.vm.internalValue).toBeUndefined()

    wrapper.setProps({ mandatory: true })

    // Toggling off single mandatory
    wrapper.vm.updateSingle('foo')
    expect(wrapper.vm.internalValue).toBe('foo')
    wrapper.vm.updateSingle('foo')
    expect(wrapper.vm.internalValue).toBe('foo')
  })

  it('should update a multiple item group', () => {
    const wrapper = mountFunction({
      propsData: { multiple: true },
    })

    // Toggling on and off
    wrapper.vm.updateMultiple('foo')
    expect(wrapper.vm.internalValue).toEqual(['foo'])
    wrapper.vm.updateMultiple('foo')
    expect(wrapper.vm.internalValue).toEqual([])

    wrapper.setProps({ mandatory: true })

    // Toggling off single mandatory
    wrapper.vm.updateMultiple('foo')
    expect(wrapper.vm.internalValue).toEqual(['foo'])
    wrapper.vm.updateMultiple('foo')
    expect(wrapper.vm.internalValue).toEqual(['foo'])

    wrapper.setProps({ max: 3 })

    // Should enforce maximum selection
    wrapper.vm.updateMultiple('bar')
    expect(wrapper.vm.internalValue).toEqual(['foo', 'bar'])
    wrapper.vm.updateMultiple('fizz')
    expect(wrapper.vm.internalValue).toEqual(['foo', 'bar', 'fizz'])
    wrapper.vm.updateMultiple('buzz')
    expect(wrapper.vm.internalValue).toEqual(['foo', 'bar', 'fizz'])
  })

  it('should update value if mandatory and dynamic items', async () => {
    const wrapper = mountFunction({
      propsData: {
        multiple: true,
        value: [3],
      },
      slots: {
        default: [
          Mock,
          Mock,
          Mock,
          Mock,
        ],
      },
    })

    const change = jest.fn()
    wrapper.vm.$on('change', change)

    const [first, second, third, fourth] = wrapper.findAll(Mock).wrappers

    fourth.destroy()

    expect(change).toHaveBeenCalledWith([])

    wrapper.setProps({ mandatory: true, value: [2] })

    third.destroy()

    expect(change).toHaveBeenCalledWith([1])

    wrapper.setProps({ multiple: false, value: 1 })

    second.destroy()

    expect(change).toHaveBeenCalledWith(0)

    first.destroy()

    expect(change).toHaveBeenCalledWith(undefined)
  })

  // https://github.com/vuetifyjs/vuetify/issues/5384
  it('should not unregister children when is destroyed', () => {
    const wrapper = mountFunction({
      propsData: {
        value: 0,
      },
      slots: {
        default: [Mock],
      },
    })

    const change = jest.fn()

    wrapper.vm.$on('change', change)

    wrapper.destroy()

    expect(change).not.toHaveBeenCalled()
  })

  // https://github.com/vuetifyjs/vuetify/issues/5000
  it('should update mandatory to first non-disabled item', () => {
    const Mock2 = {
      name: 'mock2',

      render (h) {
        return h(VItem, {
          props: {
            disabled: true,
          },
          scopedSlots: {
            default: defaultSlot,
          },
        })
      },
    }

    const wrapper = mountFunction({
      propsData: {
        mandatory: true,
      },
      slots: {
        default: [
          Mock2,
          Mock,
          Mock,
        ],
      },
    })

    expect(wrapper.vm.internalValue).toBe(1)
  })

  // https://github.com/vuetifyjs/vuetify/issues/6278
  it('should infer index dynamically', () => {
    const wrapper = mount(VItemGroup, {
      propsData: { value: 0 },
      slots: {
        default: [
          Mock,
          Mock,
          Mock,
        ],
      },
    })

    const items = wrapper.findAll(Mock)
    const item2 = items.at(1)
    const item3 = items.at(2)

    item2.destroy()

    item3.trigger('click')

    expect(wrapper.vm.internalValue).toBe(1)
  })

  it('should have the correct selected index, item and items', () => {
    const wrapper = mountFunction({
      slots: {
        default: [Mock, Mock, Mock],
      },
    })

    expect(wrapper.vm.items).toHaveLength(3)

    wrapper.setProps({ value: 1 })

    expect(wrapper.vm.selectedIndex).toBe(1)
    expect(wrapper.vm.selectedItem).toEqual(wrapper.vm.items[1])

    wrapper.setProps({ value: 2 })

    expect(wrapper.vm.selectedIndex).toBe(2)
    expect(wrapper.vm.selectedItem).toEqual(wrapper.vm.items[2])
  })

  it('should render with a specified tag when the tag prop is provided with a value', () => {
    const wrapper = mountFunction({
      propsData: {
        tag: 'button',
      },
    })

    expect(wrapper.is('button')).toBe(true)
  })
})


import VChip from './VChip'

export { VChip }
export default VChip


<template>
  <v-container>
    <v-row>
      <v-col cols="12">
        <h3
          ref="radio"
          class="text-h5"
        >
          Target
        </h3>

        <v-radio-group
          v-model="type"
          row
        >
          <v-radio
            label="Number"
            value="number"
          ></v-radio>

          <v-radio
            label="Selector"
            value="selector"
          ></v-radio>

          <v-radio
            label="DOMElement"
            value="element"
          ></v-radio>
        </v-radio-group>

        <v-text-field
          v-if="type === 'number'"
          v-model="number"
          type="number"
          label="Number"
        ></v-text-field>

        <v-select
          v-if="type === 'selector'"
          v-model="selector"
          label="Selector"
          :items="selections"
        ></v-select>

        <v-select
          v-if="type === 'element'"
          v-model="selected"
          :items="elements"
          label="DOMElement"
        ></v-select>
      </v-col>

      <v-col cols="12">
        <h3 class="text-h5">
          Options
        </h3>

        <v-select
          v-model="easing"
          :items="easings"
          label="Easing"
        ></v-select>

        <v-slider
          v-model="duration"
          min="0"
          max="1000"
          label="Duration"
          thumb-label
        ></v-slider>

        <v-slider
          v-model="offset"
          min="-500"
          max="500"
          label="Offset"
          thumb-label
        ></v-slider>
      </v-col>

      <v-col>
        <v-btn
          ref="button"
          block
          color="primary"
          @click="$vuetify.goTo(target, options)"
        >
          scroll
        </v-btn>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
  import * as easings from 'vuetify/lib/services/goto/easing-patterns'

  export default {
    data () {
      return {
        type: 'number',
        number: 9999,
        selector: '#first',
        selections: ['#first', '#second', '#third'],
        selected: 'Button',
        elements: ['Button', 'Radio group'],
        duration: 300,
        offset: 0,
        easing: 'easeInOutCubic',
        easings: Object.keys(easings),
      }
    },
    computed: {
      target () {
        const value = this[this.type]
        if (!isNaN(value)) return Number(value)
        else return value
      },
      options () {
        return {
          duration: this.duration,
          offset: this.offset,
          easing: this.easing,
        }
      },
      element () {
        if (this.selected === 'Button') return this.$refs.button
        else if (this.selected === 'Radio group') return this.$refs.radio
        else return null
      },
    },
  }
</script>

<codepen-additional>
  const easings = {
    linear: '',
    easeInQuad: '',
    easeOutQuad: '',
    easeInOutQuad: '',
    easeInCubic: '',
    easeOutCubic: '',
    easeInOutCubic: '',
    easeInQuart: '',
    easeOutQuart: '',
    easeInOutQuart: '',
    easeInQuint: '',
    easeOutQuint: '',
    easeInOutQuint: ''
  }
</codepen-additional>


---
meta:
  title: Progress circular component
  description: The progress circular component is useful for displaying a visual indicator of numerical data in a circle.
  keywords: progress circular, vuetify progress circular component, vue progress circular component, circular progress
related:
  - /components/cards/
  - /components/progress-linear/
  - /components/lists/
---

# Progress circular

The `v-progress-circular` component is used to convey data circularly to users. It also can be put into an indeterminate state to portray loading.

<entry-ad />

## Usage

In its simplest form, v-progress-circular displays a circular progress bar. Use the value prop to control the progress.

<example file="v-progress-circular/usage" />

## API

- [v-progress-circular](/api/v-progress-circular)

<inline-api page="components/progress-circular" />

## Examples

### Props

#### Color

Alternate colors can be applied to `v-progress-circular` using the `color` prop.

<example file="v-progress-circular/prop-color" />

#### Indeterminate

Using the `indeterminate` prop, a `v-progress-circular` continues to animate indefinitely.

<example file="v-progress-circular/prop-indeterminate" />

#### Rotate

The `rotate` prop gives you the ability to customize the `v-progress-circular`'s origin.

<example file="v-progress-circular/prop-rotate" />

#### Size and Width

The `size` and `width` props allow you to easily alter the size and width of the `v-progress-circular` component.

<example file="v-progress-circular/prop-size-and-width" />

<backmatter />


it('should update value if mandatory and dynamic items', async () => {

it('should update a multiple item group with a custom comparator', () => {

wrapper.setProps({ valueComparator: (a: any, b: any) => a?.startsWith(b?.[0]), value: ['foo'] })


// Libraries
import Vue from 'vue'

// Plugins
import Router from 'vue-router'

// Components
import VBtn from '../VBtn'

// Utilities
import {
  createLocalVue,
  mount,
  Wrapper,
} from '@vue/test-utils'
import { compileToFunctions } from 'vue-template-compiler'

describe('VBtn.ts', () => { // eslint-disable-line max-statements
  let mountFunction: (options?: object) => Wrapper<Vue>
  let router: Router
  let localVue: typeof Vue

  beforeEach(() => {
    router = new Router()
    localVue = createLocalVue()
    localVue.use(Router)

    mountFunction = (options = {}) => {
      return mount(VBtn, {
        localVue,
        router,
        ...options,
      })
    }
  })

  it('should render component and match snapshot', () => {
    expect(mountFunction().html()).toMatchSnapshot()
  })

  it('should render component with color prop and match snapshot', () => {
    expect(mountFunction({
      propsData: {
        color: 'green darken-1',
      },
    }).html()).toMatchSnapshot()

    expect(mountFunction({
      propsData: {
        color: 'green darken-1',
        text: true,
      },
    }).html()).toMatchSnapshot()
  })

  it('should render component with loader slot and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        loading: true,
      },
      slots: {
        loader: [compileToFunctions('loader')],
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render component with loader and match snapshot', () => {
    const wrapper = mount(VBtn, {
      propsData: {
        loading: true,
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render tile button and match snapshot', () => {
    const wrapper = mount(VBtn, {
      propsData: {
        tile: true,
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render an  tag when using href prop', () => {
    const wrapper = mountFunction({
      propsData: {
        href: 'http://www.google.com',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render specified tag when using tag prop', () => {
    const wrapper = mountFunction({
      propsData: {
        tag: 'a',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should register and unregister', () => {
    const register = jest.fn()
    const unregister = jest.fn()

    const wrapper = mountFunction({
      provide: {
        btnToggle: {
          register,
          unregister,
        },
      },
    })

    expect(register).toHaveBeenCalled()
    wrapper.destroy()
    expect(unregister).toHaveBeenCalled()
  })

  it('should emit a click event', async () => {
    const wrapper = mountFunction({
      propsData: {
        href: '#!',
      },
    })

    const click = jest.fn()
    wrapper.vm.$on('click', click)
    wrapper.trigger('click')

    wrapper.setProps({ href: undefined, to: '/foo' })
    wrapper.trigger('click')

    expect(click.mock.calls).toHaveLength(2)
  })

  it('should use custom active-class', () => {
    const wrapper = mountFunction({
      propsData: {
        inputValue: true,
        activeClass: 'foo',
      },
    })

    expect(wrapper.classes('foo')).toBe(true)
  })

  it('should have v-btn--plain class when plain prop is set to true', () => {
    const wrapper = mountFunction({
      propsData: {
        plain: true,
      },
    })

    expect(wrapper.classes('v-btn--plain')).toBe(true)
  })

  it('should have the correct icon classes', () => {
    const wrapper = mountFunction({
      propsData: {
        icon: true,
      },
    })
    expect(wrapper.classes('v-btn--icon')).toBe(true)

    wrapper.setProps({ icon: false })

    expect(wrapper.classes('v-btn--icon')).toBe(false)
  })

  it('should have the correct elevation', async () => { // eslint-disable-line max-statements
    const wrapper = mountFunction()

    wrapper.setProps({ disabled: true })
    expect(wrapper.classes('elevation-2')).toBe(false)
    expect(wrapper.classes('v-btn--disabled')).toBe(true)

    wrapper.setProps({ disabled: false, elevation: 24 })
    expect(wrapper.classes('elevation-24')).toBe(true)

    wrapper.setProps({ elevation: 2 })
    expect(wrapper.classes('elevation-2')).toBe(true)
  })

  it('should toggle on route change if provided a to prop', async () => {
    const toggle = jest.fn()
    const register = jest.fn()
    const unregister = jest.fn()
    const wrapper = mountFunction({
      provide: {
        btnToggle: {
          activeClass: 'foobar',
          register,
          unregister,
        },
      },
      methods: { toggle },
      ref: 'link',
    })

    router.push('/foobar')

    await wrapper.vm.$nextTick()
    expect(toggle).not.toHaveBeenCalled()

    wrapper.setProps({ to: 'fizzbuzz' })

    router.push('/fizzbuzz')

    await wrapper.vm.$nextTick()
    expect(toggle).toHaveBeenCalled()
  })

  it('should call toggle when used in button group', () => {
    const register = jest.fn()
    const unregister = jest.fn()
    const toggle = jest.fn()
    const wrapper = mountFunction({
      provide: {
        btnToggle: { register, unregister },
      },
      methods: { toggle },
    })

    wrapper.trigger('click')
    expect(toggle).toHaveBeenCalled()
  })

  it('should stringify non string|number values', () => {
    const wrapper = mountFunction({
      propsData: {
        value: 'foo',
      },
    })

    expect(wrapper.attributes('value')).toBe('foo')

    wrapper.setProps({ value: 2 })
    expect(wrapper.attributes('value')).toBe('2')

    wrapper.setProps({ value: { foo: 'bar' } })
    expect(wrapper.attributes('value')).toBe('{"foo":"bar"}')
  })

  it('should not add color classes if disabled', () => {
    const wrapper = mountFunction({
      propsData: {
        color: 'primary darken-2',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()

    wrapper.setProps({
      disabled: true,
    })
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should retain focus when clicked', async () => {
    const wrapper = mountFunction({
      propsData: {
        retainFocusOnClick: true,
      },
    })
    const event = new MouseEvent('click', { detail: 1 })
    const blur = jest.fn()

    wrapper.element.blur = blur
    wrapper.element.dispatchEvent(event)

    expect(blur).not.toHaveBeenCalled()

    wrapper.setProps({ retainFocusOnClick: false })
    wrapper.element.dispatchEvent(event)

    expect(blur).toHaveBeenCalled()
  })
})


import VDialog from './VDialog'

export { VDialog }
export default VDialog


<template>
  <app-btn
    :to="{
      name: 'Documentation',
      params: {
        category: 'introduction',
        page: 'why-vuetify'
      }
    }"
    path="guide"
    @click="onClick"
  />
</template>

<script>
  // Utilities
  import { get } from 'vuex-pathify'

  export default {
    name: 'GuideLink',

    computed: {
      name: get('route/name'),
    },

    methods: {
      onClick () {
        this.$gtag.event('click', {
          event_category: 'toolbar',
          event_label: 'guide',
          value: this.name,
        })
      },
    },
  }
</script>


['freebsd11', 'freebsd12', 'freebsd13', 'freebsd14', 'freebsd15', 'openbsd', 'netbsd'].includes(binaryTarget)

import Debug from '@prisma/debug'
import {
  assertNodeAPISupported,
  BinaryTarget,
  binaryTargets,
  getNodeAPIName,
  getPlatformInfo,
} from '@prisma/get-platform'
import execa from 'execa'
import fs from 'fs'
import { ensureDir } from 'fs-extra'
import { bold, yellow } from 'kleur/colors'
import pFilter from 'p-filter'
import path from 'path'
import tempDir from 'temp-dir'
import { promisify } from 'util'

import { BinaryType } from './BinaryType'
import { chmodPlusX } from './chmodPlusX'
import { cleanupCache } from './cleanupCache'
import { downloadZip } from './downloadZip'
import { allEngineEnvVarsSet, getBinaryEnvVarPath } from './env'
import { getHash } from './getHash'
import { getBar } from './log'
import { getCacheDir, getDownloadUrl, overwriteFile } from './utils'

const { enginesOverride } = require('../package.json')

const debug = Debug('prisma:fetch-engine:download')
const exists = promisify(fs.exists)

const channel = 'master'

// matches `/snapshot/` or `C:\\snapshot\\` or `C:/snapshot/` for vercel's pkg apps
export const vercelPkgPathRegex = /^((\w:[\\\/])|\/)snapshot[\/\\]/

export type BinaryDownloadConfiguration = {
  [binary in BinaryType]?: string // that is a path to the binary download location
}

export type BinaryPaths = {
  [binary in BinaryType]?: { [binaryTarget in BinaryTarget]: string } // key: target, value: path
}

export interface DownloadOptions {
  binaries: BinaryDownloadConfiguration
  binaryTargets?: BinaryTarget[]
  showProgress?: boolean
  progressCb?: (progress: number) => void
  version?: string
  skipDownload?: boolean
  failSilent?: boolean
  printVersion?: boolean
  skipCacheIntegrityCheck?: boolean
}

type BinaryDownloadJob = {
  binaryName: string
  targetFolder: string
  binaryTarget: BinaryTarget
  fileName: string
  targetFilePath: string
  envVarPath: string | undefined
  skipCacheIntegrityCheck: boolean
}

export async function download(options: DownloadOptions): Promise<BinaryPaths> {
  if (enginesOverride?.['branch'] || enginesOverride?.['folder']) {
    // if this is true the engines have been fetched before and already cached
    // into .cache/prisma/master/_local_ for us to be able to use this version
    options.version = '_local_'
    options.skipCacheIntegrityCheck = true
  }

  // get platform
  const { binaryTarget, ...os } = await getPlatformInfo()

  if (os.targetDistro && ['nixos'].includes(os.targetDistro) && !allEngineEnvVarsSet(Object.keys(options.binaries))) {
    console.error(
      `${yellow('Warning')} Precompiled engine files are not available for ${
        os.targetDistro
      }, please provide the paths via environment variables, see https://pris.ly/d/custom-engines`,
    )
  } else if (['freebsd11', 'freebsd12', 'freebsd13', 'freebsd14', 'openbsd', 'netbsd'].includes(binaryTarget)) {
    console.error(
      `${yellow(
        'Warning',
      )} Precompiled engine files are not available for ${binaryTarget}. Read more about building your own engines at https://pris.ly/d/build-engines`,
    )
  } else if (BinaryType.QueryEngineLibrary in options.binaries) {
    assertNodeAPISupported()
  }

  // no need to do anything, if there are no binaries
  if (!options.binaries || Object.values(options.binaries).length === 0) {
    return {} // we don't download anything if nothing is provided
  }

  // merge options
  const opts = {
    ...options,
    binaryTargets: options.binaryTargets ?? [binaryTarget],
    version: options.version ?? 'latest',
    binaries: options.binaries,
  }

  // creates a matrix of binaries x binary targets
  const binaryJobs = Object.entries(opts.binaries).flatMap(([binaryName, targetFolder]) =>
    opts.binaryTargets.map((binaryTarget) => {
      const fileName = getBinaryName(binaryName as BinaryType, binaryTarget)
      const targetFilePath = path.join(targetFolder, fileName)
      return {
        binaryName,
        targetFolder,
        binaryTarget,
        fileName,
        targetFilePath,
        envVarPath: getBinaryEnvVarPath(binaryName as BinaryType)?.path,
        skipCacheIntegrityCheck: !!opts.skipCacheIntegrityCheck,
      }
    }),
  )

  if (process.env.BINARY_DOWNLOAD_VERSION) {
    debug(`process.env.BINARY_DOWNLOAD_VERSION is set to "${process.env.BINARY_DOWNLOAD_VERSION}"`)
    opts.version = process.env.BINARY_DOWNLOAD_VERSION
  }

  if (opts.printVersion) {
    console.log(`version: ${opts.version}`)
  }

  // filter out files, which don't yet exist or have to be created
  const binariesToDownload = await pFilter(binaryJobs, async (job) => {
    const needsToBeDownloaded = await binaryNeedsToBeDownloaded(job, binaryTarget, opts.version)
    const isSupported = binaryTargets.includes(job.binaryTarget as BinaryTarget)
    const shouldDownload =
      isSupported &&
      !job.envVarPath && // this is for custom binaries
      needsToBeDownloaded
    if (needsToBeDownloaded && !isSupported) {
      throw new Error(`Unknown binaryTarget ${job.binaryTarget} and no custom engine files were provided`)
    }
    return shouldDownload
  })

  if (binariesToDownload.length > 0) {
    const cleanupPromise = cleanupCache() // already start cleaning up while we download

    let finishBar: undefined | (() => void)
    let setProgress: undefined | ((sourcePath: string) => (progress: number) => void)

    if (opts.showProgress) {
      const collectiveBar = getCollectiveBar(opts)
      finishBar = collectiveBar.finishBar
      setProgress = collectiveBar.setProgress
    }

    const promises = binariesToDownload.map((job) => {
      const downloadUrl = getDownloadUrl({
        channel: 'all_commits',
        version: opts.version,
        binaryTarget: job.binaryTarget,
        binaryName: job.binaryName,
      })

      debug(`${downloadUrl} will be downloaded to ${job.targetFilePath}`)

      return downloadBinary({
        ...job,
        downloadUrl,
        version: opts.version,
        failSilent: opts.failSilent,
        progressCb: setProgress ? setProgress(job.targetFilePath) : undefined,
      })
    })

    await Promise.all(promises)

    await cleanupPromise // make sure, that cleanup finished
    if (finishBar) {
      finishBar()
    }
  }

  const binaryPaths = binaryJobsToBinaryPaths(binaryJobs)
  const dir = eval('__dirname')

  // this is necessary for pkg
  if (dir.match(vercelPkgPathRegex)) {
    for (const engineType in binaryPaths) {
      const binaryTargets = binaryPaths[engineType]
      for (const binaryTarget in binaryTargets) {
        const binaryPath = binaryTargets[binaryTarget]
        binaryTargets[binaryTarget] = await maybeCopyToTmp(binaryPath)
      }
    }
  }

  return binaryPaths
}

function getCollectiveBar(options: DownloadOptions): {
  finishBar: () => void
  setProgress: (sourcePath: string) => (progress: number) => void
} {
  const hasNodeAPI = 'libquery-engine' in options.binaries
  const bar = getBar(
    `Downloading Prisma engines${hasNodeAPI ? ' for Node-API' : ''} for ${options.binaryTargets
      ?.map((p) => bold(p))
      .join(' and ')}`,
  )

  const progressMap: { [key: string]: number } = {}
  // Object.values is faster than Object.keys
  const numDownloads = Object.values(options.binaries).length * Object.values(options?.binaryTargets ?? []).length
  const setProgress =
    (sourcePath: string) =>
    (progress): void => {
      progressMap[sourcePath] = progress
      const progressValues = Object.values(progressMap)
      const totalProgress =
        progressValues.reduce((acc, curr) => {
          return acc + curr
        }, 0) / numDownloads
      if (options.progressCb) {
        options.progressCb(totalProgress)
      }
      if (bar) {
        bar.update(totalProgress)
      }
    }

  return {
    setProgress,
    finishBar: (): void => {
      bar.update(1)
      bar.terminate()
    },
  }
}

function binaryJobsToBinaryPaths(jobs: BinaryDownloadJob[]): BinaryPaths {
  return jobs.reduce<BinaryPaths>((acc, job) => {
    if (!acc[job.binaryName]) {
      acc[job.binaryName] = {}
    }

    // if an env var path has been provided, prefer that one
    acc[job.binaryName][job.binaryTarget] = job.envVarPath || job.targetFilePath

    return acc
  }, {} as BinaryPaths)
}

async function binaryNeedsToBeDownloaded(
  job: BinaryDownloadJob,
  nativePlatform: string,
  version: string,
): Promise<boolean> {
  // If there is an ENV Override and the file exists then it does not need to be downloaded
  if (job.envVarPath && fs.existsSync(job.envVarPath)) {
    return false
  }
  // 1. Check if file exists
  const targetExists = await exists(job.targetFilePath)
  // 2. If exists, check, if cached file exists and is up to date and has same hash as file.
  // If not, copy cached file over
  const cachedFile = await getCachedBinaryPath({
    ...job,
    version,
  })

  if (cachedFile) {
    // for local development, when using `enginesOverride`
    // we don't have the sha256 hash, so we can't check it
    if (job.skipCacheIntegrityCheck === true) {
      await overwriteFile(cachedFile, job.targetFilePath)

      return false
    }

    const sha256FilePath = cachedFile + '.sha256'
    if (await exists(sha256FilePath)) {
      const sha256File = await fs.promises.readFile(sha256FilePath, 'utf-8')
      const sha256Cache = await getHash(cachedFile)
      if (sha256File === sha256Cache) {
        if (!targetExists) {
          debug(`copying ${cachedFile} to ${job.targetFilePath}`)

          // TODO Remove when https://github.com/docker/for-linux/issues/1015 is fixed
          // Workaround for https://github.com/prisma/prisma/issues/7037
          await fs.promises.utimes(cachedFile, new Date(), new Date())

          await overwriteFile(cachedFile, job.targetFilePath)
        }
        const targetSha256 = await getHash(job.targetFilePath)
        if (sha256File !== targetSha256) {
          debug(`overwriting ${job.targetFilePath} with ${cachedFile} as hashes do not match`)
          await overwriteFile(cachedFile, job.targetFilePath)
        }
        return false
      } else {
        return true
      }
    } else if (process.env.PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING) {
      // If the env var is truthy we do not error if the checksum file is missing
      debug(
        `The checksum file: ${sha256FilePath} is missing but this was ignored as the PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING environment variable is truthy.`,
      )
      return false
    } else {
      return true
    }
  }

  // If there is no cache and the file doesn't exist, we for sure need to download it
  if (!targetExists) {
    debug(`file ${job.targetFilePath} does not exist and must be downloaded`)

    return true
  }

  // 3. If same platform, check --version and compare to expected version
  if (job.binaryTarget === nativePlatform) {
    const currentVersion = await getVersion(job.targetFilePath, job.binaryName)

    if (currentVersion?.includes(version) !== true) {
      debug(`file ${job.targetFilePath} exists but its version is ${currentVersion} and we expect ${version}`)

      return true
    }
  }

  return false
}

export async function getVersion(enginePath: string, binaryName: string) {
  try {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
    if (binaryName === BinaryType.QueryEngineLibrary) {
      assertNodeAPISupported()

      const commitHash = require(enginePath).version().commit
      return `${BinaryType.QueryEngineLibrary} ${commitHash}`
    } else {
      const result = await execa(enginePath, ['--version'])

      return result.stdout
    }
  } catch {}

  return undefined
}

export function getBinaryName(binaryName: BinaryType, binaryTarget: BinaryTarget): string {
  if (binaryName === BinaryType.QueryEngineLibrary) {
    return `${getNodeAPIName(binaryTarget, 'fs')}`
  }
  const extension = binaryTarget === 'windows' ? '.exe' : ''
  return `${binaryName}-${binaryTarget}${extension}`
}

type GetCachedBinaryOptions = BinaryDownloadJob & {
  version: string
  failSilent?: boolean
}

async function getCachedBinaryPath({
  version,
  binaryTarget,
  binaryName,
}: GetCachedBinaryOptions): Promise<string | null> {
  const cacheDir = await getCacheDir(channel, version, binaryTarget)
  if (!cacheDir) {
    return null
  }

  const cachedTargetPath = path.join(cacheDir, binaryName)

  if (!fs.existsSync(cachedTargetPath)) {
    return null
  }

  // All versions not called 'latest' are unique
  // only latest needs more checks
  if (version !== 'latest') {
    return cachedTargetPath
  }

  if (await exists(cachedTargetPath)) {
    return cachedTargetPath
  }

  return null
}

type DownloadBinaryOptions = BinaryDownloadJob & {
  version: string
  downloadUrl: string
  progressCb?: (progress: number) => void
  failSilent?: boolean
}

async function downloadBinary(options: DownloadBinaryOptions): Promise<void> {
  const { version, progressCb, targetFilePath, downloadUrl } = options

  const targetDir = path.dirname(targetFilePath)

  try {
    fs.accessSync(targetDir, fs.constants.W_OK)
    await ensureDir(targetDir)
  } catch (e) {
    if (options.failSilent || (e as NodeJS.ErrnoException).code !== 'EACCES') {
      return
    } else {
      throw new Error(`Can't write to ${targetDir} please make sure you install "prisma" with the right permissions.`)
    }
  }

  debug(`Downloading ${downloadUrl} to ${targetFilePath} ...`)

  if (progressCb) {
    progressCb(0)
  }

  const { sha256, zippedSha256 } = await downloadZip(downloadUrl, targetFilePath, progressCb)
  if (progressCb) {
    progressCb(1)
  }

  chmodPlusX(targetFilePath)

  // Cache result
  await saveFileToCache(options, version, sha256, zippedSha256)
}

async function saveFileToCache(
  job: BinaryDownloadJob,
  version: string,
  sha256: string | null,
  zippedSha256: string | null,
): Promise<void> {
  // always fail silent, as the cache is optional
  const cacheDir = await getCacheDir(channel, version, job.binaryTarget)
  if (!cacheDir) {
    return
  }

  const cachedTargetPath = path.join(cacheDir, job.binaryName)
  const cachedSha256Path = path.join(cacheDir, job.binaryName + '.sha256')
  const cachedSha256ZippedPath = path.join(cacheDir, job.binaryName + '.gz.sha256')

  try {
    await overwriteFile(job.targetFilePath, cachedTargetPath)
    if (sha256 != null) {
      await fs.promises.writeFile(cachedSha256Path, sha256)
    }
    if (zippedSha256 != null) {
      await fs.promises.writeFile(cachedSha256ZippedPath, zippedSha256)
    }
  } catch (e) {
    debug(e)
    // let this fail silently - the CI system may have reached the file size limit
  }
}

export async function maybeCopyToTmp(file: string): Promise<string> {
  // in this case, we are in a "pkg" context with a virtual fs
  // to make this work, we need to copy the binary to /tmp and execute it from there

  const dir = eval('__dirname')
  if (dir.match(vercelPkgPathRegex)) {
    const targetDir = path.join(tempDir, 'prisma-binaries')
    await ensureDir(targetDir)
    const target = path.join(targetDir, path.basename(file))
    const data = await fs.promises.readFile(file)
    await fs.promises.writeFile(target, data)
    // We have to read and write until https://github.com/zeit/pkg/issues/639
    // is resolved
    // await copyFile(file, target)
    plusX(target)
    return target
  }

  return file
}

export function plusX(file): void {
  const s = fs.statSync(file)
  const newMode = s.mode | 64 | 8 | 1
  if (s.mode === newMode) {
    return
  }
  const base8 = newMode.toString(8).slice(-3)
  fs.chmodSync(file, base8)
}


import { jestConsoleContext, jestContext } from '@prisma/get-platform'
import * as miniProxy from '@prisma/mini-proxy'
import fs from 'fs'
import fetch from 'node-fetch'
import path from 'path'
import rimraf from 'rimraf'

import { DbPush } from '../../../../migrate/src/commands/DbPush'
import { Studio } from '../../Studio'

const originalEnv = { ...process.env }

const ctx = jestContext.new().add(jestConsoleContext()).assemble()

const STUDIO_TEST_PORT = 5678

const testIf = (condition: boolean) => (condition ? test : test.skip)
const describeIf = (condition: boolean) => (condition ? describe : describe.skip)

function sendRequest(message: any): Promise<any> {
  return fetch(`http://localhost:${STUDIO_TEST_PORT}/api`, {
    method: 'POST',
    headers: {
      'Content-Type': 'text/plain',
    },
    body: JSON.stringify(message),
  }).then((res) => res.json())
}

let studio: Studio
// Prisma Studio ignores env vars for overriding engine paths, skipping test for now
describeIf(!process.env.PRISMA_QUERY_ENGINE_LIBRARY && !process.env.PRISMA_QUERY_ENGINE_BINARY)(
  'studio with alternative urls and prisma://',
  () => {
    afterEach(() => {
      // Back to original env vars
      process.env = { ...originalEnv }
    })

    test('queries work if url is prisma:// and directUrl is set', async () => {
      process.env.PDP_URL = 'prisma://aws-us-east-1.prisma-data.com/?api_key=MY_API_KEY'
      process.env.DATABASE_URL = process.env.TEST_POSTGRES_URI!.replace('tests', `tests-${Date.now()}-studio`)

      ctx.fixture('schema-only-data-proxy-direct-url')

      const studio = Studio.new()

      await DbPush.new().parse(['--schema', 'schema.prisma', '--skip-generate'])
      const result = studio.parse(['--port', `${STUDIO_TEST_PORT}`, '--browser', 'none'])

      await expect(result).resolves.not.toThrow()

      const res = await sendRequest({
        requestId: 1,
        channel: 'prisma',
        action: 'clientRequest',
        payload: {
          data: {
            modelName: 'SomeUser',
            operation: 'findMany',
            args: {
              select: {
                id: true,
              },
            },
          },
        },
      })

      expect(res).toMatchSnapshot()

      studio.instance?.stop()
    })

    testIf(process.platform !== 'win32')('queries work if url is prisma:// via the mini-proxy', async () => {
      process.env.DATABASE_URL = process.env.TEST_POSTGRES_URI!.replace('tests', `tests-${Date.now()}-studio`)
      process.env.PDP_URL = miniProxy.generateConnectionString({
        envVar: 'PDP_URL',
        databaseUrl: process.env.DATABASE_URL,
        port: miniProxy.defaultServerConfig.port,
      })

      ctx.fixture('schema-only-data-proxy')

      await DbPush.new().parse(['--schema', 'schema.prisma', '--skip-generate'])
      delete process.env.DATABASE_URL

      const studio = Studio.new()
      const result = studio.parse(['--port', `${STUDIO_TEST_PORT}`, '--browser', 'none'])

      await expect(result).resolves.not.toThrow()

      const res = await sendRequest({
        requestId: 1,
        channel: 'prisma',
        action: 'clientRequest',
        payload: {
          data: {
            modelName: 'SomeUser',
            operation: 'findMany',
            args: {
              select: {
                id: true,
              },
            },
          },
        },
      })

      expect(res).toMatchSnapshot()

      studio.instance?.stop()

      expect(ctx.mocked['console.warn'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
    })
  },
)

describe('studio with default schema.prisma filename', () => {
  jest.setTimeout(20_000)

  beforeAll(async () => {
    // Before every test, we'd like to reset the DB.
    // We do this by duplicating the original SQLite DB file, and using the duplicate as the datasource in our schema
    rimraf.sync(path.join(__dirname, '../fixtures/studio-test-project/dev_tmp.db'))
    fs.copyFileSync(
      path.join(__dirname, '../fixtures/studio-test-project/dev.db'),
      path.join(__dirname, '../fixtures/studio-test-project/dev_tmp.db'),
    )

    // Clean up Client generation directory
    rimraf.sync(path.join(__dirname, '../prisma-client'))
    studio = Studio.new()

    await studio.parse([
      '--schema',
      path.join(__dirname, '../fixtures/studio-test-project/schema.prisma'),
      '--port',
      `${STUDIO_TEST_PORT}`,
      '--browser',
      'none',
    ])

    // Give Studio time to start
    await new Promise((r) => setTimeout(() => r(null), 2_000))
  })

  afterAll(() => {
    studio.instance!.stop()
  })

  test('can start up correctly', async () => {
    const res = await fetch(`http://localhost:${STUDIO_TEST_PORT}`)
    expect(res.status).toEqual(200)
  })

  test('can respond to `findMany` queries', async () => {
    const res = await sendRequest({
      requestId: 1,
      channel: 'prisma',
      action: 'clientRequest',
      payload: {
        data: {
          modelName: 'with_all_field_types',
          operation: 'findMany',
          args: {
            select: {
              id: true,
              string: true,
              int: true,
              float: true,
              datetime: true,
              relation: true,
              relation_list: true,
            },
          },
        },
      },
    })

    expect(res).toMatchSnapshot()
  })

  test('can respond to `create` queries', async () => {
    const res = await sendRequest({
      requestId: 2,
      channel: 'prisma',
      action: 'clientRequest',
      payload: {
        data: {
          modelName: 'with_all_field_types',
          operation: 'create',
          args: {
            data: {
              id: 3,
              string: '',
              int: 0,
              float: 0.0,
              datetime: '2020-08-03T00:00:00.000Z',
              relation: {
                connect: {
                  id: 3,
                },
              },
              relation_list: {
                connect: {
                  id: 3,
                },
              },
            },
            select: {
              id: true,
              string: true,
              int: true,
              float: true,
              datetime: true,
              relation: true,
              relation_list: true,
            },
          },
        },
      },
    })

    expect(res).toMatchSnapshot()
  })

  test('can respond to `update` queries', async () => {
    const res = await sendRequest({
      requestId: 3,
      channel: 'prisma',
      action: 'clientRequest',
      payload: {
        data: {
          modelName: 'with_all_field_types',
          operation: 'update',
          args: {
            where: {
              id: 1,
            },
            data: {
              string: 'Changed String',
              int: 100,
              float: 100.5,
              datetime: '2025-08-03T00:00:00.000Z',
              relation: {
                connect: {
                  id: 3,
                },
              },
              relation_list: {
                connect: {
                  id: 3,
                },
              },
            },
            select: {
              id: true,
              string: true,
              int: true,
              float: true,
              datetime: true,
              relation: true,
              relation_list: true,
            },
          },
        },
      },
    })

    expect(res).toMatchSnapshot()
  })

  test('can respond to `delete` queries', async () => {
    const res = await sendRequest({
      requestId: 4,
      channel: 'prisma',
      action: 'clientRequest',
      payload: {
        data: {
          modelName: 'with_all_field_types',
          operation: 'delete',
          args: {
            where: { id: 2 },
            select: {
              id: true,
              string: true,
              int: true,
              float: true,
              datetime: true,
              relation: true,
              relation_list: true,
            },
          },
        },
      },
    })
    expect(res).toMatchSnapshot()
  })
})

describe('studio with custom schema.prisma filename', () => {
  jest.setTimeout(20_000)

  beforeAll(async () => {
    // Before every test, we'd like to reset the DB.
    // We do this by duplicating the original SQLite DB file, and using the duplicate as the datasource in our schema
    rimraf.sync(path.join(__dirname, '../fixtures/studio-test-project-custom-filename/dev_tmp.db'))
    fs.copyFileSync(
      path.join(__dirname, '../fixtures/studio-test-project-custom-filename/dev.db'),
      path.join(__dirname, '../fixtures/studio-test-project-custom-filename/dev_tmp.db'),
    )

    // Clean up Client generation directory
    rimraf.sync(path.join(__dirname, '../prisma-client'))
    studio = Studio.new()

    await studio.parse([
      '--schema',
      path.join(__dirname, '../fixtures/studio-test-project-custom-filename/schema1.prisma'),
      '--port',
      `${STUDIO_TEST_PORT}`,
      '--browser',
      'none',
    ])

    // Give Studio time to start
    await new Promise((r) => setTimeout(() => r(null), 2_000))
  })

  afterAll(() => {
    studio.instance!.stop()
  })

  test('can start up correctly', async () => {
    const res = await fetch(`http://localhost:${STUDIO_TEST_PORT}`)
    expect(res.status).toEqual(200)
  })

  test('can respond to `findMany` queries', async () => {
    const res = await sendRequest({
      requestId: 1,
      channel: 'prisma',
      action: 'clientRequest',
      payload: {
        data: {
          modelName: 'with_all_field_types',
          operation: 'findMany',
          args: {
            select: {
              id: true,
              string: true,
              int: true,
              float: true,
              datetime: true,
              relation: true,
              relation_list: true,
            },
          },
        },
      },
    })

    expect(res).toMatchSnapshot()
  })

  test('can respond to `create` queries', async () => {
    const res = await sendRequest({
      requestId: 2,
      channel: 'prisma',
      action: 'clientRequest',
      payload: {
        data: {
          modelName: 'with_all_field_types',
          operation: 'create',
          args: {
            data: {
              id: 3,
              string: '',
              int: 0,
              float: 0.0,
              datetime: '2020-08-03T00:00:00.000Z',
              relation: {
                connect: {
                  id: 3,
                },
              },
              relation_list: {
                connect: {
                  id: 3,
                },
              },
            },
            select: {
              id: true,
              string: true,
              int: true,
              float: true,
              datetime: true,
              relation: true,
              relation_list: true,
            },
          },
        },
      },
    })

    expect(res).toMatchSnapshot()
  })

  test('can respond to `update` queries', async () => {
    const res = await sendRequest({
      requestId: 3,
      channel: 'prisma',
      action: 'clientRequest',
      payload: {
        data: {
          modelName: 'with_all_field_types',
          operation: 'update',
          args: {
            where: {
              id: 1,
            },
            data: {
              string: 'Changed String',
              int: 100,
              float: 100.5,
              datetime: '2025-08-03T00:00:00.000Z',
              relation: {
                connect: {
                  id: 3,
                },
              },
              relation_list: {
                connect: {
                  id: 3,
                },
              },
            },
            select: {
              id: true,
              string: true,
              int: true,
              float: true,
              datetime: true,
              relation: true,
              relation_list: true,
            },
          },
        },
      },
    })

    expect(res).toMatchSnapshot()
  })

  test('can respond to `delete` queries', async () => {
    const res = await sendRequest({
      requestId: 4,
      channel: 'prisma',
      action: 'clientRequest',
      payload: {
        data: {
          modelName: 'with_all_field_types',
          operation: 'delete',
          args: {
            where: { id: 2 },
            select: {
              id: true,
              string: true,
              int: true,
              float: true,
              datetime: true,
              relation: true,
              relation_list: true,
            },
          },
        },
      },
    })
    expect(res).toMatchSnapshot()
  })
})


generator client {
  provider = "prisma-client-js"
  output   = "../generated/client"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @default(autoincrement()) @id
  createdAt DateTime @default(now())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

model Profile {
  id     Int     @default(autoincrement()) @id
  bio    String?
  user   User    @relation(fields: [userId], references: [id])
  userId Int     @unique
}

model User {
  id      Int      @default(autoincrement()) @id
  email   String   @unique
  name    String?
  posts   Post[]
  profile Profile?
}


| 'freebsd11'
| 'freebsd12'
| 'freebsd13'
| 'freebsd14'
| 'openbsd'
| 'netbsd'
| 'arm'

import Debug from '@prisma/debug'
import cp from 'child_process'
import fs from 'fs/promises'
import os from 'os'
import { match } from 'ts-pattern'
import { promisify } from 'util'

import { BinaryTarget } from './binaryTargets'
import { link } from './link'
import { warn } from './logger'

const exec = promisify(cp.exec)

const debug = Debug('prisma:get-platform')

const supportedLibSSLVersions = ['1.0.x', '1.1.x', '3.0.x'] as const

// https://www.geeksforgeeks.org/node-js-process-arch-property/
export type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | 's390' | 's390x' | 'mipsel' | 'ia32' | 'mips' | 'ppc' | 'ppc64'
export type DistroInfo = {
  /**
   * The original distro is the Linux distro name detected via its release file.
   * E.g., on Arch Linux, the original distro is `arch`. On Linux Alpine, the original distro is `alpine`.
   */
  originalDistro?: string

  /**
   * The family distro is the Linux distro name that is used to determine Linux families based on the same base distro, and likely using the same package manager.
   * E.g., both Ubuntu and Debian belong to the `debian` family of distros, and thus rely on the same package manager (`apt`).
   */
  familyDistro?: string

  /**
   * The target distro is the Linux distro associated with the Prisma Engines.
   * E.g., on Arch Linux, Debian, and Ubuntu, the target distro is `debian`. On Linux Alpine, the target distro is `musl`.
   */
  targetDistro?: 'rhel' | 'debian' | 'musl' | 'arm' | 'nixos' | 'freebsd11' | 'freebsd12' | 'freebsd13' | 'freebsd14'
}
type GetOsResultLinux = {
  platform: 'linux'
  arch: Arch
  archFromUname: string | undefined
  /**
   * Starting from version 3.0, OpenSSL is basically adopting semver, and will be API and ABI compatible within a major version.
   */
  libssl?: (typeof supportedLibSSLVersions)[number]
} & DistroInfo

export type GetOSResult =
  | {
      platform: Omit<NodeJS.Platform, 'linux'>
      arch: Arch
      targetDistro?: DistroInfo['targetDistro']
      familyDistro?: never
      originalDistro?: never
      archFromUname?: never
      libssl?: never
    }
  | GetOsResultLinux

/**
 * For internal use only. This public export will be eventually removed in favor of `getPlatformWithOSResult`.
 */
export async function getos(): Promise<GetOSResult> {
  const platform = os.platform()
  const arch = process.arch as Arch
  if (platform === 'freebsd') {
    const version = await getCommandOutput(`freebsd-version`)
    if (version && version.trim().length > 0) {
      const regex = /^(\d+)\.?/
      const match = regex.exec(version)
      if (match) {
        return {
          platform: 'freebsd',
          targetDistro: `freebsd${match[1]}` as GetOSResult['targetDistro'],
          arch,
        }
      }
    }
  }

  if (platform !== 'linux') {
    return {
      platform,
      arch,
    }
  }

  const distroInfo = await resolveDistro()
  const archFromUname = await getArchFromUname()
  const libsslSpecificPaths = computeLibSSLSpecificPaths({ arch, archFromUname, familyDistro: distroInfo.familyDistro })
  const { libssl } = await getSSLVersion(libsslSpecificPaths)

  return {
    platform: 'linux',
    libssl,
    arch,
    archFromUname,
    ...distroInfo,
  }
}

export function parseDistro(osReleaseInput: string): DistroInfo {
  const idRegex = /^ID="?([^"\n]*)"?$/im
  const idLikeRegex = /^ID_LIKE="?([^"\n]*)"?$/im

  const idMatch = idRegex.exec(osReleaseInput)
  const id = (idMatch && idMatch[1] && idMatch[1].toLowerCase()) || ''

  const idLikeMatch = idLikeRegex.exec(osReleaseInput)
  const idLike = (idLikeMatch && idLikeMatch[1] && idLikeMatch[1].toLowerCase()) || ''

  /**
   * Example output of /etc/os-release:
   *
   * Alpine Linux => ID=alpine                                     => targetDistro=musl, familyDistro=alpine
   * Raspbian     => ID=raspbian, ID_LIKE=debian                   => targetDistro=arm, familyDistro=debian
   * Debian       => ID=debian                                     => targetDistro=debian, familyDistro=debian
   * Distroless   => ID=debian                                     => targetDistro=debian, familyDistro=debian
   * Ubuntu       => ID=ubuntu, ID_LIKE=debian                     => targetDistro=debian, familyDistro=debian
   * Arch Linux   => ID=arch                                       => targetDistro=debian, familyDistro=arch
   * Manjaro      => ID=manjaro, ID_LIKE=arch                      => targetDistro=debian, familyDistro=arch
   * Red Hat      => ID=rhel, ID_LIKE=fedora                       => targetDistro=rhel, familyDistro=rhel
   * Centos       => ID=centos, ID_LIKE=rhel fedora                => targetDistro=rhel, familyDistro=rhel
   * Alma Linux   => ID="almalinux", ID_LIKE="rhel centos fedora"  => targetDistro=rhel, familyDistro=rhel
   * Fedora       => ID=fedora                                     => targetDistro=rhel, familyDistro=rhel
   */
  const distroInfo = match({ id, idLike })
    .with(
      { id: 'alpine' },
      ({ id: originalDistro }) =>
        ({
          targetDistro: 'musl',
          familyDistro: originalDistro,
          originalDistro,
        } as const),
    )
    .with(
      { id: 'raspbian' },
      ({ id: originalDistro }) =>
        ({
          targetDistro: 'arm',
          familyDistro: 'debian',
          originalDistro,
        } as const),
    )
    .with(
      { id: 'nixos' },
      ({ id: originalDistro }) =>
        ({
          targetDistro: 'nixos',
          originalDistro,
          familyDistro: 'nixos',
        } as const),
    )
    .with(
      { id: 'debian' },
      { id: 'ubuntu' },
      ({ id: originalDistro }) =>
        ({
          targetDistro: 'debian',
          familyDistro: 'debian',
          originalDistro,
        } as const),
    )
    .with(
      { id: 'rhel' },
      { id: 'centos' },
      { id: 'fedora' },
      ({ id: originalDistro }) =>
        ({
          targetDistro: 'rhel',
          familyDistro: 'rhel',
          originalDistro,
        } as const),
    )
    .when(
      ({ idLike }) => idLike.includes('debian') || idLike.includes('ubuntu'),
      ({ id: originalDistro }) =>
        ({
          targetDistro: 'debian',
          familyDistro: 'debian',
          originalDistro,
        } as const),
    )
    .when(
      ({ idLike }) => id === 'arch' || idLike.includes('arch'),
      ({ id: originalDistro }) =>
        ({
          targetDistro: 'debian',
          familyDistro: 'arch',
          originalDistro,
        } as const),
    )
    .when(
      ({ idLike }) =>
        idLike.includes('centos') || idLike.includes('fedora') || idLike.includes('rhel') || idLike.includes('suse'),
      ({ id: originalDistro }) =>
        ({
          targetDistro: 'rhel',
          familyDistro: 'rhel',
          originalDistro,
        } as const),
    )
    .otherwise(({ id: originalDistro }) => {
      /* Generic distro info fallback */
      return {
        targetDistro: undefined,
        familyDistro: undefined,
        originalDistro,
      } as const
    })

  debug(`Found distro info:\n${JSON.stringify(distroInfo, null, 2)}`)
  return distroInfo
}

export async function resolveDistro(): Promise<DistroInfo> {
  // https://github.com/retrohacker/getos/blob/master/os.json

  const osReleaseFile = '/etc/os-release'
  try {
    const osReleaseInput = await fs.readFile(osReleaseFile, { encoding: 'utf-8' })
    return parseDistro(osReleaseInput)
  } catch (_) {
    return {
      targetDistro: undefined,
      familyDistro: undefined,
      originalDistro: undefined,
    }
  }
}

/**
 * Parse the OpenSSL version from the output of the openssl binary, e.g.
 * "OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)" -> "3.0.x"
 */
export function parseOpenSSLVersion(input: string): GetOsResultLinux['libssl'] | undefined {
  const match = /^OpenSSL\s(\d+\.\d+)\.\d+/.exec(input)
  if (match) {
    const partialVersion = `${match[1]}.x`
    return sanitiseSSLVersion(partialVersion)
  }

  return undefined
}

/**
 * Parse the OpenSSL version from the output of the libssl.so file, e.g.
 * "libssl.so.3" -> "3.0.x"
 */
export function parseLibSSLVersion(input: string): GetOsResultLinux['libssl'] {
  const match = /libssl\.so\.(\d)(\.\d)?/.exec(input)
  if (match) {
    const partialVersion = `${match[1]}${match[2] ?? '.0'}.x`
    return sanitiseSSLVersion(partialVersion)
  }

  return undefined
}

function sanitiseSSLVersion(version: string): GetOsResultLinux['libssl'] {
  const sanitisedVersion = (() => {
    if (isLibssl1x(version)) {
      return version
    }

    /**
     * Sanitise OpenSSL 3+. E.g., '3.1.x' becomes '3.0.x'
     */
    const versionSplit = version.split('.')
    versionSplit[1] = '0'
    return versionSplit.join('.') as NonNullable<GetOsResultLinux['libssl']>
  })()

  /* Validate that we've parsed a libssl version we actually support */
  if (supportedLibSSLVersions.includes(sanitisedVersion)) {
    return sanitisedVersion
  }

  return undefined
}

type ComputeLibSSLSpecificPathsParams = {
  arch: Arch
  archFromUname: Awaited<ReturnType<typeof getArchFromUname>>
  familyDistro: DistroInfo['familyDistro']
}

export function computeLibSSLSpecificPaths(args: ComputeLibSSLSpecificPathsParams) {
  return match(args)
    .with({ familyDistro: 'musl' }, () => {
      /* Linux Alpine */
      debug('Trying platform-specific paths for "alpine"')
      return ['/lib']
    })
    .with({ familyDistro: 'debian' }, ({ archFromUname }) => {
      /* Linux Debian, Ubuntu, etc */
      debug('Trying platform-specific paths for "debian" (and "ubuntu")')
      return [`/usr/lib/${archFromUname}-linux-gnu`, `/lib/${archFromUname}-linux-gnu`]
    })
    .with({ familyDistro: 'rhel' }, () => {
      /* Linux Red Hat, OpenSuse etc */
      debug('Trying platform-specific paths for "rhel"')
      return ['/lib64', '/usr/lib64']
    })
    .otherwise(({ familyDistro, arch, archFromUname }) => {
      /* Other Linux distros, we don't do anything specific and fall back to the next blocks */
      debug(`Don't know any platform-specific paths for "${familyDistro}" on ${arch} (${archFromUname})`)
      return []
    })
}

type GetOpenSSLVersionResult =
  | {
      libssl: GetOsResultLinux['libssl']
      strategy: 'libssl-specific-path' | 'ldconfig' | 'openssl-binary'
    }
  | {
      libssl?: never
      strategy?: never
    }

/**
 * On Linux, returns the libssl version excluding the patch version, e.g. "1.1.x".
 * Reading the version from the libssl.so file is more reliable than reading it from the openssl binary.
 * Older versions of libssl are preferred, e.g. "1.0.x" over "1.1.x", because of Vercel serverless
 * having different build and runtime environments, with the runtime environment having an old version
 * of libssl, and the build environment having both that old version and a newer version of libssl installed.
 * Because of https://github.com/prisma/prisma/issues/17499, we explicitly filter out libssl 0.x.
 *
 * This function never throws.
 */
export async function getSSLVersion(libsslSpecificPaths: string[]): Promise<GetOpenSSLVersionResult> {
  const excludeLibssl0x = 'grep -v "libssl.so.0"'
  const libsslFilenameFromSpecificPath: string | undefined = await findLibSSLInLocations(libsslSpecificPaths)

  if (libsslFilenameFromSpecificPath) {
    debug(`Found libssl.so file using platform-specific paths: ${libsslFilenameFromSpecificPath}`)
    const libsslVersion = parseLibSSLVersion(libsslFilenameFromSpecificPath)
    debug(`The parsed libssl version is: ${libsslVersion}`)
    if (libsslVersion) {
      return { libssl: libsslVersion, strategy: 'libssl-specific-path' }
    }
  }

  debug('Falling back to "ldconfig" and other generic paths')
  let libsslFilename: string | undefined = await getCommandOutput(
    /**
     * The `ldconfig -p` returns the dynamic linker cache paths, where libssl.so files are likely to be included.
     * Each line looks like this:
     * 	libssl.so (libc6,hard-float) => /usr/lib/arm-linux-gnueabihf/libssl.so.1.1
     * But we're only interested in the filename, so we use sed to remove everything before the `=>` separator,
     * and then we remove the path and keep only the filename.
     * The second sed commands uses `|` as a separator because the paths may contain `/`, which would result in the
     * `unknown option to 's'` error (see https://stackoverflow.com/a/9366940/6174476) - which would silently
     * fail with error code 0.
     */
    `ldconfig -p | sed "s/.*=>s*//" | sed "s|.*/||" | grep libssl | sort | ${excludeLibssl0x}`,
  )

  if (!libsslFilename) {
    /**
     * Fall back to the rhel-specific paths (although `familyDistro` isn't detected as rhel) when the `ldconfig` command fails.
     */
    libsslFilename = await findLibSSLInLocations(['/lib64', '/usr/lib64', '/lib'])
  }

  if (libsslFilename) {
    debug(`Found libssl.so file using "ldconfig" or other generic paths: ${libsslFilename}`)
    const libsslVersion = parseLibSSLVersion(libsslFilename)
    debug(`The parsed libssl version is: ${libsslVersion}`)
    if (libsslVersion) {
      return { libssl: libsslVersion, strategy: 'ldconfig' }
    }
  }

  /* Reading the libssl.so version didn't work, fall back to openssl */

  const openSSLVersionLine: string | undefined = await getCommandOutput('openssl version -v')

  if (openSSLVersionLine) {
    debug(`Found openssl binary with version: ${openSSLVersionLine}`)
    const openSSLVersion = parseOpenSSLVersion(openSSLVersionLine)
    debug(`The parsed openssl version is: ${openSSLVersion}`)
    if (openSSLVersion) {
      return { libssl: openSSLVersion, strategy: 'openssl-binary' }
    }
  }

  /* Reading openssl didn't work */
  debug(`Couldn't find any version of libssl or OpenSSL in the system`)
  return {}
}

/**
 * Looks for libssl in specified directories, returns the first one found
 * @param directories
 * @returns
 */
async function findLibSSLInLocations(directories: string[]) {
  for (const dir of directories) {
    const libssl = await findLibSSL(dir)
    if (libssl) {
      return libssl
    }
  }
  return undefined
}

/**
 * Looks for libssl in specific directory
 * @param directory
 * @returns
 */
async function findLibSSL(directory: string) {
  try {
    const dirContents = await fs.readdir(directory)
    return dirContents.find((value) => value.startsWith('libssl.so.') && !value.startsWith('libssl.so.0'))
  } catch (e) {
    if (e.code === 'ENOENT') {
      return undefined
    }
    throw e
  }
}

/**
 * Get the binary target for the current platform, e.g. `linux-musl-arm64-openssl-3.0.x` for Linux Alpine on arm64.
 */
export async function getBinaryTargetForCurrentPlatform(): Promise<BinaryTarget> {
  const { binaryTarget } = await getPlatformInfoMemoized()
  return binaryTarget
}

export type PlatformInfo = GetOSResult & { binaryTarget: BinaryTarget }

function isPlatformInfoDefined(args: Partial<PlatformInfo>): args is PlatformInfo {
  return args.binaryTarget !== undefined
}

/**
 * Get the binary target and other system information (e.g., the libssl version to look for) for the current platform.
 */
export async function getPlatformInfo(): Promise<PlatformInfo> {
  const { memoized: _, ...rest } = await getPlatformInfoMemoized()
  return rest
}

let memoizedPlatformWithInfo: Partial<PlatformInfo> = {}

export async function getPlatformInfoMemoized(): Promise<PlatformInfo & { memoized: boolean }> {
  if (isPlatformInfoDefined(memoizedPlatformWithInfo)) {
    return Promise.resolve({ ...memoizedPlatformWithInfo, memoized: true })
  }

  const args = await getos()
  const binaryTarget = getBinaryTargetForCurrentPlatformInternal(args)
  memoizedPlatformWithInfo = { ...args, binaryTarget }
  return { ...(memoizedPlatformWithInfo as PlatformInfo), memoized: false }
}

/**
 * This function is only exported for testing purposes.
 */
export function getBinaryTargetForCurrentPlatformInternal(args: GetOSResult): BinaryTarget {
  const { platform, arch, archFromUname, libssl, targetDistro, familyDistro, originalDistro } = args

  if (platform === 'linux' && !['x64', 'arm64'].includes(arch)) {
    warn(
      `Prisma only officially supports Linux on amd64 (x86_64) and arm64 (aarch64) system architectures. If you are using your own custom Prisma engines, you can ignore this warning, as long as you've compiled the engines for your system architecture "${archFromUname}".`,
    )
  }

  // sometimes we fail to detect the libssl version to use, so we default to 1.1.x
  const defaultLibssl = '1.1.x' as const
  if (platform === 'linux' && libssl === undefined) {
    /**
     * Ask the user to install libssl manually, and provide some additional instructions based on the detected Linux distro family.
     * TODO: we should also provide a pris.ly link to a documentation page with more details on how to install libssl.
     */
    const additionalMessage = match({ familyDistro })
      .with({ familyDistro: 'debian' }, () => {
        return "Please manually install OpenSSL via `apt-get update -y && apt-get install -y openssl` and try installing Prisma again. If you're running Prisma on Docker, add this command to your Dockerfile, or switch to an image that already has OpenSSL installed."
      })
      .otherwise(() => {
        return 'Please manually install OpenSSL and try installing Prisma again.'
      })

    warn(
      `Prisma failed to detect the libssl/openssl version to use, and may not work as expected. Defaulting to "openssl-${defaultLibssl}".
${additionalMessage}`,
    )
  }

  // sometimes we fail to detect the distro in use, so we default to debian
  const defaultDistro = 'debian' as const
  if (platform === 'linux' && targetDistro === undefined) {
    warn(
      `Prisma doesn't know which engines to download for the Linux distro "${originalDistro}". Falling back to Prisma engines built "${defaultDistro}".
Please report your experience by creating an issue at ${link(
        'https://github.com/prisma/prisma/issues',
      )} so we can add your distro to the list of known supported distros.`,
    )
  }

  // Apple Silicon (M1)
  if (platform === 'darwin' && arch === 'arm64') {
    return 'darwin-arm64'
  }

  if (platform === 'darwin') {
    return 'darwin'
  }

  if (platform === 'win32') {
    return 'windows'
  }

  if (platform === 'freebsd') {
    return targetDistro as BinaryTarget
  }

  if (platform === 'openbsd') {
    return 'openbsd'
  }

  if (platform === 'netbsd') {
    return 'netbsd'
  }

  if (platform === 'linux' && targetDistro === 'nixos') {
    return 'linux-nixos'
  }

  if (platform === 'linux' && arch === 'arm64') {
    // 64 bit ARM (musl or glibc)
    const baseName = targetDistro === 'musl' ? 'linux-musl-arm64' : 'linux-arm64'
    return `${baseName}-openssl-${libssl || defaultLibssl}` as BinaryTarget
  }

  if (platform === 'linux' && arch === 'arm') {
    // 32 bit ARM
    return `linux-arm-openssl-${libssl || defaultLibssl}` as BinaryTarget
  }

  if (platform === 'linux' && targetDistro === 'musl') {
    const base = 'linux-musl'
    if (!libssl) {
      return base
    }

    if (isLibssl1x(libssl)) {
      // Alpine 3.16 or below linked with OpenSSL 1.1
      return base
    } else {
      // Alpine 3.17 or above linked with OpenSSL 3.0
      return `${base}-openssl-${libssl}`
    }
  }

  // when the platform is linux
  if (platform === 'linux' && targetDistro && libssl) {
    return `${targetDistro}-openssl-${libssl}` as BinaryTarget
  }

  if (platform !== 'linux') {
    warn(`Prisma detected unknown OS "${platform}" and may not work as expected. Defaulting to "linux".`)
  }

  // if just OpenSSL is known, fallback to debian with a specific libssl version
  if (libssl) {
    return `${defaultDistro}-openssl-${libssl}`
  }

  // if just the targetDistro is known, fallback to latest OpenSSL 1.1
  if (targetDistro) {
    return `${targetDistro}-openssl-${defaultLibssl}` as BinaryTarget
  }

  // use the debian build with OpenSSL 1.1 as a last resort
  // TODO: perhaps we should default to 'debian-openssl-3.0.x'
  return `${defaultDistro}-openssl-${defaultLibssl}`
}

/**
 * Given a promise generator, returns the promise's result.
 * If the promise throws, returns undefined.
 */
async function discardError<T>(runPromise: () => Promise<T>): Promise<T | undefined> {
  try {
    return await runPromise()
  } catch (e) {
    return undefined
  }
}

/**
 * Executes system command and returns its output. If command fails, returns undefined
 */
function getCommandOutput(command: string) {
  return discardError(async () => {
    const result = await exec(command)

    debug(`Command "${command}" successfully returned "${result.stdout}"`)
    return result.stdout
  })
}

/**
 * Returns the architecture of a system from the output of `uname -m` (whose format is different than `process.arch`).
 * This function never throws.
 * TODO: deprecate this function in favor of `os.machine()` once either Node v16.18.0 or v18.9.0 becomes the minimum
 * supported Node.js version for Prisma.
 */
export async function getArchFromUname(): Promise<string | undefined> {
  if (typeof os['machine'] === 'function') {
    return os['machine']()
  }
  const arch = await getCommandOutput('uname -m')
  return arch?.trim()
}

// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
function isLibssl1x(libssl: NonNullable<GetOSResult['libssl']> | string): libssl is '1.0.x' | '1.1.x' {
  return libssl.startsWith('1.')
}