0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-09 08:20:45 -05:00

Merge pull request #5019 from penpot/niwinz-static-error-duplication

 Add improvements on how UI (React) errors are handled
This commit is contained in:
Alejandro 2024-08-26 09:26:28 +02:00 committed by GitHub
commit 7ea5c79393
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 189 additions and 113 deletions

View file

@ -20,8 +20,8 @@
:git/url "https://github.com/funcool/beicon.git"} :git/url "https://github.com/funcool/beicon.git"}
funcool/rumext funcool/rumext
{:git/tag "v2.13" {:git/tag "v2.14"
:git/sha "dc8e1e5" :git/sha "0016623"
:git/url "https://github.com/funcool/rumext.git"} :git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.5.0"} instaparse/instaparse {:mvn/version "1.5.0"}

View file

@ -103,6 +103,7 @@
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-error-boundary": "^4.0.13",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.5",
"rxjs": "8.0.0-alpha.14", "rxjs": "8.0.0-alpha.14",
"sax": "^1.4.1", "sax": "^1.4.1",

View file

@ -56,6 +56,14 @@
(print-explain! cause) (print-explain! cause)
(print-trace! cause)))) (print-trace! cause))))
(defn exception->error-data
[cause]
(let [data (ex-data cause)]
(-> data
(assoc :hint (or (:hint data) (ex-message cause)))
(assoc ::instance cause)
(assoc ::trace (.-stack cause)))))
(defn print-error! (defn print-error!
[cause] [cause]
(cond (cond
@ -66,22 +74,14 @@
(print-cause! (ex-message cause) (ex-data cause)) (print-cause! (ex-message cause) (ex-data cause))
:else :else
(let [trace (.-stack cause)] (print-cause! (ex-message cause) (exception->error-data cause))))
(print-cause! (ex-message cause)
{:hint (ex-message cause)
::trace trace
::instance cause}))))
(defn on-error (defn on-error
"A general purpose error handler." "A general purpose error handler."
[error] [error]
(if (map? error) (if (map? error)
(ptk/handle-error error) (ptk/handle-error error)
(let [data (ex-data error) (let [data (exception->error-data error)]
data (-> data
(assoc :hint (or (:hint data) (ex-message error)))
(assoc ::instance error)
(assoc ::trace (.-stack error)))]
(ptk/handle-error data)))) (ptk/handle-error data))))
;; Set the main potok error handler ;; Set the main potok error handler
@ -285,6 +285,7 @@
(let [message (ex-message cause)] (let [message (ex-message cause)]
(or (= message "Possible side-effect in debug-evaluate") (or (= message "Possible side-effect in debug-evaluate")
(= message "Unexpected end of input") (= message "Unexpected end of input")
(str/starts-with? message "invalid props on component")
(str/starts-with? message "Unexpected token ")))) (str/starts-with? message "Unexpected token "))))
(on-unhandled-error [event] (on-unhandled-error [event]

View file

@ -8,9 +8,9 @@
(:require (:require
[app.config :as cf] [app.config :as cf]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.debug.icons-preview :refer [icons-preview]] [app.main.ui.debug.icons-preview :refer [icons-preview]]
[app.main.ui.error-boundary :refer [error-boundary*]]
[app.main.ui.frame-preview :as frame-preview] [app.main.ui.frame-preview :as frame-preview]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.notifications :as notifications] [app.main.ui.notifications :as notifications]
@ -21,7 +21,6 @@
[app.main.ui.static :as static] [app.main.ui.static :as static]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.router :as rt]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def auth-page (def auth-page
@ -42,15 +41,8 @@
(def workspace-page (def workspace-page
(mf/lazy-component app.main.ui.workspace/workspace)) (mf/lazy-component app.main.ui.workspace/workspace))
(mf/defc on-main-error
[{:keys [error] :as props}]
(mf/with-effect
(st/emit! (rt/assign-exception error)))
[:span "Internal application error"])
(mf/defc main-page (mf/defc main-page
{::mf/wrap [#(mf/catch % {:fallback on-main-error})] {::mf/props :obj}
::mf/props :obj}
[{:keys [route profile]}] [{:keys [route profile]}]
(let [{:keys [data params]} route] (let [{:keys [data params]} route]
[:& (mf/provider ctx/current-route) {:value route} [:& (mf/provider ctx/current-route) {:value route}
@ -137,7 +129,7 @@
{:keys [file-id]} path-params] {:keys [file-id]} path-params]
[:? {} [:? {}
(if (:token query-params) (if (:token query-params)
[:> static/error-container {} [:> static/error-container* {}
[:div.image i/detach] [:div.image i/detach]
[:div.main-message (tr "viewer.breaking-change.message")] [:div.main-message (tr "viewer.breaking-change.message")]
[:div.desc-message (tr "viewer.breaking-change.description")]] [:div.desc-message (tr "viewer.breaking-change.description")]]
@ -186,8 +178,8 @@
[:& (mf/provider ctx/current-route) {:value route} [:& (mf/provider ctx/current-route) {:value route}
[:& (mf/provider ctx/current-profile) {:value profile} [:& (mf/provider ctx/current-profile) {:value profile}
(if edata (if edata
[:& static/exception-page {:data edata :route route}] [:> static/exception-page* {:data edata :route route}]
[:* [:> error-boundary* {:fallback static/internal-error*}
[:& notifications/current-notification] [:& notifications/current-notification]
(when route (when route
[:& main-page {:route route :profile profile}])])]])) [:& main-page {:route route :profile profile}])])]]))

View file

@ -0,0 +1,47 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.error-boundary
"React error boundary components"
(:require
["react-error-boundary" :as reb]
[app.main.errors :as errors]
[app.main.refs :as refs]
[goog.functions :as gfn]
[rumext.v2 :as mf]))
(mf/defc error-boundary*
{::mf/props :obj}
[{:keys [fallback children]}]
(let [fallback-wrapper
(mf/with-memo [fallback]
(mf/fnc fallback-wrapper*
{::mf/props :obj}
[{:keys [error reset-error-boundary]}]
(let [route (mf/deref refs/route)
data (errors/exception->error-data error)]
[:> fallback {:data data
:route route
:on-reset reset-error-boundary}])))
on-error
(mf/with-memo []
;; NOTE: The debounce is necessary just for simplicity,
;; becuase for some reasons the error is reported twice in a
;; very small amount of time, so we debounce for 100ms for
;; avoid duplicate and redundant reports
(gfn/debounce (fn [error info]
(js/console.error
"Component trace: \n"
(unchecked-get info "componentStack")
"\n"
error))
100))]
[:> reb/ErrorBoundary
{:FallbackComponent fallback-wrapper
:onError on-error}
children]))

View file

@ -29,8 +29,8 @@
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc error-container (mf/defc error-container*
{::mf/wrap-props false} {::mf/props :obj}
[{:keys [children]}] [{:keys [children]}]
(let [profile-id (:profile-id @st/state)] (let [profile-id (:profile-id @st/state)]
[:section {:class (stl/css :exception-layout)} [:section {:class (stl/css :exception-layout)}
@ -57,12 +57,10 @@
(mf/defc invalid-token (mf/defc invalid-token
[] []
[:> error-container {} [:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")] [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
[:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]]) [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])
(mf/defc login-dialog (mf/defc login-dialog
{::mf/props :obj} {::mf/props :obj}
[{:keys [show-dialog]}] [{:keys [show-dialog]}]
@ -247,37 +245,49 @@
[:& request-dialog {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard")}] [:& request-dialog {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard")}]
(and (some? file-id) (:already-requested requested)) (and (some? file-id) (:already-requested requested))
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file") :content [(tr "not-found.no-permission.already-requested.or-others.file")] :button-text (tr "not-found.no-permission.go-dashboard")}] [:& request-dialog {:title (tr "not-found.no-permission.already-requested.file")
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
:button-text (tr "not-found.no-permission.go-dashboard")}]
(:already-requested requested) (:already-requested requested)
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project") :content [(tr "not-found.no-permission.already-requested.or-others.project")] :button-text (tr "not-found.no-permission.go-dashboard")}] [:& request-dialog {:title (tr "not-found.no-permission.already-requested.project")
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
:button-text (tr "not-found.no-permission.go-dashboard")}]
(:sent requested) (:sent requested)
[:& request-dialog {:title (tr "not-found.no-permission.done.success") :content [(tr "not-found.no-permission.done.remember")] :button-text (tr "not-found.no-permission.go-dashboard")}] [:& request-dialog {:title (tr "not-found.no-permission.done.success")
:content [(tr "not-found.no-permission.done.remember")]
:button-text (tr "not-found.no-permission.go-dashboard")}]
(some? file-id) (some? file-id)
[:& request-dialog {:title (tr "not-found.no-permission.file") :content [(tr "not-found.no-permission.you-can-ask.file") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard")}] [:& request-dialog {:title (tr "not-found.no-permission.file")
:content [(tr "not-found.no-permission.you-can-ask.file")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")}]
(some? team-id) (some? team-id)
[:& request-dialog {:title (tr "not-found.no-permission.project") :content [(tr "not-found.no-permission.you-can-ask.project") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard")}]))])) [:& request-dialog {:title (tr "not-found.no-permission.project")
:content [(tr "not-found.no-permission.you-can-ask.project")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")}]))]))
(mf/defc not-found*
(mf/defc not-found
[] []
[:> error-container {} [:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "labels.not-found.main-message")] [:div {:class (stl/css :main-message)} (tr "labels.not-found.main-message")]
[:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.error")] [:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.error")]
[:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.doesnt-exist")]]) [:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.doesnt-exist")]])
(mf/defc bad-gateway*
(mf/defc bad-gateway
[] []
(let [handle-retry (let [handle-retry
(mf/use-fn (mf/use-fn
(fn [] (st/emit! (rt/assign-exception nil))))] (fn [] (st/emit! (rt/assign-exception nil))))]
[:> error-container {} [:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "labels.bad-gateway.main-message")] [:div {:class (stl/css :main-message)} (tr "labels.bad-gateway.main-message")]
[:div {:class (stl/css :desc-message)} (tr "labels.bad-gateway.desc-message")] [:div {:class (stl/css :desc-message)} (tr "labels.bad-gateway.desc-message")]
[:div {:class (stl/css :sign-info)} [:div {:class (stl/css :sign-info)}
@ -286,13 +296,12 @@
(mf/defc service-unavailable (mf/defc service-unavailable
[] []
(let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil)))] (let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil)))]
[:> error-container {} [:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "labels.service-unavailable.main-message")] [:div {:class (stl/css :main-message)} (tr "labels.service-unavailable.main-message")]
[:div {:class (stl/css :desc-message)} (tr "labels.service-unavailable.desc-message")] [:div {:class (stl/css :desc-message)} (tr "labels.service-unavailable.desc-message")]
[:div {:class (stl/css :sign-info)} [:div {:class (stl/css :sign-info)}
[:button {:on-click on-click} (tr "labels.retry")]]])) [:button {:on-click on-click} (tr "labels.retry")]]]))
(defn generate-report (defn generate-report
[data] [data]
(try (try
@ -336,17 +345,16 @@
(println))] (println))]
(wapi/create-blob content "text/plain")) (wapi/create-blob content "text/plain"))
(catch :default err (catch :default cause
(.error js/console err) (.error js/console "error on generating report.txt" cause)
nil))) nil)))
(mf/defc internal-error*
(mf/defc internal-error
{::mf/props :obj} {::mf/props :obj}
[{:keys [data]}] [{:keys [data on-reset] :as props}]
(let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil))) (let [report-uri (mf/use-ref nil)
report-uri (mf/use-ref nil)
report (mf/use-memo (mf/deps data) #(generate-report data)) report (mf/use-memo (mf/deps data) #(generate-report data))
on-reset (or on-reset #(st/emit! (rt/assign-exception nil)))
on-download on-download
(mf/use-fn (mf/use-fn
@ -357,22 +365,24 @@
(mf/with-effect [report] (mf/with-effect [report]
(when (some? report) (when (some? report)
(let [uri (wapi/create-uri report)] (let [uri (wapi/create-uri report)]
(mf/set-ref-val! report-uri uri) (mf/set-ref-val! report-uri uri)
(fn [] (fn []
(wapi/revoke-uri uri))))) (wapi/revoke-uri uri)))))
[:> error-container {} [:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "labels.internal-error.main-message")] [:div {:class (stl/css :main-message)} (tr "labels.internal-error.main-message")]
[:div {:class (stl/css :desc-message)} (tr "labels.internal-error.desc-message")] [:div {:class (stl/css :desc-message)} (tr "labels.internal-error.desc-message")]
(when (some? report) (when (some? report)
[:a {:on-click on-download} "Download report.txt"]) [:a {:on-click on-download} "Download report.txt"])
[:div {:class (stl/css :sign-info)} [:div {:class (stl/css :sign-info)}
[:button {:on-click on-click} (tr "labels.retry")]]])) [:button {:on-click on-reset} (tr "labels.retry")]]]))
(mf/defc exception-page (mf/defc exception-page*
{::mf/props :obj} {::mf/props :obj}
[{:keys [data route] :as props}] [{:keys [data route] :as props}]
(let [file-info (mf/use-state {:pending true}) (let [file-info (mf/use-state {:pending true})
team-info (mf/use-state {:pending true}) team-info (mf/use-state {:pending true})
type (:type data) type (:type data)
@ -409,18 +419,24 @@
(case (:type data) (case (:type data)
:not-found :not-found
(if request-access? (if request-access?
[:& request-access {:file-id (:file-id @file-info) :team-id (:team-id @team-info) :is-default (:is-default @team-info) :workspace? workspace?}] [:& request-access {:file-id (:file-id @file-info)
[:& not-found]) :team-id (:team-id @team-info)
:is-default (:is-default @team-info)
:workspace? workspace?}]
[:> not-found* {}])
:authentication :authentication
(if request-access? (if request-access?
[:& request-access {:file-id (:file-id @file-info) :team-id (:team-id @team-info) :is-default (:is-default @team-info) :workspace? workspace?}] [:& request-access {:file-id (:file-id @file-info)
[:& not-found]) :team-id (:team-id @team-info)
:is-default (:is-default @team-info)
:workspace? workspace?}]
[:> not-found* {}])
:bad-gateway :bad-gateway
[:& bad-gateway] [:> bad-gateway* props]
:service-unavailable :service-unavailable
[:& service-unavailable] [:& service-unavailable]
[:> internal-error props]))) [:> internal-error* props])))

View file

@ -73,47 +73,40 @@
on-tab-change on-tab-change
(mf/use-fn #(st/emit! (dw/go-to-layout (keyword %)))) (mf/use-fn #(st/emit! (dw/go-to-layout (keyword %))))
tabs (if ^boolean mode-inspect? layers-tab
#js [#js {:label (tr "workspace.sidebar.layers") (mf/html
:id "layers" [:article {:class (stl/css :layers-tab)
:content (mf/html [:article {:class (stl/css :layers-tab) :style #js {"--height" (str size-pages "px")}}
:style #js {"--height" (str size-pages "px")}}
[:& sitemap {:layout layout [:& sitemap {:layout layout
:toggle-pages toggle-pages :toggle-pages toggle-pages
:show-pages? @show-pages? :show-pages? @show-pages?
:size size-pages}] :size size-pages}]
(when @show-pages? (when @show-pages?
[:div {:class (stl/css :resize-area-horiz) [:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages :on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages :on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}]) :on-pointer-move on-pointer-move-pages}])
[:& layers-toolbox {:size-parent size [:& layers-toolbox {:size-parent size
:size size-pages}]])}] :size size-pages}]])
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content (mf/html [:article {:class (stl/css :layers-tab)
:style #js {"--height" (str size-pages "px")}}
[:& sitemap {:layout layout
:toggle-pages toggle-pages
:show-pages? @show-pages?
:size size-pages}]
(when @show-pages? assets-tab
[:div {:class (stl/css :resize-area-horiz) (mf/html [:& assets-toolbox {:size (- size 58)}])
:on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}])
[:& layers-toolbox {:size-parent size
:size size-pages}]])}
#js {:label (tr "workspace.toolbar.assets")
:id "assets"
:content (mf/html [:& assets-toolbox {:size (- size 58)}])}])]
tabs
(if ^boolean mode-inspect?
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content layers-tab}]
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content layers-tab}
#js {:label (tr "workspace.toolbar.assets")
:id "assets"
:content assets-tab}])]
[:& (mf/provider muc/sidebar) {:value :left} [:& (mf/provider muc/sidebar) {:value :left}
[:aside {:ref parent-ref [:aside {:ref parent-ref

View file

@ -146,25 +146,30 @@
(st/emit! :interrupt (udw/set-workspace-read-only true)) (st/emit! :interrupt (udw/set-workspace-read-only true))
(st/emit! :interrupt (udw/set-workspace-read-only false))))) (st/emit! :interrupt (udw/set-workspace-read-only false)))))
design-content (mf/html [:& design-menu {:selected selected design-content
:objects objects (mf/html [:& design-menu {:selected selected
:page-id page-id :objects objects
:file-id file-id :page-id page-id
:selected-shapes selected-shapes :file-id file-id
:shapes-with-children shapes-with-children}]) :selected-shapes selected-shapes
:shapes-with-children shapes-with-children}])
inspect-content
(mf/html [:div {:class (stl/css :element-options :inspect-options)}
[:& hrs/right-sidebar {:page-id page-id
:objects objects
:file-id file-id
:frame shape-parent-frame
:shapes selected-shapes
:on-change-section on-change-section
:on-expand on-expand
:from :workspace}]])
interactions-content
(mf/html [:div {:class (stl/css :element-options :interaction-options)}
[:& interactions-menu {:shape (first shapes)}]])
inspect-content (mf/html [:div {:class (stl/css :element-options :inspect-options)}
[:& hrs/right-sidebar {:page-id page-id
:objects objects
:file-id file-id
:frame shape-parent-frame
:shapes selected-shapes
:on-change-section on-change-section
:on-expand on-expand
:from :workspace}]])
interactions-content (mf/html [:div {:class (stl/css :element-options :interaction-options)}
[:& interactions-menu {:shape (first shapes)}]])
tabs tabs
#js [#js {:label (tr "workspace.options.design") #js [#js {:label (tr "workspace.options.design")
:id "design" :id "design"

