├── .gitignore ├── nbb.edn ├── .github └── workflows │ └── ci.yml ├── src └── logseq │ ├── graph_validator │ ├── state.cljs │ ├── config.cljs │ ├── validations │ │ ├── class.cljs │ │ └── property.cljs │ └── default_validations.cljs │ └── graph_validator.cljs ├── graph_validator.mjs ├── package.json ├── LICENSE.md ├── CHANGELOG.md ├── action.yml ├── README.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.nbb 3 | -------------------------------------------------------------------------------- /nbb.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {logseq/graph-parser 3 | {:git/url "https://github.com/logseq/logseq" 4 | :git/sha "65640ed8857199d89268295863cd556f5d90ea0b" 5 | :deps/root "deps/graph-parser"} 6 | #_{:local/root "../logseq/deps/graph-parser"}}} 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | name: Run graph tests 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v4 10 | 11 | # TODO: Test a real graph 12 | - name: Run graph-validator tests 13 | uses: logseq/graph-validator@main 14 | -------------------------------------------------------------------------------- /src/logseq/graph_validator/state.cljs: -------------------------------------------------------------------------------- 1 | (ns logseq.graph-validator.state 2 | "State that is globally accessible to all tests") 3 | 4 | (def db-conn "Datascript conn return from parsed graph" nil) 5 | (def all-asts "Vec of ast maps returned from a parsed graph" (atom nil)) 6 | (def graph-dir "Directory of given graph" (atom nil)) 7 | (def config "Graph's current config" (atom nil)) -------------------------------------------------------------------------------- /src/logseq/graph_validator/config.cljs: -------------------------------------------------------------------------------- 1 | (ns logseq.graph-validator.config 2 | "Graph-specific config to define for validator") 3 | 4 | (def default-config 5 | "Provides defaults for config and lists all available config keys" 6 | {;; Additional namespaces to run under .graph-validator/ 7 | :add-namespaces [] 8 | ;; Exclude individual tests 9 | :exclude []}) 10 | -------------------------------------------------------------------------------- /graph_validator.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { loadFile, addClassPath } from '@logseq/nbb-logseq' 4 | import { fileURLToPath } from 'url'; 5 | import { dirname, resolve } from 'path'; 6 | 7 | const __dirname = fileURLToPath(dirname(import.meta.url)); 8 | addClassPath(resolve(__dirname, 'src')); 9 | const { main } = await loadFile(resolve(__dirname, 'src/logseq/graph_validator.cljs')); 10 | 11 | // Expects to be called as node X.js ... 12 | const args = process.argv.slice(2) 13 | main.apply(null, args); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@logseq/graph-validator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "bin": { 6 | "logseq-graph-validator": "graph_validator.mjs" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/logseq/graph-validator.git" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/logseq/graph-validator/issues" 17 | }, 18 | "homepage": "https://github.com/logseq/graph-validator#readme", 19 | "dependencies": { 20 | "@logseq/nbb-logseq": "logseq/nbb-logseq#feat-db-v24", 21 | "mldoc": "^1.5.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Logseq 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/logseq/graph_validator/validations/class.cljs: -------------------------------------------------------------------------------- 1 | (ns logseq.graph-validator.validations.class 2 | "Validations related to managing classes" 3 | (:require [clojure.test :refer [deftest is]] 4 | [logseq.graph-validator.state :as state] 5 | [logseq.db.file-based.rules :as file-rules] 6 | [datascript.core :as d])) 7 | 8 | (defn- get-classes [] 9 | (->> (d/q 10 | '[:find (pull ?b [*]) 11 | :in $ % 12 | :where (page-property ?b :type "Class")] 13 | @state/db-conn 14 | [(:page-property file-rules/query-dsl-rules)]) 15 | (map first) 16 | (map #(assoc (:block/properties %) :block/title (:block/title %))))) 17 | 18 | (deftest classes-have-parents 19 | (is (empty? (remove #(or (some? (:parent %)) (= "Thing" (:block/title %))) 20 | (get-classes))) 21 | "All classes have parent property except Thing")) 22 | 23 | (deftest classes-have-urls 24 | (is (empty? (remove :url (get-classes))) 25 | "All classes have urls")) 26 | 27 | (deftest classes-are-capitalized 28 | (is (empty? (remove #(re-find #"^[A-Z]" (:block/title %)) (get-classes))) 29 | "All classes start with a capital letter")) 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | * Use nbb-logseq and latest logseq deps that supports db-version 4 | 5 | ## 0.5.0 6 | * Add optional class validations 7 | * Add optional property validations 8 | * Fixes for `assets-exist-and-are-used` validation: 9 | * asset subdirectories caused a false negative 10 | * pdf highlights caused a false negative 11 | * Fix `tags-and-page-refs-have-pages` validation - whiteboard page refs weren't recognized 12 | * Add var to allow validations to access graph's config 13 | * Fix exclude option unable to exclude tests from other namespaces 14 | 15 | ## 0.4.0 16 | * Add support for custom validations 17 | * Introduce a .graph-validator/config.edn 18 | * Introduce a CLI version of the action 19 | 20 | ## 0.3.0 21 | * Add validation for tags and page-refs having pages 22 | * Add validation for assets existing and being used 23 | * Add exclude option for excluding certain validations 24 | 25 | ## 0.2.0 26 | * Add detection of invalid properties 27 | * Internally action uses nbb.edn instead of yarn workspaces for managing logseq dep. Much easier to 28 | manage graph-parser updates 29 | * Bump to logseq/graph-parser 223b62de28855d5ac6d82a330aa3c16ec1165272 (0.8.10) 30 | 31 | ## 0.1.0 32 | 33 | * Initial release with detection of invalid block refs and invalid queries 34 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Logseq Graph Validator' 2 | description: 'Runs tests on a logseq graph to see that some features are correctly used e.g. block refs and queries' 3 | inputs: 4 | directory: 5 | description: 'Directory to parse' 6 | required: true 7 | default: '.' 8 | exclude: 9 | description: 'Validations to exclude' 10 | required: false 11 | default: 'logseq-graph-validator-empty' 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | repository: logseq/graph-validator 20 | path: .logseq-graph-validator 21 | 22 | - name: Set up Clojure 23 | uses: DeLaGuardo/setup-clojure@master 24 | with: 25 | cli: 1.11.1.1182 26 | bb: 1.0.164 27 | 28 | - name: Set up Node 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: '18' 32 | cache: 'yarn' 33 | cache-dependency-path: .logseq-graph-validator/yarn.lock 34 | 35 | - name: Fetch yarn deps 36 | run: cd .logseq-graph-validator && yarn install --immutable 37 | shell: bash 38 | 39 | - name: Nbb cache 40 | uses: actions/cache@v3 41 | id: nbb-deps 42 | with: 43 | path: | 44 | ~/.m2/repository 45 | ~/.gitlibs 46 | .logseq-graph-validator/.nbb/.cache 47 | key: ${{ runner.os }}-nbb-deps-${{ hashFiles('.logseq-graph-validator/nbb.edn') }} 48 | restore-keys: ${{ runner.os }}-nbb-deps- 49 | 50 | - name: Fetch nbb deps 51 | if: steps.nbb-deps.outputs.cache-hit != 'true' 52 | run: cd .logseq-graph-validator && yarn nbb-logseq -e ':fetching-deps' 53 | shell: bash 54 | 55 | - name: Run nbb tests 56 | run: cd .logseq-graph-validator && node graph_validator.mjs --directory ${{ inputs.directory }} --exclude ${{ inputs.exclude }} 57 | shell: bash 58 | -------------------------------------------------------------------------------- /src/logseq/graph_validator.cljs: -------------------------------------------------------------------------------- 1 | (ns logseq.graph-validator 2 | "Github action that runs tests on a given graph directory" 3 | (:require [clojure.test :as t] 4 | [logseq.graph-parser.cli :as gp-cli] 5 | [logseq.graph-validator.state :as state] 6 | [logseq.graph-validator.config :as config] 7 | [logseq.graph-validator.default-validations] 8 | [clojure.edn :as edn] 9 | [clojure.string :as string] 10 | [babashka.cli :as cli] 11 | [promesa.core :as p] 12 | [nbb.classpath :as classpath] 13 | ["fs" :as fs] 14 | ["path" :as path])) 15 | 16 | (defn- setup-graph [dir] 17 | (when-not (fs/existsSync dir) 18 | (println (str "Error: The directory '" dir "' does not exist.")) 19 | (js/process.exit 1)) 20 | (println "Parsing graph" dir) 21 | (reset! state/graph-dir dir) 22 | (let [{:keys [conn asts]} (gp-cli/parse-graph dir {:verbose false})] 23 | (reset! state/all-asts (mapcat :ast asts)) 24 | ;; Gross but necessary to avoid double deref for every db fetch 25 | (set! state/db-conn conn) 26 | (println "Ast node count:" (count @state/all-asts)))) 27 | 28 | (defn- exclude-tests 29 | "Hacky way to exclude tests because t/run-tests doesn't give us test level control" 30 | [tests] 31 | (doseq [t tests] 32 | (let [[test-ns test-name] 33 | (if (string/includes? (str t) "/") 34 | (string/split t #"/") ["logseq.graph-validator.default-validations" t])] 35 | (when-let [var (get (ns-publics (symbol test-ns)) (symbol test-name))] 36 | (println "Excluded test" var) 37 | (alter-meta! var dissoc :test))))) 38 | 39 | (def spec 40 | "Options spec" 41 | {:add-namespaces {:alias :a 42 | :coerce [] 43 | :desc "Additional namespaces to test"} 44 | :directory {:desc "Graph directory to validate" 45 | :alias :d 46 | :default "."} 47 | :exclude {:alias :e 48 | :coerce [] 49 | :desc "Specific tests to exclude"} 50 | :help {:alias :h 51 | :desc "Print help"}}) 52 | 53 | (defn- read-config [config] 54 | (try 55 | (edn/read-string config) 56 | (catch :default _ 57 | (println "Error: Failed to parse config. Make sure it is valid EDN") 58 | (js/process.exit 1)))) 59 | 60 | (defn- get-validator-config [dir user-config] 61 | (merge-with (fn [v1 v2] 62 | (if (and (map? v1) (map? v2)) 63 | (merge v1 v2) v2)) 64 | config/default-config 65 | (when (fs/existsSync (path/join dir ".graph-validator" "config.edn")) 66 | (read-config 67 | (str (fs/readFileSync (path/join dir ".graph-validator" "config.edn"))))) 68 | user-config)) 69 | 70 | (defn- run-tests [dir user-config] 71 | (let [;; Only allow non-empty options to not override .graph-validator/config.edn 72 | user-config' (into {} (keep (fn [[k v]] (when (seq v) [k v])) user-config)) 73 | {:keys [exclude add-namespaces] :as config} (get-validator-config dir user-config')] 74 | (reset! state/config config) 75 | (when (seq add-namespaces) 76 | (classpath/add-classpath (path/join dir ".graph-validator"))) 77 | (-> (p/do! (apply require (map symbol add-namespaces))) 78 | (p/then 79 | (fn [_promise-results] 80 | (when (seq exclude) 81 | (exclude-tests exclude)) 82 | (setup-graph dir) 83 | (apply t/run-tests (into ['logseq.graph-validator.default-validations] 84 | (map symbol add-namespaces))))) 85 | (p/catch 86 | (fn [err] 87 | (prn :unexpected-failure! err) 88 | (js/process.exit 1)))))) 89 | 90 | (defn -main [& args] 91 | (let [options (-> (cli/parse-opts args {:spec spec}) 92 | ;; Handle empty collection values coming from action.yml 93 | (update :exclude #(if (= ["logseq-graph-validator-empty"] %) [] %))) 94 | _ (when (:help options) 95 | (println (str "Usage: logseq-graph-validator [OPTIONS]\nOptions:\n" 96 | (cli/format-opts {:spec spec}))) 97 | (js/process.exit 1)) 98 | ;; Debugging info for CI 99 | _ (when js/process.env.CI (println "Options:" (pr-str options))) 100 | ;; In CI, move up a directory since the script is run in subdirectory of 101 | ;; a project 102 | dir (if js/process.env.CI (path/join ".." (:directory options)) (:directory options))] 103 | (run-tests dir (select-keys options [:add-namespaces :exclude])))) 104 | 105 | #js {:main -main} 106 | -------------------------------------------------------------------------------- /src/logseq/graph_validator/default_validations.cljs: -------------------------------------------------------------------------------- 1 | (ns logseq.graph-validator.default-validations 2 | "Default validations that are enabled on graph-validator" 3 | (:require ["fs" :as fs] 4 | ["path" :as path] 5 | [clojure.edn :as edn] 6 | [clojure.set :as set] 7 | [clojure.string :as string] 8 | [clojure.test :as t :refer [deftest is]] 9 | [clojure.walk :as walk] 10 | [datascript.core :as d] 11 | [logseq.common.util.block-ref :as block-ref] 12 | [logseq.db.file-based.rules :as file-rules] 13 | [logseq.graph-validator.state :as state])) 14 | 15 | (defn- extract-subnodes-by-pred [pred node] 16 | (cond 17 | (= "Heading" (ffirst node)) 18 | (filter pred (-> node first second :title)) 19 | 20 | ;; E.g. for subnodes buried in Paragraph 21 | (vector? (-> node first second)) 22 | (filter pred (-> node first second)))) 23 | 24 | (defn- ast->block-refs [ast] 25 | (->> ast 26 | (mapcat (partial extract-subnodes-by-pred 27 | #(and (= "Link" (first %)) 28 | (= "Block_ref" (-> % second :url first))))) 29 | (map #(-> % second :url second)))) 30 | 31 | (defn- ast->embed-refs [ast] 32 | (->> ast 33 | (mapcat (partial extract-subnodes-by-pred 34 | #(and (= "Macro" (first %)) 35 | (= "embed" (:name (second %))) 36 | (block-ref/get-block-ref-id (str (first (:arguments (second %)))))))) 37 | (map #(-> % second :arguments first block-ref/get-block-ref-id)))) 38 | 39 | (deftest block-refs-link-to-blocks-that-exist 40 | (let [block-refs (ast->block-refs @state/all-asts)] 41 | (println "Found" (count block-refs) "block refs") 42 | (is (empty? 43 | (set/difference 44 | (set block-refs) 45 | (->> (d/q '[:find (pull ?b [:block/properties]) 46 | :in $ % 47 | :where (has-property ?b :id)] 48 | @state/db-conn 49 | [(:has-property file-rules/query-dsl-rules)]) 50 | (map first) 51 | (map (comp :id :block/properties)) 52 | set)))))) 53 | 54 | (deftest embed-block-refs-link-to-blocks-that-exist 55 | (let [embed-refs (ast->embed-refs @state/all-asts)] 56 | (println "Found" (count embed-refs) "embed block refs") 57 | (is (empty? 58 | (set/difference 59 | (set embed-refs) 60 | (->> (d/q '[:find (pull ?b [:block/properties]) 61 | :in $ % 62 | :where (has-property ?b :id)] 63 | @state/db-conn 64 | [(:has-property file-rules/query-dsl-rules)]) 65 | (map first) 66 | (map (comp :id :block/properties)) 67 | set)))))) 68 | 69 | (defn- ast->queries 70 | [ast] 71 | (->> ast 72 | (mapcat (fn [nodes] 73 | (keep 74 | (fn [subnode] 75 | (when (= ["Custom" "query"] (take 2 subnode)) 76 | (get subnode 4))) 77 | nodes))))) 78 | 79 | (deftest advanced-queries-have-valid-schema 80 | (let [query-strings (ast->queries @state/all-asts)] 81 | (println "Found" (count query-strings) "queries") 82 | (is (empty? (keep #(let [query (try (edn/read-string %) 83 | (catch :default _ nil))] 84 | (when (nil? query) %)) 85 | query-strings)) 86 | "Queries are valid EDN") 87 | 88 | (is (empty? (keep #(let [query (try (edn/read-string %) 89 | (catch :default _ nil))] 90 | (when (not (contains? query :query)) %)) 91 | query-strings)) 92 | "Queries have required :query key"))) 93 | 94 | (deftest invalid-properties-dont-exist 95 | (is (empty? 96 | (->> (d/q '[:find (pull ?b [*]) 97 | :in $ 98 | :where 99 | [?b :block/properties]] 100 | @state/db-conn) 101 | (map first) 102 | (filter #(seq (:block/invalid-properties %))))))) 103 | 104 | (defn- ast->asset-links [ast] 105 | (->> ast 106 | (mapcat (partial extract-subnodes-by-pred 107 | #(and (= "Link" (first %)) 108 | (= "Search" (-> % second :url first))))) 109 | (map #(-> % second :url second)) 110 | (keep #(when (and (string? %) (= "assets" (path/basename (path/dirname %)))) 111 | %)))) 112 | 113 | ;; Only checks top-level assets for now 114 | (deftest assets-exist-and-are-used 115 | (let [used-assets (set (map path/basename (ast->asset-links @state/all-asts))) 116 | ;; possibly used because not all pdfs will have pdf highlights 117 | possibly-used-assets (->> used-assets 118 | (filter #(re-find #"\.pdf$" %)) 119 | (map #(string/replace-first % #"\.pdf$" ".edn")) 120 | set) 121 | all-assets (if (fs/existsSync (path/join @state/graph-dir "assets")) 122 | (->> (fs/readdirSync (path/join @state/graph-dir "assets") #js {:withFileTypes true}) 123 | (filter #(not (.isDirectory %))) 124 | (map #(.-name %)) 125 | set) 126 | #{})] 127 | (println "Found" (count used-assets) "assets") 128 | (is (empty? (set/difference used-assets all-assets)) 129 | "All used assets should exist") 130 | (is (empty? (set/difference all-assets used-assets possibly-used-assets)) 131 | "All assets should be used"))) 132 | 133 | (defn- keep-for-ast [keep-node-fn nodes] 134 | (let [found (atom [])] 135 | (walk/postwalk 136 | (fn [elem] 137 | (when-let [saveable-val (and (vector? elem) (keep-node-fn elem))] 138 | (swap! found conj saveable-val)) 139 | elem) 140 | nodes) 141 | @found)) 142 | 143 | (def ast->tag 144 | #(when (and (= "Tag" (first %)) (= "Plain" (-> % second first first))) 145 | (-> % second first second))) 146 | 147 | (defn- ast->tags [nodes] 148 | (keep-for-ast ast->tag nodes)) 149 | 150 | (defn- ast->false-tags [nodes] 151 | (keep-for-ast 152 | (fn [node] 153 | (cond 154 | ;; Pull out background-color properties 155 | (= "Property_Drawer" (first node)) 156 | (->> (second node) 157 | (filter #(#{"background-color"} (first %))) 158 | (mapcat #(get % 2)) 159 | (keep ast->tag)) 160 | 161 | ;; Pull out tags in advanced queries 162 | (= ["Custom" "query"] (take 2 node)) 163 | (when (= "Paragraph" (ffirst (get node 3))) 164 | (->> (get node 3) 165 | first 166 | second 167 | (keep ast->tag))))) 168 | 169 | nodes)) 170 | 171 | (defn- ast->page-refs [nodes] 172 | (keep-for-ast 173 | #(when (and (= "Link" (first %)) (= "Page_ref" (-> % second :url first))) 174 | (-> % second :url second)) 175 | nodes)) 176 | 177 | (defn- get-all-aliases 178 | [db] 179 | (->> (d/q '[:find (pull ?b [:block/properties]) 180 | :in $ % 181 | :where 182 | (has-page-property ?b :alias)] 183 | db 184 | [(:has-page-property file-rules/query-dsl-rules)]) 185 | (map first) 186 | (map (comp :alias :block/properties)) 187 | (mapcat identity) 188 | (map string/lower-case) 189 | set)) 190 | 191 | ;; Ignores journal page references since those are used as datestamps 192 | (deftest tags-and-page-refs-have-pages 193 | (let [used-tags* (set (map (comp string/lower-case path/basename) (ast->tags @state/all-asts))) 194 | false-used-tags (mapcat identity (ast->false-tags @state/all-asts)) 195 | used-tags (apply disj used-tags* false-used-tags) 196 | used-page-refs* (set (map string/lower-case (ast->page-refs @state/all-asts))) 197 | ;; TODO: Add more thorough version with gp-config/get-date-formatter as needed 198 | used-page-refs (set (remove #(re-find #"^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+" %) 199 | used-page-refs*)) 200 | aliases (get-all-aliases @state/db-conn) 201 | all-db-pages* (->> (d/q '[:find ?n 202 | :where [?b :block/name ?n] [?b :block/file] (not [?b :block/type "journal"])] 203 | @state/db-conn) 204 | (map first) 205 | set) 206 | all-pages (into all-db-pages* aliases)] 207 | (println "Found" (count all-pages) "pages in db") 208 | (println "Found" (count used-tags) "tags") 209 | (println "Found" (count used-page-refs) "page refs") 210 | (is (empty? (set/difference used-tags all-pages)) 211 | "All used tags should have pages") 212 | 213 | (is (empty? (set/difference used-page-refs all-pages)) 214 | "All used page refs should have pages"))) 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | This is a [github action](https://github.com/features/actions) to run 4 | [validations](#validations) on a Logseq graph. This action can also be run as a 5 | [CLI](#cli). Validations check to ensure queries, block refs and properties are 6 | valid. This action can catch errors that show up in the UI e.g. `Invalid query`. 7 | 8 | ## Usage 9 | 10 | To setup this action, add the file `.github/workflows/test.yml` to your graph's 11 | github repository with the following content: 12 | 13 | ``` yaml 14 | on: [push] 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | name: Run graph tests 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Run graph-validator tests 25 | uses: logseq/graph-validator@main 26 | ``` 27 | 28 | That's it! This job will then run on future git pushes and fail if any invalid parts 29 | of your graph are detected. 30 | 31 | NOTE: The above example defaults to picking up new changes. If you'd prefer to stay on a stable version use the format `logseq/graph-validator@VERSION` e.g. `logseq/graph-validator@v0.1.0`. See CHANGELOG.md for released versions. 32 | 33 | ### Action Inputs 34 | 35 | This action can take inputs e.g.: 36 | 37 | ```yaml 38 | - name: Run graph-validator tests 39 | uses: logseq/graph-validator@main 40 | with: 41 | directory: logseq-graph-directory 42 | exclude: some-validation-test 43 | ``` 44 | 45 | This action has the following inputs: 46 | 47 | #### `directory` 48 | 49 | **Required:** The directory of the graph to test. Defaults to `.`. 50 | 51 | #### `exclude` 52 | 53 | Optional: A whitespace separated list of validations to exclude from running. 54 | Validation names are listed in [default validations](#default-validations) e.g. 55 | `tags-and-page-refs-have-pages`. Defaults to empty. 56 | 57 | ### CLI 58 | 59 | To use this as a CLI locally, first install 60 | [babashka](https://github.com/babashka/babashka#installation) and 61 | [clojure](https://clojure.org/guides/install_clojure). Then: 62 | 63 | ```sh 64 | $ git clone https://github.com/logseq/graph-validator 65 | $ cd graph-validator && yarn install 66 | $ yarn global add $PWD 67 | ``` 68 | 69 | Then use it from any logseq graph directory! 70 | ```sh 71 | $ logseq-graph-validator 72 | Parsing graph . 73 | ... 74 | Ran 6 tests containing 9 assertions. 75 | 0 failures, 0 errors. 76 | 77 | # Use the exclude option to exclude certain validations from being run 78 | $ logseq-graph-validator --exclude assets-exist-and-are-used tags-and-page-refs-have-pages 79 | Excluded test #'action/assets-exist-and-are-used 80 | Excluded test #'action/tags-and-page-refs-have-pages 81 | Parsing graph . 82 | ... 83 | Ran 4 tests containing 5 assertions. 84 | 0 failures, 0 errors. 85 | ``` 86 | 87 | NOTE: Running the CLI currently depends on a clean git state e.g. `git status` prints `nothing to 88 | commit, working tree clean`. 89 | 90 | ## Configuration 91 | 92 | To configure the validator, create a `.graph-validator/config.edn` file in your 93 | graph's directory. See [the config 94 | file](https://github.com/logseq/graph-validator/blob/main/src/logseq/graph_validator/config.cljs) 95 | for the full list of configuration keys. 96 | 97 | ## Validations 98 | 99 | Validations runs on _all_ files for a given graph. A validation prints if it 100 | fails. A validation can have multiple errors. For engineers, a validation is 101 | just a ClojureScript `deftest`. 102 | 103 | ### Default Validations 104 | 105 | These are validations that are enabled by default. Any of them can be disabled 106 | with the `exclude` option above. Available validations: 107 | 108 | - `block-refs-link-to-blocks-that-exist` - If a block ref e.g. 109 | `((694dc3ff-e714-4db0-8b36-58f2ff0b48a4))` links to a nonexistent block, 110 | Logseq displays it as invalid. This validation prints all such invalid block ids. 111 | - `embed-block-refs-link-to-blocks-that-exist` - Similar to 112 | `block-refs-link-to-blocks-that-exist`, if an embedded block ref is invalid, 113 | this validation prints its corresponding invalid block id. 114 | - `advanced-queries-have-valid-schema` - If an [advanced query](https://docs.logseq.com/#/page/advanced%20queries) 115 | is not a valid map or missing required keys, this validation prints those 116 | queries. 117 | - `invalid-properties-dont-exist` - A 118 | [property](https://docs.logseq.com/#/page/properties/block/usage) can get in 119 | an invalid state with invalid names. This validation prints those invalid 120 | properties. 121 | - `assets-exist-and-are-used` - This validation catches two types of common 122 | issues with invalid assets - asset links that point to assets that don't exist 123 | and assets that are not referenced anywhere in the graphs. 124 | - `tags-and-page-refs-have-pages` - This validation prints all tags and page 125 | refs that don't have a Logseq page. This is useful for those using Logseq more 126 | like a personal wikipedia and want to ensure that each link has meaningful content. 127 | 128 | ### Custom Validations 129 | 130 | Custom validations can be added to your graph by writing nbb-logseq compatible 131 | [cljs tests](https://clojurescript.org/tools/testing) under `.graph-validator/`. 132 | graph-validator already handles parsing the graph, so all a test does is 133 | query against the graph's datascript db, `logseq.graph-parser.state/db-conn`. See 134 | `logseq.graph-parser.state` for other available state to use in tests. For 135 | example, add a `.graph-validator/foo.cljs` with the content: 136 | 137 | ```cljs 138 | (ns foo 139 | (:require [cljs.test :refer [deftest is]] 140 | [logseq.graph-validator.state :as state] 141 | [datascript.core :as d])) 142 | 143 | (deftest no-page-named-foo 144 | (is (= 0 145 | (->> (d/q '[:find (pull ?b [*]) 146 | :in $ ?name 147 | :where 148 | [?b :block/name ?bn] 149 | [(= ?name ?bn)]] 150 | @state/db-conn 151 | "foo") 152 | count)))) 153 | ``` 154 | 155 | This test does a silly check that the page 'foo' doesn't exist in the graph. To 156 | enable this custom test in your action, create `.graph-validator/config.edn` 157 | with `{:add-namespaces [foo]}`. 158 | 159 | For a real world example of a custom validation, see [this example in docs](https://github.com/logseq/docs/blob/master/.graph-validator/schema.cljs). 160 | 161 | ## Development 162 | 163 | This github action use [nbb-logseq](https://github.com/logseq/nbb-logseq) and the [graph-parser 164 | library](https://github.com/logseq/logseq/tree/master/deps/graph-parser) to analyze a Logseq graph 165 | using its database and markdown AST data. 166 | 167 | ## Write your own Logseq action 168 | 169 | This github action serves as an example that can be easily customized. This 170 | action can validate almost anything in a Logseq graph as it has access to the 171 | graph's database connection and to the full markdown AST of a graph. To write 172 | your own action: 173 | 174 | 1. Copy this whole repository. 175 | 2. Write your own implementation in `action.cljs`. 176 | 1. `logseq.graph-parser.cli/parse-graph` is the fn you'll want to create a database connection and fetch markdown ast data. 177 | 2. This example uses `cljs.test` tests to run multiple validations on a graph. This is a personal preference and ultimately you only need your script to exit `0` on success and a non-zero code on failure. 178 | 3. Update `action.yml` with your action's name, description and inputs. 179 | 180 | Your action can then be used as `user/repo@main`. To allow others to use specific versions of your action, [publish it](https://docs.github.com/en/actions/creating-actions/publishing-actions-in-github-marketplace). 181 | 182 | ### Github action type 183 | 184 | This action [is a composite action](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action) that installs dependencies at job runtime. It would have been preferable to use a [javascript action](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action) that already bundles dependencies with a tool like `ncc`. `ncc` is not able to handle dynamic imports i.e. requires of npm libraries in cljs code. https://github.com/borkdude/nbb-action-example demonstrates a workaround for this. Unfortunately the graph-parser library is a large, fast-moving library that would be difficult to maintain with such an approach. A docker action approach has not been investigated and could also be promising. 185 | 186 | ### Run this action elsewhere 187 | 188 | You may want to run this locally or in another environment e.g. gitlab. To run this locally: 189 | 190 | ```sh 191 | # Setup once 192 | $ yarn install 193 | 194 | # Run this each time 195 | $ node graph_validator.mjs /path/to/graph 196 | ``` 197 | 198 | To run this in another environment, clone this repo, install dependencies and 199 | run tests. These steps are shown in the `action.yml` file. You can ignore the 200 | caching steps which are specific to github. 201 | 202 | ## LICENSE 203 | See LICENSE.md 204 | 205 | ## Additional Links 206 | * https://github.com/borkdude/nbb-action-example - Example nbb github action that inspired this one 207 | * https://github.com/logseq/docs - Logseq graph that uses this action 208 | -------------------------------------------------------------------------------- /src/logseq/graph_validator/validations/property.cljs: -------------------------------------------------------------------------------- 1 | (ns logseq.graph-validator.validations.property 2 | "This ns manages all user properties. Validations ensure the following about a graph: 3 | * Each property has a page with required url and rangeIncludes properties. 4 | domainIncludes and unique are optional properties. To ignore certain 5 | properties having pages and thus validations, add them to the config :property/ignore-list. 6 | * Property names start with a lower case letter 7 | * The rangeIncludes property is a list of values a property can have. They can be 8 | a descendant of Class or DataType. 9 | * If a Class descendant, then the value is valid if its a page with a type property that is the 10 | rangeIncludes class or a descendant of the rangeIncludes class 11 | * If a DataType descendant, then the value must be that specific DataType as 12 | determined by its validator function. The following datatypes are already 13 | defined: Integer, String, Boolean, Date, Time, Uri, Integer, Float and 14 | StringWithRefs. Custom data types can be defined by creating a page for them 15 | and then adding a validation fn for them in :property/data-type-validations. 16 | * A property can have an optional domainIncludes property. This property is a 17 | list of classes that would use the property. 18 | * If a property has the :unique property set to true, a validation is run to ensure every 19 | property value is unique 20 | 21 | These validations rely on the following special property and class names: 22 | * Classes and properties are pages and they inherit from a `Class` page and a `Property` page 23 | * Pages have classes through a `type` property. Classes can have subclasses through the `parent` property 24 | * A property's range and domain values are specified through `domainIncludes` and `rangeIncludes` properties 25 | * Property values that are literal values are instances of subclasses to the `DataType` class." 26 | (:require [clojure.test :refer [deftest is use-fixtures]] 27 | [clojure.set :as set] 28 | [clojure.string :as string] 29 | [logseq.graph-validator.state :as state] 30 | [logseq.graph-parser.property :as gp-property] 31 | [logseq.common.util.page-ref :as page-ref] 32 | [logseq.common.util :as common-util] 33 | [logseq.db.file-based.rules :as file-rules] 34 | [datascript.core :as d] 35 | [clojure.edn :as edn] 36 | ["path" :as path] 37 | ["fs" :as fs])) 38 | 39 | (def logseq-config (atom nil)) 40 | 41 | ;; Copied from graph-validator for now 42 | (defn- read-config [config] 43 | (try 44 | (edn/read-string config) 45 | (catch :default _ 46 | (println "Error: Failed to parse config. Make sure it is valid EDN") 47 | (js/process.exit 1)))) 48 | 49 | (use-fixtures 50 | :once 51 | (fn [f] 52 | (reset! logseq-config 53 | (read-config 54 | (str (fs/readFileSync (path/join @state/graph-dir "logseq" "config.edn"))))) 55 | (f))) 56 | 57 | ;; Db util fns 58 | ;; =========== 59 | (defn- get-property-text-values-for-property 60 | "Get property values for property from :block/properties-text-values" 61 | [db property] 62 | (let [pred (fn [_db properties] 63 | (get properties property))] 64 | (->> 65 | (d/q 66 | '[:find ?property-val 67 | :in $ ?pred 68 | :where 69 | [?b :block/properties-text-values ?p] 70 | [(?pred $ ?p) ?property-val]] 71 | db 72 | pred)))) 73 | 74 | (defn- get-all-properties 75 | "Get all unique property names" 76 | [db] 77 | (let [properties (d/q 78 | '[:find [?p ...] 79 | :where 80 | [_ :block/properties ?p]] 81 | db)] 82 | (->> (map keys properties) 83 | (apply concat) 84 | distinct 85 | set))) 86 | 87 | (defn- get-all-property-values 88 | "Get all property values from :properties" 89 | [db] 90 | (->> (d/q 91 | '[:find ?p 92 | :in $ 93 | :where 94 | [?b :block/properties ?p] 95 | [(missing? $ ?b :block/pre-block?)]] 96 | db) 97 | (map first))) 98 | 99 | (defn- get-all-property-entities 100 | "Get all property entities from :properties" 101 | [db] 102 | (d/q 103 | '[:find (pull ?b [:block/content :block/title :block/properties]) ?p 104 | :in $ 105 | :where 106 | [?b :block/properties ?p] 107 | [(missing? $ ?b :block/pre-block?)]] 108 | db)) 109 | 110 | (defn- get-all-property-text-values 111 | "Get all property values from :properties-text-values" 112 | [db] 113 | (->> (d/q 114 | '[:find ?p 115 | :in $ 116 | :where 117 | [?b :block/properties-text-values ?p] 118 | [(missing? $ ?b :block/pre-block?)]] 119 | db) 120 | (map first))) 121 | 122 | (defn- get-all-user-properties 123 | [db] 124 | (let [built-in-properties (set/union (gp-property/hidden-built-in-properties) 125 | (gp-property/editable-built-in-properties) 126 | ;; Copied properties for namespaces that can't be loaded :( 127 | #{:card-last-interval :card-repeats :card-last-reviewed 128 | :card-next-schedule :card-last-score :card-ease-factor 129 | :background-image})] 130 | (set/difference (get-all-properties db) 131 | built-in-properties))) 132 | 133 | (defn- get-all-page-things 134 | [] 135 | (->> (d/q '[:find (pull ?b [*]) 136 | :in $ % 137 | :where [?b :block/title] 138 | (has-page-property ?b :type)] 139 | @state/db-conn 140 | [(:has-page-property file-rules/query-dsl-rules)]) 141 | (map first))) 142 | 143 | ;; Macro fns 144 | ;; ========= 145 | (defn- macro-subs 146 | [macro-content arguments] 147 | (loop [s macro-content 148 | args arguments 149 | n 1] 150 | (if (seq args) 151 | (recur 152 | (string/replace s (str "$" n) (first args)) 153 | (rest args) 154 | (inc n)) 155 | s))) 156 | 157 | (defn- macro-expand-value 158 | "Checks each value for a macro and expands it if there's a logseq config for it" 159 | [val logseq-config] 160 | (if-let [[_ macro args] (and (string? val) 161 | (seq (re-matches #"\{\{(\S+)\s+(.*)\}\}" val)))] 162 | (if-let [content (get-in logseq-config [:macros macro])] 163 | (macro-subs content (string/split args #"\s+")) 164 | val) 165 | val)) 166 | 167 | ;; Other util fns 168 | ;; ============== 169 | (defn- validate-property-only-has-not-refs 170 | "This is for properties that have strings with refs in them. Can't validate 171 | them as strings since they may have refs so have to look at raw values and at 172 | least confirm they aren't just a ref" 173 | [property] 174 | (is (empty? 175 | (remove #(and (not (page-ref/page-ref? %)) 176 | (not (re-matches #"#\S+" %))) 177 | (map first (get-property-text-values-for-property @state/db-conn property)))) 178 | (str "All values for " property " have correct data type"))) 179 | 180 | (def preds 181 | {:Integer int? 182 | :String string? 183 | :Boolean boolean? 184 | :Uri common-util/url? 185 | ;; TODO: Make this configurable 186 | :Date #(re-matches #"\d{2}-\d{2}-\d{4}" %) 187 | :Time #(re-matches #"\d\d?:\d\d" %) 188 | ;; Have to convert decimal for now b/c of inconsistent int parsing 189 | :Float #(if (string? %) (float? (parse-double %)) (float? %))}) 190 | 191 | (defn- get-preds* 192 | [] 193 | (merge preds 194 | (update-vals (:property/data-type-validations @state/config) 195 | (fn [f] 196 | (fn [x] (eval (list f x))))))) 197 | 198 | (def get-preds (memoize get-preds*)) 199 | 200 | (defn- validate-data-type 201 | [property {:keys [data-type]} property-values] 202 | (if (= :StringWithRefs data-type) 203 | (validate-property-only-has-not-refs property) 204 | (if-let [pred (get (get-preds) data-type)] 205 | (do 206 | (assert (pos? (count (get property-values property))) 207 | (str "Property " property " must have at least one value")) 208 | (is (empty? 209 | (remove pred (map #(macro-expand-value % @logseq-config) 210 | (get property-values property)))) 211 | (str "All values for " property " have data type " data-type))) 212 | (throw (ex-info (str "No data-type definition for " data-type) {}))))) 213 | 214 | ;; Tests 215 | ;; ===== 216 | (deftest properties-with-data-types-have-valid-ranges 217 | (let [all-things (get-all-page-things) 218 | children-maps (->> all-things 219 | (filter #(= "Class" (first (get-in % [:block/properties :type])))) 220 | (map #(vector (:block/title %) (first (get-in % [:block/properties :parent])))) 221 | (into {})) 222 | range-properties (->> all-things 223 | (filter #(= "Property" (first (get-in % [:block/properties :type])))) 224 | (keep #(when-let [ranges (get-in % [:block/properties :rangeincludes])] 225 | (vector (keyword (:block/name %)) ranges))) 226 | (filter #(contains? (set (map children-maps (second %))) "DataType")) 227 | (into {})) 228 | _ (println "Validating ranges with data types for" (count range-properties) "properties") 229 | property-values (->> (get-all-property-values @state/db-conn) 230 | (mapcat identity) 231 | (reduce (fn [m [k v]] (update m k (fnil conj #{}) v)) {}))] 232 | (doseq [[property ranges] range-properties] 233 | ;; later: support multiple data-types 234 | (validate-data-type property {:data-type (keyword (first ranges))} property-values)))) 235 | 236 | (defn- is-ancestor? [children-maps parent-classes child-class] 237 | (seq 238 | (set/intersection 239 | (loop [ancestors #{} 240 | current-thing child-class] 241 | (if-let [parent (children-maps current-thing)] 242 | (recur (conj ancestors parent) parent) 243 | ancestors)) 244 | parent-classes))) 245 | 246 | (defn- validate-property-ranges 247 | [property ranges property-values page-classes children-maps] 248 | (let [property-values-to-classes (select-keys page-classes (map first property-values))] 249 | (is (empty? (->> property-values-to-classes 250 | (remove #(contains? ranges (val %))) 251 | (remove #(is-ancestor? children-maps ranges (val %))))) 252 | (str "All property values for " property " must be one of " ranges)))) 253 | 254 | (deftest properties-with-refs-have-valid-ranges 255 | (let [all-things (get-all-page-things) 256 | children-maps (->> all-things 257 | (filter #(= "Class" (first (get-in % [:block/properties :type])))) 258 | (map #(vector (:block/title %) (first (get-in % [:block/properties :parent])))) 259 | (into {})) 260 | range-properties (->> all-things 261 | (filter #(= "Property" (first (get-in % [:block/properties :type])))) 262 | (keep #(when-let [ranges (get-in % [:block/properties :rangeincludes])] 263 | (vector (keyword (:block/name %)) ranges))) 264 | (filter #(not (contains? (set (map children-maps (second %))) "DataType"))) 265 | (into {})) 266 | _ (println "Validating ranges with entities for" (count range-properties) "properties") 267 | property-values (->> (get-all-property-values @state/db-conn) 268 | (mapcat identity) 269 | (reduce (fn [m [k v]] (update m k (fnil conj #{}) v)) {})) 270 | page-classes (->> all-things 271 | (mapcat (fn [b] 272 | (map #(vector % (first (get-in b [:block/properties :type]))) 273 | (into [(:block/title b)] 274 | (get-in b [:block/properties :alias]))))) 275 | (into {}))] 276 | (doseq [[property ranges] range-properties] 277 | (validate-property-ranges property ranges (property-values property) page-classes children-maps)))) 278 | 279 | (defn- validate-property-domains 280 | [property domains property-ents-to-classes children-maps] 281 | (is (empty? (->> property-ents-to-classes 282 | (remove #(contains? domains (val %))) 283 | (remove #(is-ancestor? children-maps domains (val %))))) 284 | (str "All property entities for " property " must be one of " domains))) 285 | 286 | (deftest properties-with-refs-have-valid-domains 287 | (let [all-things (get-all-page-things) 288 | domain-properties (->> all-things 289 | (filter #(= "Property" (first (get-in % [:block/properties :type])))) 290 | (keep #(when-let [domains (get-in % [:block/properties :domainincludes])] 291 | (vector (keyword (:block/name %)) domains))) 292 | (into {})) 293 | property-ents (->> (get-all-property-entities @state/db-conn) 294 | (mapcat (fn [[ent props]] 295 | (map #(vector (first %) 296 | (-> ent 297 | (dissoc :block/properties) 298 | (assoc :types (get-in ent [:block/properties :type])))) 299 | props))) 300 | (reduce (fn [m [prop ent]] 301 | (assoc-in m 302 | [prop (select-keys ent [:block/content :block/title])] 303 | ;; later: Support multiple types 304 | (first (:types ent)))) 305 | {})) 306 | _ (println "Validating domains for" (count domain-properties) "properties") 307 | children-maps (->> all-things 308 | (filter #(= "Class" (first (get-in % [:block/properties :type])))) 309 | (map #(vector (:block/title %) (first (get-in % [:block/properties :parent])))) 310 | (into {}))] 311 | (doseq [[property domains] domain-properties] 312 | (validate-property-domains property domains (property-ents property) children-maps)))) 313 | 314 | (defn- get-property-pages [] 315 | (->> (d/q 316 | '[:find (pull ?b [*]) 317 | :in $ % 318 | :where (page-property ?b :type "Property")] 319 | @state/db-conn 320 | [(:page-property file-rules/query-dsl-rules)]) 321 | (map first) 322 | (map #(assoc (:block/properties %) :block/name (:block/name %))))) 323 | 324 | (deftest properties-have-property-pages 325 | (is (empty? 326 | (set/difference (set (map name (get-all-user-properties @state/db-conn))) 327 | (set (map :block/name (get-property-pages))) 328 | (set (map name (:property/ignore-list @state/config))))))) 329 | 330 | (deftest properties-have-urls 331 | (is (empty? (remove :url (get-property-pages))) 332 | "All properties have urls")) 333 | 334 | (deftest properties-have-ranges 335 | (is (empty? (remove :rangeincludes (get-property-pages))) 336 | "All properties have rangeIncludes")) 337 | 338 | (deftest properties-are-lower-cased 339 | (is (empty? (remove #(re-find #"^[a-z]" (:block/name %)) (get-property-pages))) 340 | "All properties start with a lowercase letter")) 341 | 342 | (deftest unique-properties-are-unique-across-graph 343 | (let [all-things (get-all-page-things) 344 | unique-properties (->> all-things 345 | (filter #(= "Property" (first (get-in % [:block/properties :type])))) 346 | (filter #(get-in % [:block/properties :unique])))] 347 | (println "Validating uniqueness for" (count unique-properties) "properties") 348 | (doseq [{property :block/name} unique-properties] 349 | (is (empty? 350 | (->> (get-all-property-text-values @state/db-conn) 351 | (map #((keyword property) %)) 352 | frequencies 353 | ((fn [x] (dissoc x nil))) 354 | (filter #(> (val %) 1)) 355 | (into {}))))))) 356 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@logseq/nbb-logseq@logseq/nbb-logseq#feat-db-v24": 6 | version "1.2.173-feat-db-v24" 7 | resolved "https://codeload.github.com/logseq/nbb-logseq/tar.gz/3d5b78b0382c7253bf9874c1f38586dd338434f4" 8 | dependencies: 9 | import-meta-resolve "^4.1.0" 10 | 11 | ansi-regex@^2.0.0: 12 | version "2.1.1" 13 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 14 | integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== 15 | 16 | ansi-regex@^3.0.0: 17 | version "3.0.1" 18 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" 19 | integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== 20 | 21 | camelcase@^5.0.0: 22 | version "5.3.1" 23 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" 24 | integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== 25 | 26 | cliui@^4.0.0: 27 | version "4.1.0" 28 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" 29 | integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== 30 | dependencies: 31 | string-width "^2.1.1" 32 | strip-ansi "^4.0.0" 33 | wrap-ansi "^2.0.0" 34 | 35 | code-point-at@^1.0.0: 36 | version "1.1.0" 37 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 38 | integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== 39 | 40 | cross-spawn@^6.0.0: 41 | version "6.0.5" 42 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" 43 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== 44 | dependencies: 45 | nice-try "^1.0.4" 46 | path-key "^2.0.1" 47 | semver "^5.5.0" 48 | shebang-command "^1.2.0" 49 | which "^1.2.9" 50 | 51 | decamelize@^1.2.0: 52 | version "1.2.0" 53 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 54 | integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== 55 | 56 | end-of-stream@^1.1.0: 57 | version "1.4.4" 58 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 59 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 60 | dependencies: 61 | once "^1.4.0" 62 | 63 | execa@^1.0.0: 64 | version "1.0.0" 65 | resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" 66 | integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== 67 | dependencies: 68 | cross-spawn "^6.0.0" 69 | get-stream "^4.0.0" 70 | is-stream "^1.1.0" 71 | npm-run-path "^2.0.0" 72 | p-finally "^1.0.0" 73 | signal-exit "^3.0.0" 74 | strip-eof "^1.0.0" 75 | 76 | find-up@^3.0.0: 77 | version "3.0.0" 78 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" 79 | integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== 80 | dependencies: 81 | locate-path "^3.0.0" 82 | 83 | get-caller-file@^1.0.1: 84 | version "1.0.3" 85 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" 86 | integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== 87 | 88 | get-stream@^4.0.0: 89 | version "4.1.0" 90 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" 91 | integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== 92 | dependencies: 93 | pump "^3.0.0" 94 | 95 | import-meta-resolve@^4.1.0: 96 | version "4.1.0" 97 | resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#f9db8bead9fafa61adb811db77a2bf22c5399706" 98 | integrity sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw== 99 | 100 | invert-kv@^2.0.0: 101 | version "2.0.0" 102 | resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" 103 | integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== 104 | 105 | is-fullwidth-code-point@^1.0.0: 106 | version "1.0.0" 107 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 108 | integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== 109 | dependencies: 110 | number-is-nan "^1.0.0" 111 | 112 | is-fullwidth-code-point@^2.0.0: 113 | version "2.0.0" 114 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 115 | integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== 116 | 117 | is-stream@^1.1.0: 118 | version "1.1.0" 119 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 120 | integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== 121 | 122 | isexe@^2.0.0: 123 | version "2.0.0" 124 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 125 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 126 | 127 | lcid@^2.0.0: 128 | version "2.0.0" 129 | resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" 130 | integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== 131 | dependencies: 132 | invert-kv "^2.0.0" 133 | 134 | locate-path@^3.0.0: 135 | version "3.0.0" 136 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" 137 | integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== 138 | dependencies: 139 | p-locate "^3.0.0" 140 | path-exists "^3.0.0" 141 | 142 | map-age-cleaner@^0.1.1: 143 | version "0.1.3" 144 | resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" 145 | integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== 146 | dependencies: 147 | p-defer "^1.0.0" 148 | 149 | mem@^4.0.0: 150 | version "4.3.0" 151 | resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" 152 | integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== 153 | dependencies: 154 | map-age-cleaner "^0.1.1" 155 | mimic-fn "^2.0.0" 156 | p-is-promise "^2.0.0" 157 | 158 | mimic-fn@^2.0.0: 159 | version "2.1.0" 160 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" 161 | integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== 162 | 163 | mldoc@^1.5.9: 164 | version "1.5.9" 165 | resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.5.9.tgz#43d740351c64285f0f4988ac9497922d54ae66fc" 166 | integrity sha512-87FQ7hseS87tsk+VdpIigpu8LH+GwmbbFgpxgFwvnbH5oOjmIrc47laH4Dyggzqiy8/vMjDHkl7vsId0eXhCDQ== 167 | dependencies: 168 | yargs "^12.0.2" 169 | 170 | nice-try@^1.0.4: 171 | version "1.0.5" 172 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" 173 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== 174 | 175 | npm-run-path@^2.0.0: 176 | version "2.0.2" 177 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" 178 | integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== 179 | dependencies: 180 | path-key "^2.0.0" 181 | 182 | number-is-nan@^1.0.0: 183 | version "1.0.1" 184 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 185 | integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== 186 | 187 | once@^1.3.1, once@^1.4.0: 188 | version "1.4.0" 189 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 190 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 191 | dependencies: 192 | wrappy "1" 193 | 194 | os-locale@^3.0.0: 195 | version "3.1.0" 196 | resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" 197 | integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== 198 | dependencies: 199 | execa "^1.0.0" 200 | lcid "^2.0.0" 201 | mem "^4.0.0" 202 | 203 | p-defer@^1.0.0: 204 | version "1.0.0" 205 | resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" 206 | integrity sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw== 207 | 208 | p-finally@^1.0.0: 209 | version "1.0.0" 210 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 211 | integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== 212 | 213 | p-is-promise@^2.0.0: 214 | version "2.1.0" 215 | resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" 216 | integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== 217 | 218 | p-limit@^2.0.0: 219 | version "2.3.0" 220 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" 221 | integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== 222 | dependencies: 223 | p-try "^2.0.0" 224 | 225 | p-locate@^3.0.0: 226 | version "3.0.0" 227 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" 228 | integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== 229 | dependencies: 230 | p-limit "^2.0.0" 231 | 232 | p-try@^2.0.0: 233 | version "2.2.0" 234 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 235 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== 236 | 237 | path-exists@^3.0.0: 238 | version "3.0.0" 239 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 240 | integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== 241 | 242 | path-key@^2.0.0, path-key@^2.0.1: 243 | version "2.0.1" 244 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 245 | integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== 246 | 247 | pump@^3.0.0: 248 | version "3.0.0" 249 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 250 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 251 | dependencies: 252 | end-of-stream "^1.1.0" 253 | once "^1.3.1" 254 | 255 | require-directory@^2.1.1: 256 | version "2.1.1" 257 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 258 | integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== 259 | 260 | require-main-filename@^1.0.1: 261 | version "1.0.1" 262 | resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" 263 | integrity sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug== 264 | 265 | semver@^5.5.0: 266 | version "5.7.1" 267 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 268 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 269 | 270 | set-blocking@^2.0.0: 271 | version "2.0.0" 272 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 273 | integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== 274 | 275 | shebang-command@^1.2.0: 276 | version "1.2.0" 277 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 278 | integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== 279 | dependencies: 280 | shebang-regex "^1.0.0" 281 | 282 | shebang-regex@^1.0.0: 283 | version "1.0.0" 284 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 285 | integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== 286 | 287 | signal-exit@^3.0.0: 288 | version "3.0.7" 289 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" 290 | integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== 291 | 292 | string-width@^1.0.1: 293 | version "1.0.2" 294 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 295 | integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== 296 | dependencies: 297 | code-point-at "^1.0.0" 298 | is-fullwidth-code-point "^1.0.0" 299 | strip-ansi "^3.0.0" 300 | 301 | string-width@^2.0.0, string-width@^2.1.1: 302 | version "2.1.1" 303 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 304 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 305 | dependencies: 306 | is-fullwidth-code-point "^2.0.0" 307 | strip-ansi "^4.0.0" 308 | 309 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 310 | version "3.0.1" 311 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 312 | integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== 313 | dependencies: 314 | ansi-regex "^2.0.0" 315 | 316 | strip-ansi@^4.0.0: 317 | version "4.0.0" 318 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 319 | integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== 320 | dependencies: 321 | ansi-regex "^3.0.0" 322 | 323 | strip-eof@^1.0.0: 324 | version "1.0.0" 325 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 326 | integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== 327 | 328 | which-module@^2.0.0: 329 | version "2.0.0" 330 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 331 | integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== 332 | 333 | which@^1.2.9: 334 | version "1.3.1" 335 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 336 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 337 | dependencies: 338 | isexe "^2.0.0" 339 | 340 | wrap-ansi@^2.0.0: 341 | version "2.1.0" 342 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" 343 | integrity sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw== 344 | dependencies: 345 | string-width "^1.0.1" 346 | strip-ansi "^3.0.1" 347 | 348 | wrappy@1: 349 | version "1.0.2" 350 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 351 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 352 | 353 | "y18n@^3.2.1 || ^4.0.0": 354 | version "4.0.3" 355 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" 356 | integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== 357 | 358 | yargs-parser@^11.1.1: 359 | version "11.1.1" 360 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" 361 | integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== 362 | dependencies: 363 | camelcase "^5.0.0" 364 | decamelize "^1.2.0" 365 | 366 | yargs@^12.0.2: 367 | version "12.0.5" 368 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" 369 | integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== 370 | dependencies: 371 | cliui "^4.0.0" 372 | decamelize "^1.2.0" 373 | find-up "^3.0.0" 374 | get-caller-file "^1.0.1" 375 | os-locale "^3.0.0" 376 | require-directory "^2.1.1" 377 | require-main-filename "^1.0.1" 378 | set-blocking "^2.0.0" 379 | string-width "^2.0.0" 380 | which-module "^2.0.0" 381 | y18n "^3.2.1 || ^4.0.0" 382 | yargs-parser "^11.1.1" 383 | --------------------------------------------------------------------------------