Merge branch 'develop' into issue-13807

This commit is contained in:
Ibrahem Khalil
2022-08-27 09:40:53 +02:00
committed by GitHub
29 changed files with 330 additions and 219 deletions
+1 -6
View File
@@ -298,23 +298,18 @@ shadow-server:##@ Start shadow-cljs in server mode for watching
yarn shadow-cljs server
test-watch: export TARGET := clojure
test-watch: status-go-library
test-watch: ##@ Watch tests and re-run no changes to cljs files
yarn install
nodemon --exec 'yarn shadow-cljs compile mocks && yarn shadow-cljs compile test && node --require ./test-resources/override.js target/test/test.js' -e cljs
test: export TARGET := clojure
test: status-go-library ##@test Run tests once in NodeJS
test: ##@test Run tests once in NodeJS
# Here we creates the gyp bindings for nodejs
yarn install
yarn shadow-cljs compile mocks && \
yarn shadow-cljs compile test && \
node --require ./test-resources/override.js target/test/test.js
coverage: ##@test Run tests once in NodeJS generating coverage
@scripts/run-coverage.sh
#--------------
# Other
#--------------
+26 -62
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.5.0'
library 'status-jenkins-lib@v1.5.1'
/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
@@ -56,43 +56,14 @@ pipeline {
}
}
}
stage('Parallel Assemble') {
parallel {
stage('Checks') { stages {
stage('Lint') {
steps {
script {
/* We want the build result to be uploaded */
catchError(
message: 'Linting check failed!',
buildResult: 'FAILURE',
stageResult: 'FAILURE'
) {
sh 'make lint'
}
}
}
}
stage('Tests') {
steps {
script {
sh 'make test'
}
}
}
} }
stage('Build') { stages {
stage('Bundle') {
steps {
script { apks = android.bundle() }
}
}
stage('Sign') {
steps {
script { apks = android.sign(apks) }
}
}
} }
stage('Bundle') {
steps {
script { apks = android.bundle() }
}
}
stage('Sign') {
steps {
script { apks = android.sign(apks) }
}
}
stage('Parallel Upload') {
@@ -103,37 +74,30 @@ pipeline {
} }
}
stage('Upload') {
steps {
script {
def urls = apks.collect { s3.uploadArtifact(it) }
/* return only the universal APK */
if (urls.size() > 1) {
env.PKG_URL = urls.find { it.contains('universal') }
} else { /* if no universal is available pick first */
env.PKG_URL = urls.first()
}
jenkins.setBuildDesc(APK: env.PKG_URL)
/* e2e builds get tested in SauceLabs */
if (utils.isE2EBuild()) {
env.SAUCE_URL = android.uploadToSauceLabs()
}
if (utils.isNightlyBuild()) {
env.DIAWI_URL = android.uploadToDiawi()
}
steps { script {
def urls = apks.collect { s3.uploadArtifact(it) }
/* return only the universal APK */
if (urls.size() > 1) {
env.PKG_URL = urls.find { it.contains('universal') }
} else { /* if no universal is available pick first */
env.PKG_URL = urls.first()
}
}
jenkins.setBuildDesc(APK: env.PKG_URL)
/* e2e builds get tested in SauceLabs */
if (utils.isE2EBuild()) {
env.SAUCE_URL = android.uploadToSauceLabs()
}
if (utils.isNightlyBuild()) {
env.DIAWI_URL = android.uploadToDiawi()
}
} }
}
}
}
stage('Cleanup') {
steps {
sh 'make purge'
}
}
}
post {
success { script { github.notifyPR(true) } }
failure { script { github.notifyPR(false) } }
always { sh 'make _fix-node-perms' }
always { sh 'make purge' }
}
}
+4 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.5.0'
library 'status-jenkins-lib@v1.5.1'
pipeline {
agent { label 'linux' }
@@ -49,6 +49,9 @@ pipeline {
stage('Android e2e') { steps { script {
apke2e = jenkins.Build('status-mobile/platforms/android-e2e')
} } }
stage('Tests') { steps { script {
ios = jenkins.Build('status-mobile/platforms/tests')
} } }
}
}
stage('Archive') {
+19 -56
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.5.0'
library 'status-jenkins-lib@v1.5.1'
/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
@@ -55,43 +55,14 @@ pipeline {
}
}
}
stage('Parallel Assemble') {
parallel {
stage('Checks') { stages {
stage('Lint') {
steps {
script {
/* We want the build result to be uploaded */
catchError(
message: 'Linting check failed!',
buildResult: 'FAILURE',
stageResult: 'FAILURE'
) {
sh 'make lint'
}
}
}
}
stage('Tests') {
steps {
script {
sh 'make test'
}
}
}
} }
stage('Build') { stages {
stage('JSBundle') {
steps {
script { ios.jsbundle() }
}
}
stage('Bundle') {
steps {
script { api = ios.bundle() }
}
}
} }
stage('JSBundle') {
steps {
script { ios.jsbundle() }
}
}
stage('Bundle') {
steps {
script { api = ios.bundle() }
}
}
stage('Parallel Upload') {
@@ -102,31 +73,23 @@ pipeline {
}
}
stage('Upload') {
steps {
script {
env.PKG_URL = s3.uploadArtifact(api)
jenkins.setBuildDesc(IPA: env.PKG_URL)
/* e2e builds get tested in SauceLabs */
if (utils.isE2EBuild()) {
env.SAUCE_URL = ios.uploadToSauceLabs()
} else {
env.DIAWI_URL = ios.uploadToDiawi()
}
steps { script {
env.PKG_URL = s3.uploadArtifact(api)
jenkins.setBuildDesc(IPA: env.PKG_URL)
/* e2e builds get tested in SauceLabs */
if (utils.isE2EBuild()) {
env.SAUCE_URL = ios.uploadToSauceLabs()
} else {
env.DIAWI_URL = ios.uploadToDiawi()
}
}
} }
}
}
}
stage('Cleanup') {
steps {
sh 'make watchman-clean'
sh 'make purge'
}
}
}
post {
success { script { github.notifyPR(true) } }
failure { script { github.notifyPR(false) } }
always { sh 'make _fix-node-perms' }
always { sh 'make purge' }
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.5.0'
library 'status-jenkins-lib@v1.5.1'
pipeline {
agent { label params.AGENT_LABEL }
+77
View File
@@ -0,0 +1,77 @@
library 'status-jenkins-lib@v1.5.0'
/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
pipeline {
agent { label 'linux && x86_64 && nix-2.8' }
options {
timestamps()
/* Prevent Jenkins jobs from running forever */
timeout(time: 10, unit: 'MINUTES')
/* Limit builds retained */
buildDiscarder(logRotator(
numToKeepStr: '10',
daysToKeepStr: '20',
artifactNumToKeepStr: '10',
))
/* Abort old PR builds. */
disableConcurrentBuilds(
abortPrevious: isPRBuild
)
}
parameters {
string(
name: 'BUILD_TYPE',
description: 'Specify build type. Values: pr / e2e / nightly / release',
defaultValue: 'pr',
)
}
environment {
LANG = "en_US.UTF-8"
LC_ALL = "en_US.UTF-8"
LANGUAGE = "en_US.UTF-8"
TARGET = 'tests'
BUILD_ENV = 'prod'
NIX_CONF_DIR = "${env.WORKSPACE}/nix"
LOG_FILE = utils.pkgFilename(ext: 'log', arch: 'tests')
}
stages {
stage('Prep') {
steps {
script {
btype = utils.getBuildType()
print "Running ${btype} build!"
/* Cleanup and Prep */
commonPrep(btype)
}
}
}
stage('Checks') {
parallel {
stage('Lint') {
steps { sh "make lint > ${LOG_FILE}" }
}
stage('Tests') {
steps { sh "make test >> ${LOG_FILE}" }
}
}
}
stage('Upload') {
steps {
script {
env.PKG_URL = s3.uploadArtifact(LOG_FILE)
}
}
}
}
post {
success { script { github.notifyPR(true) } }
failure { script { github.notifyPR(false) } }
always { sh 'make purge' }
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.5.0'
library 'status-jenkins-lib@v1.5.1'
pipeline {
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.5.0'
library 'status-jenkins-lib@v1.5.1'
pipeline {
agent { label 'macos' }
+1 -1
View File
@@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.5.0'
library 'status-jenkins-lib@v1.5.1'
pipeline {
agent { label 'linux' }
+4 -1
View File
@@ -12,7 +12,10 @@
abi-include = "armeabi-v7a;arm64-v8a;x86"; # Android architectures to build for
};
status-go = { src-override = null; };
status-go = {
src-override = null;
ipfs-gateway-url = "https://ipfs.status.im/";
};
};
# Android SDK requires an accepted license
+3 -1
View File
@@ -1,7 +1,7 @@
{ lib, callPackage, mkShell }:
let
inherit (lib) attrValues mapAttrs;
inherit (lib) getConfig attrValues mapAttrs;
# Metadata common to all builds of status-go
meta = {
@@ -17,6 +17,8 @@ let
goBuildParams = {
GitCommit = source.rev;
Version = source.cleanVersion;
# FIXME: This should be moved to status-go config.
IpfsGatewayURL = getConfig "status-go.ipfs-gateway-url" "https://ipfs.status.im/";
};
# These are necessary for status-go to show correct version
-1
View File
@@ -83,7 +83,6 @@
"@babel/register": "7.0.0",
"@mapbox/node-pre-gyp": "^1.0.9",
"clj-kondo": "^2020.1.13",
"coveralls": "^3.0.4",
"jest": "^25.1.0",
"nodemon": "^2.0.16",
"nyc": "^14.1.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env bash
# Fail on first error
set -e
TEST_JS=target/test/test.js
COVERAGE_REPORT=coverage-report/lcov.info
rm -rf "${COVERAGE_REPORT}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
if [ ! -f "$TEST_JS" ]; then
echo -e "${YELLOW}test suite not found, compiling and running...${NC}"
make test
else
echo -e "${YELLOW}test suite found.${NC}"
fi
echo -e "${YELLOW}running test suite with coverage...${NC}"
nyc node target/test/test.js
echo -e "${GREEN}DONE.${NC}"
if [ ! -f "$COVERAGE_REPORT" ]; then
echo -e "${RED}coverage report not generated! check nyc configuration file .nycrc${NC}"
exit 1
fi
## COVERALLS ENV ##
# COVERALLS_REPO_TOKEN (the secret repo token from coveralls.io)
# optionals ENV
# COVERALLS_SERVICE_NAME (the name of your build system, "jenkins"
# COVERALLS_SERVICE_JOB_ID
if [ -z "$COVERALLS_REPO_TOKEN" ]; then
echo -e "${RED}COVERALLS_REPO_TOKEN not set!${NC}"
exit 1
fi
echo -e "${YELLOW}uploading coverage to coveralls...${NC}"
cat coverage-report/lcov.info | coveralls
echo -e "${GREEN}DONE.${NC}"
+51
View File
@@ -0,0 +1,51 @@
(ns quo2.components.tag
(:require [quo.react-native :as rn]
[quo2.foundations.colors :as colors]
[quo.theme :as theme]
[quo2.components.text :as text]))
(def themes {:light {:background-color colors/neutral-20}
:dark {:background-color colors/neutral-80}})
(defn get-value-from-size [size big-option small-option]
(if (= size :big) big-option small-option))
(defn tag-container [size]
{:height (get-value-from-size size 32 24)
:align-items :center
:flex-direction :row
:border-radius 20})
(defn tag "[tag opts \"label\"]]
opts
{
:size :small/:big
:token-img-src :token-img-src
:token-img-style {}
:border-color :color
:overlay child-elements
}"
[_ _]
(fn [{:keys [size token-img-src token-img-style border-color overlay]
:or {size :small}} label]
[rn/view
{:style (when border-color
{:border-color border-color
:border-radius 20
:border-width 1})}
[rn/view
{:style (merge (tag-container size) (get themes (theme/get-theme)))}
[rn/image
{:source token-img-src
:style (merge
{:height (get-value-from-size size 28 20)
:width (get-value-from-size size 28 20)
:margin-left 2
:margin-right (get-value-from-size size 8 6)} token-img-style)}]
[text/text
{:weight :medium
:number-of-lines 1
:style
{:margin-right (get-value-from-size size 12 11)}
:size (get-value-from-size size :paragraph-2 :label)} label]
overlay]]))
+55
View File
@@ -0,0 +1,55 @@
(ns quo2.components.token-tag
(:require [quo2.foundations.colors :as colors]
[quo.react-native :as rn]
[quo.theme :as theme]
[status-im.ui.components.icons.icons :as icons]
[quo2.components.tag :as tag]))
(defn get-value-from-size [size big-option small-option]
(if (= size :big) big-option small-option))
(def icon-container-styles
{:display :flex
:align-items :center
:justify-content :center
:position :absolute
:border-radius 20
:margin-left 2})
(defn token-tag
"[token-tag opts]
opts
{
:token string
:value string
:size :small/:big
:token-img-src :token-img-src
:border-color :color
:is-required true/false
:is-purchasable true/false
}"
[_ _]
(fn [{:keys [token value size token-img-src border-color is-required is-purchasable]
:or
{size :small border-color (if (= (theme/get-theme) :dark) colors/purple-60 colors/purple-50)}}]
[tag/tag {:size size
:token-img-src token-img-src
:border-color (when is-required border-color)
:overlay
(when (or is-required is-purchasable)
[rn/view
{:style (merge icon-container-styles
{:width 15.5
:height 15.5
:background-color border-color
:border-color (if (= (theme/get-theme) :dark) colors/black colors/white)
:border-width 1
:right (get-value-from-size size -3.75 -5.75)
:bottom (get-value-from-size size (- 32 7.75 4) (- 24 7.75 2)) ; (- height (icon-height/2) spacing)
})}
[icons/icon (if is-required :main-icons2/required-checkmark12 :main-icons2/purchasable12)
{:no-color true
:width 13.5
:height 13.5}]])}
(str value " " token)]))
+4
View File
@@ -11,6 +11,7 @@
[quo2.screens.context-tags :as context-tags]
[quo2.screens.group-avatar :as group-avatar]
[quo2.screens.activity-logs :as activity-logs]
[quo2.screens.token-tag :as token-tag]
[quo2.screens.counter :as counter]
[quo2.screens.segmented :as segmented]
[quo2.screens.info-message :as info-message]
@@ -42,6 +43,9 @@
{:name :quo2-tabs
:insets {:top false}
:component tabs/preview-tabs}
{:name :quo2-token-tag
:insets {:top false}
:component token-tag/preview-token-tag}
{:name :quo2-segmented
:insets {:top false}
:component segmented/preview-segmented}
+69
View File
@@ -0,0 +1,69 @@
(ns quo2.screens.token-tag
(:require [quo.react-native :as rn]
[quo.previews.preview :as preview]
[reagent.core :as reagent]
[quo2.components.token-tag :as quo2]
[quo.design-system.colors :as colors]))
(def descriptor [{:label "Size:"
:key :size
:type :select
:options [{:key :big
:value "big"}
{:key :small
:value "small"}]}
{:label "Value:"
:key :value
:type :select
:options [{:key 0
:value "0"}
{:key 10
:value "10"}
{:key 100
:value "100"}
{:key 1000
:value "1000"}
{:key 10000
:value "10000"}]}
{:label "Community Color:"
:key :border-color
:type :select
:options [{:key "#00a191"
:value "green"}
{:key "red"
:value "red"}]}
{:label "Is Required:"
:key :is-required
:type :boolean}
{:label "Is Purchasable:"
:key :is-purchasable
:type :boolean}
{:label "Token:"
:key :token
:type :select
:options [{:key "ETH"
:value "ETH"}
{:key "SNT"
:value "SNT"}]}])
(def eth-token (js/require "../resources/images/tokens/mainnet/ETH.png"))
(def snt-token (js/require "../resources/images/tokens/mainnet/SNT.png"))
(defn cool-preview []
(let [state (reagent/atom {:size :big :value 10 :token "ETH" :is-required true :is-purchasable false})]
(fn []
[rn/view {:margin-bottom 50
:padding 16}
[preview/customizer state descriptor]
[rn/view {:padding-vertical 60
:align-items :center}
[quo2/token-tag (merge @state {:token-img-src (if (= (get-in @state [:token]) "ETH") eth-token snt-token)})]]])))
(defn preview-token-tag []
[rn/view {:background-color (:ui-background @colors/theme)
:flex 1}
[rn/flat-list {:flex 1
:keyboardShouldPersistTaps :always
:header [cool-preview]
:key-fn str}]])
+3 -3
View File
@@ -50,7 +50,7 @@
{:events [:browser.ui/remove-browser-pressed]}
[{:keys [db]} browser-id]
{:db (update-in db [:browser/browsers] dissoc browser-id)
::json-rpc/call [{:method "browsers_deleteBrowser"
::json-rpc/call [{:method "wakuext_deleteBrowser"
:params [browser-id]
:on-success #()}]})
@@ -59,7 +59,7 @@
[{:keys [db]}]
{:db (dissoc db :browser/browsers)
::json-rpc/call (for [browser-id (keys (get db :browser/browsers))]
{:method "browsers_deleteBrowser"
{:method "wakuext_deleteBrowser"
:params [browser-id]
:on-success #()})})
@@ -96,7 +96,7 @@
{:db (update-in db
[:browser/browsers browser-id]
merge updated-browser)
::json-rpc/call [{:method "browsers_addBrowser"
::json-rpc/call [{:method "wakuext_addBrowser"
:params [(select-keys updated-browser [:browser-id :timestamp :name :dapp? :history :history-index])]
:on-success #()}]}))
+1 -1
View File
@@ -131,7 +131,7 @@
(update :keycard dissoc :flow)
(dissoc :restored-account?))}
(multiaccounts.create/prepare-intro-wizard)
(navigation/navigate-to-cofx :get-your-keys nil)))
(navigation/set-stack-root :onboarding [:get-your-keys])))
(fx/defn recovery-no-key
{:events [:keycard.recovery.no-key.ui/generate-key-pressed]}
+1 -1
View File
@@ -328,7 +328,7 @@
(fx/defn initialize-browser [_]
{::json-rpc/call
[{:method "browsers_getBrowsers"
[{:method "wakuext_getBrowsers"
:on-success #(re-frame/dispatch [::initialize-browsers %])}
{:method "browsers_getBookmarks"
:on-success #(re-frame/dispatch [::initialize-bookmarks %])}
+3 -3
View File
@@ -3,7 +3,7 @@
"_comment": "Instead use: scripts/update-status-go.sh <rev>",
"owner": "status-im",
"repo": "status-go",
"version": "v0.106.2",
"commit-sha1": "f43f43cc30e56d342cf8d25d2e0e56aecc83fa6e",
"src-sha256": "0j9xgkblv56h3vvkiwsy3wnwazs0fcs4z3f3mvlvs058vpdkv9hw"
"version": "v0.107.0",
"commit-sha1": "86054875a4f0296ed38858553448dfe92c14da9c",
"src-sha256": "1mw6v20h20g1zy30qs97ys2dv5nggzvdbfmdfygwvf3fj018yklg"
}
@@ -129,6 +129,7 @@ class TestWalletManagementDeviceMerged(MultipleSharedDeviceTestCase):
self.sign_in.just_fyi("Check that 0 asset is not disappearing after relogin")
self.wallet.reopen_app()
self.home.continue_syncing_button.click_if_shown(5)
self.sign_in.wallet_button.click()
self.wallet.asset_by_name(asset).scroll_to_element()
@@ -906,10 +906,9 @@ class TestChatManagement(SingleDeviceTestCase):
profile.close_button.click()
if not profile.element_by_text(h_node).is_element_displayed():
self.errors.append('"%s" history node is not pinned' % h_node)
profile.home_button.click()
profile.just_fyi('Relogin and check that settings are preserved')
home.relogin()
home.reopen_app()
home.profile_button.click()
profile.sync_settings_button.click()
if not profile.element_by_text(h_node).is_element_displayed():
+3 -5
View File
@@ -538,16 +538,14 @@ class BaseView(object):
def get_back_to_home_view(self, times_to_click_on_back_btn=3):
counter = 0
while self.back_button.is_element_displayed(2) or self.close_button.is_element_displayed(2) or self.navigate_up_button.is_element_displayed(2):
while self.back_button.is_element_displayed(2) or self.navigate_up_button.is_element_displayed(2):
try:
if counter >= times_to_click_on_back_btn:
break
if self.back_button.is_element_displayed(2):
self.back_button.click_until_presence_of_element(self.home_button)
elif self.close_button.is_element_displayed(2):
self.close_button.click_until_presence_of_element(self.home_button)
self.back_button.click_until_absense_of_element(self.back_button)
else:
self.navigate_up_button.click_until_presence_of_element(self.home_button)
self.navigate_up_button.click_until_absense_of_element(self.navigate_up_button)
counter += 1
except (NoSuchElementException, TimeoutException):
continue
-21
View File
@@ -3370,17 +3370,6 @@ cosmiconfig@^5.0.5, cosmiconfig@^5.1.0:
js-yaml "^3.13.1"
parse-json "^4.0.0"
coveralls@^3.0.4:
version "3.0.13"
resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-3.0.13.tgz#64d8c99af3b8a771a2e6e5bcdf556c3c5061d29d"
integrity sha512-Bch6FJI4ebK6Mwr+DjFCJbb/RayNwn5pscSDE4Ux6cgjNkAcjdgGZBRfQnuYTt5tY81MqGK8m2r+xc5FDF513A==
dependencies:
js-yaml "^3.13.1"
lcov-parse "^1.0.0"
log-driver "^1.2.7"
minimist "^1.2.5"
request "^2.88.0"
cp-file@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d"
@@ -6058,11 +6047,6 @@ latest-version@^5.1.0:
dependencies:
package-json "^6.3.0"
lcov-parse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0"
integrity sha1-6w1GtUER68VhrLTECO+TY73I9+A=
leven@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
@@ -6169,11 +6153,6 @@ lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
log-driver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8"
integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==
log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"