View file

@ -1738,6 +1738,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.12.5":
version: 7.25.0
resolution: "@babel/runtime@npm:7.25.0"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: 10c0/bd3faf246170826cef2071a94d7b47b49d532351360ecd17722d03f6713fd93a3eb3dbd9518faa778d5e8ccad7392a7a604e56bd37aaad3f3aa68d619ccd983d
languageName: node
linkType: hard
"@babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": "@babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7":
version: 7.24.5 version: 7.24.5
resolution: "@babel/runtime@npm:7.24.5" resolution: "@babel/runtime@npm:7.24.5"
@ -6657,6 +6666,7 @@ __metadata:
randomcolor: "npm:^0.6.2" randomcolor: "npm:^0.6.2"
react: "npm:18.3.1" react: "npm:18.3.1"
react-dom: "npm:18.3.1" react-dom: "npm:18.3.1"
react-error-boundary: "npm:^4.0.13"
react-virtualized: "npm:^9.22.5" react-virtualized: "npm:^9.22.5"
rimraf: "npm:^5.0.7" rimraf: "npm:^5.0.7"
rxjs: "npm:8.0.0-alpha.14" rxjs: "npm:8.0.0-alpha.14"
@ -10690,6 +10700,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-error-boundary@npm:^4.0.13":
version: 4.0.13
resolution: "react-error-boundary@npm:4.0.13"
dependencies:
"@babel/runtime": "npm:^7.12.5"
peerDependencies:
react: ">=16.13.1"
checksum: 10c0/6f3e0e4d7669f680ccf49c08c9571519c6e31f04dcfc30a765a7136c7e6fbbbe93423dd5a9fce12107f8166e54133e9dd5c2079a00c7a38201ac811f7a28b8e7
languageName: node
linkType: hard
"react-is@npm:18.1.0": "react-is@npm:18.1.0":
version: 18.1.0 version: 18.1.0
resolution: "react-is@npm:18.1.0" resolution: "react-is@npm:18.1.0"