diff --git a/.eslintrc.js b/.eslintrc.js
index d0903916f59..f55b74b0a17 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -56,7 +56,12 @@ module.exports = {
allowExpressions: true,
},
],
- '@typescript-eslint/explicit-module-boundary-types': ['error'],
+ '@typescript-eslint/explicit-module-boundary-types': [
+ 'error',
+ {
+ allowArgumentsExplicitlyTypedAsAny: true,
+ },
+ ],
},
},
{
diff --git a/frontend/src/lib/components/Annotations/AnnotationMarker.js b/frontend/src/lib/components/Annotations/AnnotationMarker.js
index 375d8b6c633..b801784d252 100644
--- a/frontend/src/lib/components/Annotations/AnnotationMarker.js
+++ b/frontend/src/lib/components/Annotations/AnnotationMarker.js
@@ -4,7 +4,6 @@ import { userLogic } from 'scenes/userLogic'
import { Button, Popover, Row, Input, Checkbox, Tooltip } from 'antd'
import { humanFriendlyDetailedTime } from '~/lib/utils'
import { DeleteOutlined, PlusOutlined, GlobalOutlined, CloseOutlined } from '@ant-design/icons'
-import _ from 'lodash'
import { annotationsLogic } from './annotationsLogic'
import moment from 'moment'
import { useEscapeKey } from 'lib/hooks/useEscapeKey'
@@ -138,37 +137,41 @@ export function AnnotationMarker({
) : (
- {_.orderBy(annotations, ['created_at'], ['asc']).map((data) => (
-
-
-
-
- {data.created_by === 'local'
- ? name || email
- : data.created_by &&
- (data.created_by.first_name || data.created_by.email)}
-
-
- {humanFriendlyDetailedTime(data.created_at)}
-
- {data.scope !== 'dashboard_item' && (
-
-
-
+ {[...annotations]
+ .sort((annotationA, annotationB) => annotationA.created_at - annotationB.created_at)
+ .map((data) => (
+
+
+
+
+ {data.created_by === 'local'
+ ? name || email
+ : data.created_by &&
+ (data.created_by.first_name || data.created_by.email)}
+
+
+ {humanFriendlyDetailedTime(data.created_at)}
+
+ {data.scope !== 'dashboard_item' && (
+
+
+
+ )}
+
+ {(!data.created_by ||
+ data.created_by.id === id ||
+ data.created_by === 'local') && (
+ {
+ onDelete(data)
+ }}
+ >
)}
-
- {(!data.created_by || data.created_by.id === id || data.created_by === 'local') && (
-
{
- onDelete(data)
- }}
- >
- )}
-
-
{data.content}
-
- ))}
+
+
{data.content}
+
+ ))}
{textAreaVisible && (
+ )
+}
-export const SceneLoading = () => (
-
-
-
-)
+export function TableRowLoading({
+ colSpan = 1,
+ asOverlay = false,
+}: {
+ colSpan: number
+ asOverlay: boolean
+}): JSX.Element {
+ return (
+
+
+
+ |
+
+ )
+}
-export let CloseButton = (props) => {
+export function SceneLoading(): JSX.Element {
+ return (
+
+
+
+ )
+}
+
+export function CloseButton(props: Record): JSX.Element {
return (
×
@@ -83,7 +98,7 @@ export let CloseButton = (props) => {
)
}
-export function Card(props) {
+export function Card(props: Record): JSX.Element {
return (
{
+export function deleteWithUndo({ undo = false, ...props }: Record
): void {
api.update('api/' + props.endpoint + '/' + props.object.id, {
...props.object,
deleted: !undo,
}).then(() => {
props.callback?.()
- let response = (
+ const response = (
{props.object.name ?? 'Untitled'}
{!undo ? ' deleted. Click here to undo.' : ' deletion undone.'}
@@ -118,7 +133,17 @@ export const deleteWithUndo = ({ undo = false, ...props }) => {
})
}
-export const DeleteWithUndo = (props) => {
+export function DeleteWithUndo(
+ props: PropsWithChildren<{
+ endpoint: string
+ object: {
+ name: string
+ id: number
+ }
+ className: string
+ style: CSSProperties
+ }>
+): JSX.Element {
const { className, style, children } = props
return (
{
)
}
-DeleteWithUndo.propTypes = {
- endpoint: PropTypes.string.isRequired,
- object: PropTypes.shape({
- name: PropTypes.string.isRequired,
- id: PropTypes.number.isRequired,
- }).isRequired,
- className: PropTypes.string,
- style: PropTypes.object,
-}
-export let selectStyle = {
+export const selectStyle: Record) => Partial> = {
control: (base) => ({
...base,
height: 31,
@@ -172,27 +188,26 @@ export let selectStyle = {
}),
}
-export let debounce = (func, wait, immediate) => {
- var timeout
+export function debounce(func: (...args: any) => void, wait: number, immediate: boolean, ...args: any): () => void {
+ let timeout: number | undefined
return function () {
- var context = this, // eslint-disable-line
- args = arguments
- var later = function () {
- timeout = null
+ const context = this // eslint-disable-line
+ function later(): void {
+ timeout = undefined
if (!immediate) func.apply(context, args)
}
- var callNow = immediate && !timeout
+ const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
-export const capitalizeFirstLetter = (string) => {
+export function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1)
}
-export const operatorMap = {
+export const operatorMap: Record = {
exact: '= equals',
is_not: "≠ doesn't equal",
icontains: '∋ contains',
@@ -205,12 +220,16 @@ export const operatorMap = {
is_not_set: '✕ is not set',
}
-export function isOperatorFlag(operator) {
+export function isOperatorFlag(operator: string): boolean {
// these filter operators can only be just set, no additional parameter
return ['is_set', 'is_not_set'].includes(operator)
}
-export function formatPropertyLabel(item, cohorts, keyMapping) {
+export function formatPropertyLabel(
+ item: Record,
+ cohorts: Record[],
+ keyMapping: Record>
+): string {
const { value, key, operator, type } = item
return type === 'cohort'
? cohorts?.find((cohort) => cohort.id === value)?.name || value
@@ -220,88 +239,92 @@ export function formatPropertyLabel(item, cohorts, keyMapping) {
: ` ${(operatorMap[operator || 'exact'] || '?').split(' ')[0]} ${value || ''}`)
}
-export const formatProperty = (property) => {
+export function formatProperty(property: Record): string {
return property.key + ` ${operatorMap[property.operator || 'exact'].split(' ')[0]} ` + property.value
}
// Format a label that gets returned from the /insights api
-export const formatLabel = (label, action) => {
- let math = 'Total'
+export function formatLabel(label: string, action: Record): string {
+ const math = 'Total'
if (action.math === 'dau') label += ` (${action.math.toUpperCase()}) `
else label += ` (${math}) `
- if (action && action.properties && !_.isEmpty(action.properties)) {
+ if (action?.properties?.length) {
label += ` (${action.properties
.map((property) => operatorMap[property.operator || 'exact'].split(' ')[0] + ' ' + property.value)
.join(', ')})`
}
-
return label
}
-export const deletePersonData = (person, callback) => {
- window.confirm('Are you sure you want to delete this user? This cannot be undone') &&
+export function deletePersonData(person: Record, callback: () => void): void {
+ if (window.confirm('Are you sure you want to delete this user? This cannot be undone')) {
api.delete('api/person/' + person.id).then(() => {
toast('Person succesfully deleted.')
if (callback) callback()
})
+ }
}
-export const savePersonData = (person) => {
+export function savePersonData(person: Record): void {
api.update('api/person/' + person.id, person).then(() => {
toast('Person Updated')
})
}
-export const objectsEqual = (obj1, obj2) => JSON.stringify(obj1) === JSON.stringify(obj2)
+export function objectsEqual(obj1: any, obj2: any): boolean {
+ return JSON.stringify(obj1) === JSON.stringify(obj2)
+}
-export const idToKey = (array, keyField = 'id') => {
- const object = {}
+export function idToKey(array: Record[], keyField: string = 'id'): any {
+ const object: Record = {}
for (const element of array) {
object[element[keyField]] = element
}
return object
}
-export const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms))
+export function delay(ms: number): Promise {
+ return new Promise((resolve) => window.setTimeout(resolve, ms))
+}
// Trigger a window.reisize event a few times 0...2 sec after the menu was collapsed/expanded
// We need this so the dashboard resizes itself properly, as the available div width will still
// change when the sidebar's expansion is animating.
-export const triggerResize = () => {
+export function triggerResize(): void {
try {
window.dispatchEvent(new Event('resize'))
} catch (error) {
// will break on IE11
}
}
-export const triggerResizeAfterADelay = () => {
+export function triggerResizeAfterADelay(): void {
for (const delay of [10, 100, 500, 750, 1000, 2000]) {
window.setTimeout(triggerResize, delay)
}
}
-export function clearDOMTextSelection() {
+export function clearDOMTextSelection(): void {
if (window.getSelection) {
- if (window.getSelection().empty) {
+ if (window.getSelection()?.empty) {
// Chrome
- window.getSelection().empty()
- } else if (window.getSelection().removeAllRanges) {
+ window.getSelection()?.empty()
+ } else if (window.getSelection()?.removeAllRanges) {
// Firefox
- window.getSelection().removeAllRanges()
+ window.getSelection()?.removeAllRanges()
}
- } else if (document.selection) {
+ } else if ((document as any).selection) {
// IE?
- document.selection.empty()
+ ;(document as any).selection.empty()
}
}
export const posthogEvents = ['$autocapture', '$pageview', '$identify', '$pageleave']
-export function isAndroidOrIOS() {
+export function isAndroidOrIOS(): boolean {
return typeof window !== 'undefined' && /Android|iPhone|iPad|iPod/i.test(window.navigator.userAgent)
}
-export function slugify(text) {
+export function slugify(text: string): string {
return text
.toString() // Cast to string
.toLowerCase() // Convert the string to lowercase letters
@@ -312,26 +335,26 @@ export function slugify(text) {
.replace(/--+/g, '-')
}
-export function humanFriendlyDuration(d) {
+export function humanFriendlyDuration(d: string | number): string {
d = Number(d)
- var days = Math.floor(d / 86400)
- var h = Math.floor((d % 86400) / 3600)
- var m = Math.floor((d % 3600) / 60)
- var s = Math.floor((d % 3600) % 60)
+ const days = Math.floor(d / 86400)
+ const h = Math.floor((d % 86400) / 3600)
+ const m = Math.floor((d % 3600) / 60)
+ const s = Math.floor((d % 3600) % 60)
- var dayDisplay = days > 0 ? days + 'd ' : ''
- var hDisplay = h > 0 ? h + (h == 1 ? 'hr ' : 'hrs ') : ''
- var mDisplay = m > 0 ? m + (m == 1 ? 'min ' : 'mins ') : ''
- var sDisplay = s > 0 ? s + 's' : hDisplay || mDisplay ? '' : '0s'
+ const dayDisplay = days > 0 ? days + 'd ' : ''
+ const hDisplay = h > 0 ? h + (h == 1 ? 'hr ' : 'hrs ') : ''
+ const mDisplay = m > 0 ? m + (m == 1 ? 'min ' : 'mins ') : ''
+ const sDisplay = s > 0 ? s + 's' : hDisplay || mDisplay ? '' : '0s'
return days > 0 ? dayDisplay + hDisplay : hDisplay + mDisplay + sDisplay
}
-export function humanFriendlyDiff(from, to) {
+export function humanFriendlyDiff(from: moment.MomentInput, to: moment.MomentInput): string {
const diff = moment(to).diff(moment(from), 'seconds')
return humanFriendlyDuration(diff)
}
-export function humanFriendlyDetailedTime(date, withSeconds = false) {
+export function humanFriendlyDetailedTime(date: moment.MomentInput, withSeconds: boolean = false): string {
let formatString = 'MMMM Do YYYY h:mm'
const today = moment().startOf('day')
const yesterday = today.clone().subtract(1, 'days').startOf('day')
@@ -345,21 +368,21 @@ export function humanFriendlyDetailedTime(date, withSeconds = false) {
return moment(date).format(formatString)
}
-export function stripHTTP(url) {
+export function stripHTTP(url: string): string {
url = url.replace(/(^[0-9]+_)/, '')
url = url.replace(/(^\w+:|^)\/\//, '')
return url
}
-export function isURL(string) {
+export function isURL(string: string): boolean {
if (!string) return false
// https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
- var expression = /^\s*https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi
- var regex = new RegExp(expression)
- return string.match && string.match(regex)
+ const expression = /^\s*https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi
+ const regex = new RegExp(expression)
+ return !!string.match(regex)
}
-export const eventToName = (event) => {
+export function eventToName(event: EventType): string {
if (event.event !== '$autocapture') return event.event
let name = ''
if (event.properties.$event_type === 'click') name += 'clicked '
@@ -379,7 +402,10 @@ export const eventToName = (event) => {
return name
}
-export function determineDifferenceType(firstDate, secondDate) {
+export function determineDifferenceType(
+ firstDate: moment.MomentInput,
+ secondDate: moment.MomentInput
+): 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' {
const first = moment(firstDate)
const second = moment(secondDate)
if (first.diff(second, 'years') !== 0) return 'year'
@@ -390,7 +416,7 @@ export function determineDifferenceType(firstDate, secondDate) {
else return 'minute'
}
-export const dateMapping = {
+export const dateMapping: Record = {
Today: ['dStart'],
Yesterday: ['-1d', 'dStart'],
'Last 24 hours': ['-24h'],
@@ -407,18 +433,21 @@ export const dateMapping = {
export const isDate = /([0-9]{4}-[0-9]{2}-[0-9]{2})/
-export function dateFilterToText(date_from, date_to) {
- if (isDate.test(date_from)) return `${date_from} - ${date_to}`
- if (moment.isMoment(date_from)) return `${date_from.format('YYYY-MM-DD')} - ${date_to.format('YYYY-MM-DD')}`
- if (date_from === 'dStart') return 'Today' // Changed to "last 24 hours" but this is backwards compatibility
+export function dateFilterToText(dateFrom: string | moment.Moment, dateTo: string | moment.Moment): string {
+ if (moment.isMoment(dateFrom) && moment.isMoment(dateTo))
+ return `${dateFrom.format('YYYY-MM-DD')} - ${dateTo.format('YYYY-MM-DD')}`
+ dateFrom = dateFrom as string
+ dateTo = dateTo as string
+ if (isDate.test(dateFrom) && isDate.test(dateTo)) return `${dateFrom} - ${dateTo}`
+ if (dateFrom === 'dStart') return 'Today' // Changed to "last 24 hours" but this is backwards compatibility
let name = 'Last 7 days'
Object.entries(dateMapping).map(([key, value]) => {
- if (value[0] === date_from && value[1] === date_to) name = key
+ if (value[0] === dateFrom && value[1] === dateTo) name = key
})[0]
return name
}
-export function humanizeNumber(number, digits = 1) {
+export function humanizeNumber(number: number, digits: number = 1): string {
// adapted from https://stackoverflow.com/a/9462382/624476
let matchingPrefix = SI_PREFIXES[SI_PREFIXES.length - 1]
for (const currentPrefix of SI_PREFIXES) {
@@ -430,7 +459,7 @@ export function humanizeNumber(number, digits = 1) {
return (number / matchingPrefix.value).toFixed(digits).replace(TRAILING_ZERO_REGEX, '$1') + matchingPrefix.symbol
}
-export function copyToClipboard(value, description) {
+export function copyToClipboard(value: string, description?: string): boolean {
const descriptionAdjusted = description ? description.trim() + ' ' : ''
try {
navigator.clipboard.writeText(value)
@@ -442,18 +471,54 @@ export function copyToClipboard(value, description) {
}
}
-export function clamp(value, min, max) {
+export function clamp(value: number, min: number, max: number): number {
return value > max ? max : value < min ? min : value
}
-export function isMobile() {
+export function isMobile(): boolean {
return navigator.userAgent.includes('Mobile')
}
-export function isMac() {
+export function isMac(): boolean {
return navigator.platform.includes('Mac')
}
-export function platformCommandControlKey() {
+export function platformCommandControlKey(): string {
return isMac() ? '⌘' : 'Ctrl'
}
+
+export function groupBy(items: T[], groupResolver: (item: T) => string | number): Record {
+ const itemsGrouped: Record = {}
+ for (const item of items) {
+ const group = groupResolver(item)
+ if (!(group in itemsGrouped)) itemsGrouped[group] = [] // Ensure there's an array to push to
+ itemsGrouped[group].push(item)
+ }
+ return itemsGrouped
+}
+
+export function uniqueBy(items: T[], uniqueResolver: (item: T) => any): T[] {
+ const uniqueKeysSoFar = new Set()
+ const itemsUnique: T[] = []
+ for (const item of items) {
+ const uniqueKey = uniqueResolver(item)
+ if (!uniqueKeysSoFar.has(uniqueKeysSoFar)) {
+ uniqueKeysSoFar.add(uniqueKey)
+ itemsUnique.push(item)
+ }
+ }
+ return itemsUnique
+}
+
+export function sample(items: T[], size: number): T[] {
+ if (size > items.length) throw Error('Sample size cannot exceed items array length!')
+ const results: T[] = []
+ const internalItems = [...items]
+ if (size === items.length) return internalItems
+ for (let i = 0; i < size; i++) {
+ const index = Math.floor(Math.random() * internalItems.length)
+ results.push(internalItems[index])
+ internalItems.splice(index, 1)
+ }
+ return results
+}
diff --git a/frontend/src/scenes/users/Cohort.js b/frontend/src/scenes/users/Cohort.js
index d170f4b3ea9..8012913ffc3 100644
--- a/frontend/src/scenes/users/Cohort.js
+++ b/frontend/src/scenes/users/Cohort.js
@@ -6,10 +6,9 @@ import { Button } from 'antd'
import { useValues, useActions } from 'kea'
import { router } from 'kea-router'
-import _ from 'lodash'
const isSubmitDisabled = (cohorts) => {
- if (cohorts && cohorts.groups) return !cohorts.groups.some((group) => !_.isEmpty(group))
+ if (cohorts && cohorts.groups) return !cohorts.groups.some((group) => Object.keys(group).length)
return true
}
diff --git a/frontend/src/scenes/users/CohortGroup.js b/frontend/src/scenes/users/CohortGroup.js
index fb22ae2a241..b437a61d641 100644
--- a/frontend/src/scenes/users/CohortGroup.js
+++ b/frontend/src/scenes/users/CohortGroup.js
@@ -5,7 +5,6 @@ import { Select } from 'antd'
import { actionsModel } from '~/models/actionsModel'
import { useValues } from 'kea'
-import _ from 'lodash'
function DayChoice({ days, name, group, onChange }) {
return (
@@ -74,16 +73,16 @@ export function CohortGroup({ onChange, onRemove, group, index }) {
endpoint="person"
pageKey={'cohort_' + index}
className=" "
- onChange={(properties) =>
+ onChange={(properties) => {
onChange(
- !_.isEmpty(properties)
+ properties.length
? {
properties: properties,
days: group.days,
}
: {}
)
- }
+ }}
propertyFilters={group.properties || {}}
style={{ margin: '1rem 0 0' }}
/>
diff --git a/latest_migrations.manifest b/latest_migrations.manifest
index 64ab05890a9..94f81319fdd 100644
--- a/latest_migrations.manifest
+++ b/latest_migrations.manifest
@@ -2,7 +2,7 @@ admin: 0003_logentry_add_action_flag_choices
auth: 0011_update_proxy_permissions
contenttypes: 0002_remove_content_type_name
ee: 0002_hook
-posthog: 0086_team_session_recording_opt_in
+posthog: 0087_fix_annotation_created_at
rest_hooks: 0002_swappable_hook_model
sessions: 0001_initial
social_django: 0008_partial_timestamp
diff --git a/package.json b/package.json
index 337573c8108..f541ec7e2bf 100644
--- a/package.json
+++ b/package.json
@@ -29,9 +29,6 @@
"@babel/runtime": "^7.10.4",
"@mariusandra/query-selector-shadow-dom": "0.7.2-posthog.2",
"@mariusandra/simmerjs": "0.7.1-posthog.1",
- "@types/lodash": "^4.14.162",
- "@types/react-syntax-highlighter": "^11.0.4",
- "@types/zxcvbn": "^4.4.0",
"antd": "^4.1.1",
"babel-preset-nano-react-app": "^0.1.0",
"bootstrap": "^4.4.1",
@@ -49,7 +46,6 @@
"kea-localstorage": "^1.0.2",
"kea-router": "^0.4.0",
"kea-window-values": "^0.0.1",
- "lodash": "^4.17.20",
"moment": "^2.24.0",
"posthog-js": "1.5.0-beta.0",
"posthog-js-lite": "^0.0.3",
@@ -80,6 +76,8 @@
"@hot-loader/react-dom": "^16.13.0",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
+ "@types/react-syntax-highlighter": "^11.0.4",
+ "@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^3.6.0",
"@typescript-eslint/parser": "^3.6.0",
"autoprefixer": "^9.7.4",
diff --git a/posthog/migrations/0087_fix_annotation_created_at.py b/posthog/migrations/0087_fix_annotation_created_at.py
new file mode 100644
index 00000000000..0c4483174d1
--- /dev/null
+++ b/posthog/migrations/0087_fix_annotation_created_at.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.7 on 2020-10-14 07:46
+
+import django.utils.timezone
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("posthog", "0086_team_session_recording_opt_in"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="annotation",
+ name="created_at",
+ field=models.DateTimeField(default=django.utils.timezone.now, null=True),
+ ),
+ ]
diff --git a/posthog/models/annotation.py b/posthog/models/annotation.py
index 107158c8bd8..aa55b118816 100644
--- a/posthog/models/annotation.py
+++ b/posthog/models/annotation.py
@@ -1,4 +1,5 @@
from django.db import models
+from django.utils import timezone
class Annotation(models.Model):
@@ -12,7 +13,7 @@ class Annotation(models.Model):
GITHUB = "GIT", "GitHub"
content: models.CharField = models.CharField(max_length=400, null=True, blank=True)
- created_at: models.DateTimeField = models.DateTimeField(null=True, blank=True)
+ created_at: models.DateTimeField = models.DateTimeField(default=timezone.now, null=True)
updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)
dashboard_item: models.ForeignKey = models.ForeignKey(
"posthog.DashboardItem", on_delete=models.SET_NULL, null=True, blank=True
diff --git a/yarn.lock b/yarn.lock
index 34fcb247d46..6e16ae78b0c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1099,11 +1099,6 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
-"@types/lodash@^4.14.162":
- version "4.14.162"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.162.tgz#65d78c397e0d883f44afbf1f7ba9867022411470"
- integrity sha512-alvcho1kRUnnD1Gcl4J+hK0eencvzq9rmzvFPRmP5rPHx9VVsJj6bKLTATPVf9ktgv4ujzh7T+XWKp+jhuODig==
-
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"