diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 7182905a6..042e16d0e 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -8,6 +8,8 @@ ENV NODE_VERSION=v20.11.1 \ CLJKONDO_VERSION=2024.03.13 \ BABASHKA_VERSION=1.3.189 \ CLJFMT_VERSION=0.12.0 \ + RUSTUP_VERSION=1.27.1 \ + RUST_VERSION=1.81.0 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 @@ -242,6 +244,27 @@ RUN set -ex; \ mv /tmp/mc /usr/local/bin/; \ chmod +x /usr/local/bin/mc; +# Install Rust toolchain +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH; + +RUN set -eux; \ + # Same steps as in Rust official Docker image https://github.com/rust-lang/docker-rust/blob/9f287282d513a84cb7c7f38f197838f15d37b6a9/1.81.0/bookworm/Dockerfile + dpkgArch="$(dpkg --print-architecture)"; \ + case "${dpkgArch##*-}" in \ + amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d' ;; \ + arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='1cffbf51e63e634c746f741de50649bbbcbd9dbe1de363c9ecef64e278dba2b2' ;; \ + *) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \ + esac; \ + url="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustArch}/rustup-init"; \ + wget "$url"; \ + echo "${rustupSha256} *rustup-init" | sha256sum -c -; \ + chmod +x rustup-init; \ + ./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host ${rustArch}; \ + rm rustup-init; \ + chmod -R a+w $RUSTUP_HOME $CARGO_HOME; + WORKDIR /home COPY files/nginx.conf /etc/nginx/nginx.conf diff --git a/docker/devenv/files/bashrc b/docker/devenv/files/bashrc index 745e3f901..dca37aadb 100644 --- a/docker/devenv/files/bashrc +++ b/docker/devenv/files/bashrc @@ -9,6 +9,9 @@ alias ls='ls --color -F' alias lsd='ls -d *(/)' alias lsf='ls -h *(.)' +# init Cargo / Rust env +. "/usr/local/cargo/env" + # include .bashrc if it exists if [ -f "$HOME/.bashrc.local" ]; then . "$HOME/.bashrc.local" diff --git a/frontend/package.json b/frontend/package.json index 1612361ab..5141d5ec8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "build:storybook": "yarn run build:storybook:assets && yarn run build:storybook:cljs && storybook build", "build:storybook:assets": "node ./scripts/build-storybook-assets.js", "build:storybook:cljs": "clojure -M:dev:shadow-cljs release storybook", + "build:renderer": "yarn run wasm-pack build ./renderer --target web --out-dir ../resources/public/js/renderer --release", "e2e:server": "node ./scripts/e2e-server.js", "e2e:test": "playwright test --project default", "fmt:clj": "cljfmt fix --parallel=true src/ test/", @@ -86,6 +87,7 @@ "typescript": "^5.4.5", "vite": "^5.1.4", "vitest": "^1.3.1", + "wasm-pack": "^0.13.0", "watcher": "^2.3.1", "workerpool": "^9.1.1" }, diff --git a/frontend/renderer/.gitignore b/frontend/renderer/.gitignore new file mode 100644 index 000000000..391ed4d66 --- /dev/null +++ b/frontend/renderer/.gitignore @@ -0,0 +1,5 @@ +target/ +debug/ + +**/*.rs.bk + diff --git a/frontend/renderer/Cargo.lock b/frontend/renderer/Cargo.lock new file mode 100644 index 000000000..c14faa4ad --- /dev/null +++ b/frontend/renderer/Cargo.lock @@ -0,0 +1,324 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cc" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "minicov" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "renderer" +version = "0.1.0" +dependencies = [ + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "minicov", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/frontend/renderer/Cargo.toml b/frontend/renderer/Cargo.toml new file mode 100644 index 000000000..56724cc1a --- /dev/null +++ b/frontend/renderer/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "renderer" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/penpot/penpot" +license-file = "../../../../LICENSE" +description = "Wasm-based canvas renderer for Penpot" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2.93" + +[profile.release] +opt-level = "s" + +[dev-dependencies] +wasm-bindgen-test = "0.3.43" diff --git a/frontend/renderer/src/lib.rs b/frontend/renderer/src/lib.rs new file mode 100644 index 000000000..e4b08cfb0 --- /dev/null +++ b/frontend/renderer/src/lib.rs @@ -0,0 +1,36 @@ +use wasm_bindgen::prelude::*; + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); +} + +#[wasm_bindgen] +pub fn print(msg: &str) { + log(msg); +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } + + #[wasm_bindgen_test] + fn it_works_in_wasm() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/frontend/resources/polyfills/dynamicImport.js b/frontend/resources/polyfills/dynamicImport.js new file mode 100644 index 000000000..7e354e13c --- /dev/null +++ b/frontend/resources/polyfills/dynamicImport.js @@ -0,0 +1,5 @@ +if (!('dynamicImport' in window)) { + window.dynamicImport = function(uri) { + return import(uri); + } +}; diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index e6ea22308..f6a388de1 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -74,6 +74,7 @@ [app.main.repo :as rp] [app.main.streams :as ms] [app.main.worker :as uw] + [app.renderer-v2 :as renderer] [app.util.dom :as dom] [app.util.globals :as ug] [app.util.http :as http] @@ -352,6 +353,9 @@ (dcm/retrieve-comment-threads file-id) (fetch-bundle project-id file-id)) + (when (contains? cf/flags :renderer-v2) + (rx/of (renderer/init))) + (->> stream (rx/filter dch/commit?) (rx/map deref) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index e113096cf..a284ec28e 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -8,6 +8,7 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] + [app.config :as cf] [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] [app.main.data.persistence :as dps] @@ -31,6 +32,7 @@ [app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox]] [app.main.ui.workspace.viewport :refer [viewport]] + [app.renderer-v2 :as renderer] [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.globals :as globals] @@ -198,6 +200,10 @@ (ntf/hide) (dw/finalize-file project-id file-id)))) + (mf/with-effect [file-ready?] + (when (and file-ready? (contains? cf/flags :renderer-v2)) + (renderer/print-msg "hello from wasm fn!"))) + [:& (mf/provider ctx/current-file-id) {:value file-id} [:& (mf/provider ctx/current-project-id) {:value project-id} [:& (mf/provider ctx/current-team-id) {:value team-id} @@ -208,7 +214,6 @@ :style {:background-color background-color :touch-action "none"}} [:& context-menu] - (if ^boolean file-ready? [:& workspace-page {:page-id page-id :file file diff --git a/frontend/src/app/renderer_v2.cljs b/frontend/src/app/renderer_v2.cljs new file mode 100644 index 000000000..10509cf8e --- /dev/null +++ b/frontend/src/app/renderer_v2.cljs @@ -0,0 +1,38 @@ +;; 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.renderer-v2 + (:require + [app.config :as cf] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +(defonce internal-module #js {}) + +(defn on-module-loaded + [module'] + (let [init-fn (.-default ^js module')] + (->> (rx/from (init-fn)) + (rx/map (constantly module'))))) + +(defn- on-module-initialized + [module] + (set! internal-module module)) + +(defn print-msg [msg] + (let [print-fn (.-print internal-module)] + (print-fn msg))) + +(defn init + [] + (ptk/reify ::init + ptk/WatchEvent + (watch [_ _ _] + (let [module-uri (assoc cf/public-uri :path "/js/renderer/renderer.js")] + (->> (rx/from (js/dynamicImport (str module-uri))) + (rx/mapcat on-module-loaded) + (rx/tap on-module-initialized) + (rx/ignore)))))) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 43a1f3c3e..601bb7c6c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3871,6 +3871,15 @@ __metadata: languageName: node linkType: hard +"axios@npm:^0.26.1": + version: 0.26.1 + resolution: "axios@npm:0.26.1" + dependencies: + follow-redirects: "npm:^1.14.8" + checksum: 10c0/77ad7f1e6ca04fcd3fa8af1795b09d8b7c005b71a31f28d99ba40cda0bdcc12a4627801d7fac5efa62b9f667a8402bd54c669039694373bc8d44f6be611f785c + languageName: node + linkType: hard + "babel-core@npm:^7.0.0-bridge.0": version: 7.0.0-bridge.0 resolution: "babel-core@npm:7.0.0-bridge.0" @@ -3976,6 +3985,17 @@ __metadata: languageName: node linkType: hard +"binary-install@npm:^1.0.1": + version: 1.1.0 + resolution: "binary-install@npm:1.1.0" + dependencies: + axios: "npm:^0.26.1" + rimraf: "npm:^3.0.2" + tar: "npm:^6.1.11" + checksum: 10c0/c0c94a81262c037a1a84f12ff9acfe667b7938b126e764b0f066d5be128d21e0bb8ac5700f4d89f8f7b860b660882deddeaca300dea0ff218d94676999a133a1 + languageName: node + linkType: hard + "bindings@npm:^1.5.0": version: 1.5.0 resolution: "bindings@npm:1.5.0" @@ -6537,6 +6557,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.14.8": + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" + peerDependenciesMeta: + debug: + optional: true + checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -6683,6 +6713,7 @@ __metadata: ua-parser-js: "npm:^1.0.38" vite: "npm:^5.1.4" vitest: "npm:^1.3.1" + wasm-pack: "npm:^0.13.0" watcher: "npm:^2.3.1" workerpool: "npm:^9.1.1" xregexp: "npm:^5.1.1" @@ -11176,6 +11207,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 + languageName: node + linkType: hard + "rimraf@npm:^5.0.7": version: 5.0.7 resolution: "rimraf@npm:5.0.7" @@ -13577,6 +13619,17 @@ __metadata: languageName: node linkType: hard +"wasm-pack@npm:^0.13.0": + version: 0.13.0 + resolution: "wasm-pack@npm:0.13.0" + dependencies: + binary-install: "npm:^1.0.1" + bin: + wasm-pack: run.js + checksum: 10c0/71ed64c9b0082d51098ec71041ce68a9323d7a0027e3a9c0b694c5931f83ce2a58f1df7255c68239ca4ab702e2daf5c550a7886f8af048f0cb76945a510268b6 + languageName: node + linkType: hard + "watcher@npm:^2.3.1": version: 2.3.1 resolution: "watcher@npm:2.3.1"