diff --git a/.circleci/config.yml b/.circleci/config.yml index 5228a7a5..9742efd9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,9 +1,8 @@ -version: 2 +version: 2.1 jobs: remix-plugin: docker: - - image: circleci/node:12 - environment: + - image: cimg/node:14.17.6-browsers working_directory: ~/repo steps: - checkout diff --git a/.github/workflows/rebase-pull-requests.yml b/.github/workflows/rebase-pull-requests.yml new file mode 100644 index 00000000..2a247041 --- /dev/null +++ b/.github/workflows/rebase-pull-requests.yml @@ -0,0 +1,11 @@ +name: Rebase Pull Requests +on: + push: + branches: [master] + workflow_dispatch: + +jobs: + rebase: + runs-on: ubuntu-latest + steps: + - uses: yann300/rebase-pull-requests@master \ No newline at end of file diff --git a/nx.json b/nx.json index d390d2c0..831e2ca3 100644 --- a/nx.json +++ b/nx.json @@ -89,6 +89,12 @@ }, "engine-theia": { "tags": [] + }, + "engine-electron": { + "tags": [] + }, + "plugin-electron": { + "tags": [] } }, "workspaceLayout": { diff --git a/package-lock.json b/package-lock.json index 78019291..05151082 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "remix-plugin", - "version": "0.3.5", + "version": "0.3.29", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3018,7 +3018,6 @@ "version": "7.11.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" }, @@ -3026,8 +3025,7 @@ "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" } } }, @@ -3278,6 +3276,38 @@ "lodash.once": "^4.1.1" } }, + "@erebos/bzz": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@erebos/bzz/-/bzz-0.13.0.tgz", + "integrity": "sha512-ETjXxeNzT7wGofz0CcrNEc/dLeLg0DALuxpMymrzK+AvLvP8PZUfiFn+tZoupSMGaLldfSLJXweOfs3BimVaRg==", + "requires": { + "@babel/runtime": "^7.8.3" + } + }, + "@erebos/bzz-node": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@erebos/bzz-node/-/bzz-node-0.13.0.tgz", + "integrity": "sha512-Mmo9awJG/Agj6lPqicj8VRdUELoT9pP2xIVniaoUqIMMZkf+lswXFylkyH578ZCNaehyZTTttaXS5WA+T9UVyA==", + "requires": { + "@babel/runtime": "^7.8.3", + "@erebos/bzz": "^0.13.0", + "form-data": "^3.0.0", + "node-fetch": "^2.6.0", + "tar-stream": "^2.1.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@eslint/eslintrc": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", @@ -7373,6 +7403,15 @@ "yargs-parser": "20.0.0" }, "dependencies": { + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "dev": true, + "requires": { + "follow-redirects": "1.5.10" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -7418,6 +7457,15 @@ "path-exists": "^4.0.0" } }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dev": true, + "requires": { + "debug": "=3.1.0" + } + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -7713,6 +7761,44 @@ "@types/node": ">= 8" } }, + "@remix-project/remix-url-resolver": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@remix-project/remix-url-resolver/-/remix-url-resolver-0.0.45.tgz", + "integrity": "sha512-B61NrKQF4FQVoui6uqsu0H+r3ma3+3FzAfPo5tj77FvJlq08B5I+eCclr5S8nUmwg3b/oR1Yqu1yqr+FH/cpuA==", + "requires": { + "@erebos/bzz-node": "^0.13.0", + "axios": "1.2.2", + "url": "^0.11.0", + "valid-url": "^1.0.9" + }, + "dependencies": { + "axios": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz", + "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@rollup/plugin-babel": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.0.2.tgz", @@ -9000,8 +9086,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -9043,23 +9128,11 @@ "dev": true }, "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "dev": true, + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", "requires": { - "follow-redirects": "1.5.10" - }, - "dependencies": { - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "dev": true, - "requires": { - "debug": "=3.1.0" - } - } + "follow-redirects": "^1.10.0" } }, "axobject-query": { @@ -9451,8 +9524,7 @@ "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "batch": { "version": "0.6.1", @@ -9497,6 +9569,37 @@ "file-uri-to-path": "1.0.0" } }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -10510,7 +10613,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -12104,8 +12206,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", @@ -12438,7 +12539,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -13584,8 +13684,7 @@ "follow-redirects": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", - "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", - "dev": true + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" }, "for-in": { "version": "1.0.2", @@ -13768,6 +13867,11 @@ "readable-stream": "^2.0.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -14515,7 +14619,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "gzip-size": { "version": "5.1.1", @@ -15084,8 +15189,7 @@ "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "iferr": { "version": "0.1.5", @@ -15252,8 +15356,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.7", @@ -18875,14 +18978,12 @@ "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "dev": true + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" }, "mime-types": { "version": "2.1.27", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "dev": true, "requires": { "mime-db": "1.44.0" } @@ -19302,8 +19403,7 @@ "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, "node-fetch-npm": { "version": "2.0.4", @@ -19413,6 +19513,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -19427,6 +19528,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -19436,6 +19538,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -19445,6 +19548,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -19453,13 +19557,15 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "dev": true, + "optional": true }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -19468,7 +19574,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -20044,7 +20151,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -21590,6 +21696,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -21688,8 +21799,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "querystring-es3": { "version": "0.2.1", @@ -22727,8 +22837,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-identifier": { "version": "0.4.2", @@ -23214,7 +23323,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -23957,7 +24067,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -24297,6 +24406,30 @@ } } }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", @@ -25245,7 +25378,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, "requires": { "punycode": "1.3.2", "querystring": "0.2.0" @@ -25254,8 +25386,7 @@ "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" } } }, @@ -25295,8 +25426,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util-promisify": { "version": "2.1.0", @@ -25348,6 +25478,11 @@ "source-map": "^0.7.3" } }, + "valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -26534,8 +26669,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", diff --git a/package.json b/package.json index e176f561..e5d82ea0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "remix-plugin", - "version": "0.3.8", + "version": "0.3.37", "license": "MIT", "scripts": { "nx": "nx", @@ -44,6 +44,7 @@ "@nrwl/node": "10.3.0", "@nrwl/web": "10.3.0", "@nrwl/workspace": "10.3.0", + "@types/electron": "^1.6.10", "@types/events": "^3.0.0", "@types/jest": "26.0.8", "@types/node": "^14.0.23", @@ -76,6 +77,8 @@ "@angular/platform-browser": "^10.1.0", "@angular/platform-browser-dynamic": "^10.1.0", "@angular/router": "^10.1.0", + "@remix-project/remix-url-resolver": "latest", + "axios": "^0.21.1", "rxjs": "~6.5.5", "zone.js": "^0.10.2" } diff --git a/packages/api/README.md b/packages/api/README.md index c24a781d..aa45fb0d 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -17,4 +17,5 @@ _Click on the name of the api to get the full documentation._ |Unit Testing |[solidityUnitTesting](./doc/unit-testing.md) |Unit testing library in solidity |Settings |[settings](./doc/settings.md) |Global settings of the IDE |Content Import |[contentImport](./doc/content-import.md) |Import files from github, swarm, ipfs, http or https. +|Terminal |[terminal](./doc/terminal.md) |Log to the terminal diff --git a/packages/api/doc/terminal.md b/packages/api/doc/terminal.md new file mode 100644 index 00000000..d49d5984 --- /dev/null +++ b/packages/api/doc/terminal.md @@ -0,0 +1,20 @@ +# File System + +- Name in Remix: `terminal` + +|Type |Name |Description | +|---------|-----------------------|------------| +|_method_ |`log` |Log text to the terminal + +## Examples + +### Methods +`log`: Get the name of the current file selected. +```typescript +await client.terminal.log({ type: 'info', value: 'I am a string' }) +// OR +await client.call('terminal',{ type: 'info', value: 'I am a string' }) +``` + + +> Type Definitions can be found [here](../src/lib/terminal/api.ts) diff --git a/packages/api/package.json b/packages/api/package.json index 17cb2f84..224edc7e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-api", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/api#readme", "repository": { "type": "git", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 4206f398..c7207cad 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,7 +1,9 @@ export * from './lib/compiler' export * from './lib/content-import' export * from './lib/editor' -export * from './lib/file-system' +export * from './lib/file-system/file-manager' +export * from './lib/file-system/file-panel' +export * from './lib/dgit' export * from './lib/git' export * from './lib/network' export * from './lib/plugin-manager' diff --git a/packages/api/src/lib/compiler/profile.ts b/packages/api/src/lib/compiler/profile.ts index aa8e3046..7b170bf8 100644 --- a/packages/api/src/lib/compiler/profile.ts +++ b/packages/api/src/lib/compiler/profile.ts @@ -2,7 +2,7 @@ import { ICompiler } from './api' import { LibraryProfile } from '@remixproject/plugin-utils' export const compilerProfile: LibraryProfile = { - name: 'compiler', - methods: ['compile', 'getCompilationResult'], + name: 'solidity', + methods: ['compile', 'getCompilationResult', 'compileWithParameters', 'setCompilerConfig'], events: ['compilationFinished'] } diff --git a/packages/api/src/lib/content-import/profile.ts b/packages/api/src/lib/content-import/profile.ts index 690124a3..7f34ed92 100644 --- a/packages/api/src/lib/content-import/profile.ts +++ b/packages/api/src/lib/content-import/profile.ts @@ -3,5 +3,5 @@ import { LibraryProfile } from '@remixproject/plugin-utils' export const contentImportProfile: LibraryProfile = { name: 'contentImport', - methods: ['resolve'], + methods: ['resolve','resolveAndSave'], } diff --git a/packages/api/src/lib/dgit/api.ts b/packages/api/src/lib/dgit/api.ts new file mode 100644 index 00000000..5e4e4c1f --- /dev/null +++ b/packages/api/src/lib/dgit/api.ts @@ -0,0 +1,32 @@ +import { StatusEvents } from "@remixproject/plugin-utils"; +export interface IDgitSystem { + events: StatusEvents + methods: { + init(): void; + add(cmd: any): string; + commit(cmd: any): string; + status(cmd: any): any[]; + rm(cmd: any): string; + log(cmd: any): any[]; + lsfiles(cmd: any): any[]; + readblob(cmd: any): { oid: string, blob: Uint8Array } + resolveref(cmd: any): string + branch(cmd: any): void + checkout(cmd: any): void + branches(): string[] + currentbranch(): string + push(cmd: any): string + pull(cmd: any): void + setIpfsConfig(config:any): boolean + zip():void + setItem(name:string, content:string):void + getItem(name: string): string + import(cmd: any): void + export(cmd: any): void + remotes(): any[] + addremote(cmd: any): void + delremote(cmd: any): void + clone(cmd: any): void + localStorageUsed(): any + }; +} diff --git a/packages/api/src/lib/dgit/index.ts b/packages/api/src/lib/dgit/index.ts new file mode 100644 index 00000000..35157069 --- /dev/null +++ b/packages/api/src/lib/dgit/index.ts @@ -0,0 +1,2 @@ +export * from './api' +export * from './profile' diff --git a/packages/api/src/lib/dgit/profile.ts b/packages/api/src/lib/dgit/profile.ts new file mode 100644 index 00000000..477692f9 --- /dev/null +++ b/packages/api/src/lib/dgit/profile.ts @@ -0,0 +1,7 @@ +import { IDgitSystem } from './api' +import { LibraryProfile } from '@remixproject/plugin-utils' + +export const dGitProfile: LibraryProfile = { + name: 'dGitProvider', + methods: ['clone', 'addremote', 'delremote', 'remotes', 'init', 'status', 'log', 'commit', 'add', 'rm', 'lsfiles', 'readblob', 'resolveref', 'branch', 'branches','checkout','currentbranch', 'zip', 'push', 'pull', 'setIpfsConfig','getItem','setItem', 'localStorageUsed'] +} diff --git a/packages/api/src/lib/editor/api.ts b/packages/api/src/lib/editor/api.ts index 8acdb1d7..e9b6a31d 100644 --- a/packages/api/src/lib/editor/api.ts +++ b/packages/api/src/lib/editor/api.ts @@ -1,18 +1,21 @@ import { HighlightPosition, Annotation } from './type' import { StatusEvents } from '@remixproject/plugin-utils' +import { HighLightOptions } from '@remixproject/plugin-api' export interface IEditor { - events: {} & StatusEvents + events: StatusEvents methods: { highlight( position: HighlightPosition, filePath: string, hexColor: string, + opt?: HighLightOptions ): void discardHighlight(): void discardHighlightAt(line: number, filePath: string): void addAnnotation(annotation: Annotation): void clearAnnotations(): void + gotoLine(line:number, col:number): void } } diff --git a/packages/api/src/lib/editor/profile.ts b/packages/api/src/lib/editor/profile.ts index d9b5d972..a84db989 100644 --- a/packages/api/src/lib/editor/profile.ts +++ b/packages/api/src/lib/editor/profile.ts @@ -3,5 +3,5 @@ import { LibraryProfile } from '@remixproject/plugin-utils' export const editorProfile: LibraryProfile = { name: 'editor', - methods: ['discardHighlight', 'highlight'], + methods: ['discardHighlight', 'highlight', 'addAnnotation', 'clearAnnotations', 'discardHighlightAt', 'gotoLine'], } diff --git a/packages/api/src/lib/editor/type.ts b/packages/api/src/lib/editor/type.ts index 00fb6504..bfb2eb19 100644 --- a/packages/api/src/lib/editor/type.ts +++ b/packages/api/src/lib/editor/type.ts @@ -9,6 +9,10 @@ export interface HighlightPosition { } } +export interface HighLightOptions { + focus: boolean +} + export interface Annotation { row: number; column: number; diff --git a/packages/api/src/lib/file-system/api.ts b/packages/api/src/lib/file-system/file-manager/api.ts similarity index 91% rename from packages/api/src/lib/file-system/api.ts rename to packages/api/src/lib/file-system/file-manager/api.ts index 729737c1..54961e7c 100644 --- a/packages/api/src/lib/file-system/api.ts +++ b/packages/api/src/lib/file-system/file-manager/api.ts @@ -6,6 +6,7 @@ export interface IFileSystem { currentFileChanged: (file: string) => void fileSaved: (file: string) => void fileAdded: (file: string) => void + folderAdded: (file: string) => void fileRemoved: (file: string) => void fileClosed: (file: string) => void noFileSelected: ()=> void @@ -30,6 +31,10 @@ export interface IFileSystem { remove(path: string): void /** Get the name of the file currently focused if any */ getCurrentFile(): string + /** close all files */ + closeAllFiles(): void + /** close a file */ + closeFile(): void // Old API /** @deprecated Use readdir */ getFolder(path: string): Folder diff --git a/packages/api/src/lib/file-system/index.ts b/packages/api/src/lib/file-system/file-manager/index.ts similarity index 100% rename from packages/api/src/lib/file-system/index.ts rename to packages/api/src/lib/file-system/file-manager/index.ts diff --git a/packages/api/src/lib/file-system/profile.ts b/packages/api/src/lib/file-system/file-manager/profile.ts similarity index 79% rename from packages/api/src/lib/file-system/profile.ts rename to packages/api/src/lib/file-system/file-manager/profile.ts index fd8ecdcb..13ba0ced 100644 --- a/packages/api/src/lib/file-system/profile.ts +++ b/packages/api/src/lib/file-system/file-manager/profile.ts @@ -22,5 +22,9 @@ export const filSystemProfile: Profile & LocationProfile = { "copyFile", "mkdir", "readdir", + "closeAllFiles", + "closeFile", + "remove", ], + events: ['currentFileChanged', 'fileAdded', 'fileClosed', 'fileRemoved', 'fileRenamed', 'fileSaved', 'noFileSelected', 'folderAdded'] }; \ No newline at end of file diff --git a/packages/api/src/lib/file-system/type.ts b/packages/api/src/lib/file-system/file-manager/type.ts similarity index 100% rename from packages/api/src/lib/file-system/type.ts rename to packages/api/src/lib/file-system/file-manager/type.ts diff --git a/packages/api/src/lib/file-system/file-panel/api.ts b/packages/api/src/lib/file-system/file-panel/api.ts new file mode 100644 index 00000000..c2d4934c --- /dev/null +++ b/packages/api/src/lib/file-system/file-panel/api.ts @@ -0,0 +1,19 @@ +import { StatusEvents } from '@remixproject/plugin-utils' +import { customAction } from './type'; +export interface IFilePanel { + events: { + setWorkspace: (workspace:any) => void + workspaceRenamed: (workspace:any) => void + workspaceDeleted: (workspace:any) => void + workspaceCreated: (workspace:any) => void + customAction: (cmd: customAction) => void + } & StatusEvents + methods: { + getCurrentWorkspace(): { name: string, isLocalhost: boolean, absolutePath: string } + getWorkspaces(): string[] + deleteWorkspace(name:string): void + createWorkspace(name:string, isEmpty:boolean): void + renameWorkspace(oldName:string, newName:string): void + registerContextMenuItem(cmd: customAction): void + } +} diff --git a/packages/api/src/lib/file-system/file-panel/index.ts b/packages/api/src/lib/file-system/file-panel/index.ts new file mode 100644 index 00000000..286140e4 --- /dev/null +++ b/packages/api/src/lib/file-system/file-panel/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './profile' +export * from './type' \ No newline at end of file diff --git a/packages/api/src/lib/file-system/file-panel/profile.ts b/packages/api/src/lib/file-system/file-panel/profile.ts new file mode 100644 index 00000000..c639eaf8 --- /dev/null +++ b/packages/api/src/lib/file-system/file-panel/profile.ts @@ -0,0 +1,13 @@ +import { IFilePanel as IFilePanel } from './api' +import { LocationProfile, Profile } from '@remixproject/plugin-utils' + +export const filePanelProfile: Profile & LocationProfile = { + name: "filePanel", + displayName: "File explorers", + description: "Provides communication between remix file explorers and remix-plugin", + location: "sidePanel", + documentation: "", + version: "0.0.1", + methods: ['getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'], + events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'], +}; \ No newline at end of file diff --git a/packages/api/src/lib/file-system/file-panel/type.ts b/packages/api/src/lib/file-system/file-panel/type.ts new file mode 100644 index 00000000..9d4ae5bd --- /dev/null +++ b/packages/api/src/lib/file-system/file-panel/type.ts @@ -0,0 +1,12 @@ +export interface customAction { + id: string, + name: string, + type: customActionType[], + path: string[], + extension: string[], + pattern: string[], + sticky?: boolean, + label?: string +} + +export type customActionType = 'file' | 'folder' \ No newline at end of file diff --git a/packages/api/src/lib/plugin-manager/profile.ts b/packages/api/src/lib/plugin-manager/profile.ts index db2255c3..4d10b6df 100644 --- a/packages/api/src/lib/plugin-manager/profile.ts +++ b/packages/api/src/lib/plugin-manager/profile.ts @@ -2,6 +2,7 @@ import { IPluginManager } from './api' import { LibraryProfile } from '@remixproject/plugin-utils' export const pluginManagerProfile: LibraryProfile & { name: 'manager' } = { - name: 'manager' as 'manager', - methods: ['getProfile', 'updateProfile', 'activatePlugin', 'deactivatePlugin', 'isActive', 'canCall'] + name: 'manager', + methods: ['getProfile', 'updateProfile', 'activatePlugin', 'deactivatePlugin', 'isActive', 'canCall'], + events: ['pluginActivated', 'pluginDeactivated', 'profileAdded', 'profileUpdated'] } diff --git a/packages/api/src/lib/remix-profile.ts b/packages/api/src/lib/remix-profile.ts index 8a67f885..49440bac 100644 --- a/packages/api/src/lib/remix-profile.ts +++ b/packages/api/src/lib/remix-profile.ts @@ -1,6 +1,6 @@ import { ProfileMap, Profile } from '@remixproject/plugin-utils' import { compilerProfile, ICompiler } from './compiler' -import { filSystemProfile, IFileSystem } from './file-system' +import { filSystemProfile, IFileSystem } from './file-system/file-manager' import { editorProfile, IEditor } from './editor' import { networkProfile, INetwork } from './network' import { udappProfile, IUdapp } from './udapp' @@ -9,12 +9,18 @@ import { unitTestProfile, IUnitTesting } from './unit-testing' import { contentImportProfile, IContentImport } from './content-import' import { ISettings, settingsProfile } from './settings' import { gitProfile, IGitSystem } from './git'; +import { IVScodeExtAPI, vscodeExtProfile } from './vscextapi'; import { IPluginManager, pluginManagerProfile } from './plugin-manager' +import { filePanelProfile, IFilePanel } from './file-system/file-panel' +import { dGitProfile, IDgitSystem } from './dgit' +import { ITerminal, terminalProfile } from './terminal' export interface IRemixApi { manager: IPluginManager, solidity: ICompiler fileManager: IFileSystem + filePanel: IFilePanel + dGitProvider: IDgitSystem solidityUnitTesting: IUnitTesting editor: IEditor network: INetwork @@ -22,6 +28,8 @@ export interface IRemixApi { contentImport: IContentImport settings: ISettings theme: ITheme + vscodeExtAPI: IVScodeExtAPI + terminal: ITerminal } export type RemixApi = Readonly @@ -31,6 +39,8 @@ export const remixApi: ProfileMap = Object.freeze({ manager: pluginManagerProfile, solidity: { ...compilerProfile, name: 'solidity' } as Profile, fileManager: { ...filSystemProfile, name: 'fileManager' } as Profile, + dGitProvider: dGitProfile, + filePanel: filePanelProfile, solidityUnitTesting: { ...unitTestProfile, name: 'solidityUnitTesting' } as Profile, editor: editorProfile, network: networkProfile, @@ -38,6 +48,8 @@ export const remixApi: ProfileMap = Object.freeze({ contentImport: contentImportProfile, settings: settingsProfile, theme: themeProfile, + vscodeExtAPI: vscodeExtProfile, + terminal: terminalProfile }) /** Profiles of all the remix's Native Plugins */ @@ -46,11 +58,15 @@ export const remixProfiles: ProfileMap = Object.freeze({ solidity: { ...compilerProfile, name: 'solidity' } as Profile, fileManager: { ...filSystemProfile, name: 'fileManager' } as Profile, git: { ...gitProfile, name: 'git' } as Profile, + dGitProvider: dGitProfile, + filePanel: filePanelProfile, solidityUnitTesting: { ...unitTestProfile, name: 'solidityUnitTesting' } as Profile, editor: editorProfile, network: networkProfile, udapp: udappProfile, contentImport: contentImportProfile, settings: settingsProfile, - theme: themeProfile + theme: themeProfile, + vscodeExtAPI: vscodeExtProfile, + terminal: terminalProfile }) diff --git a/packages/api/src/lib/standard-profile.ts b/packages/api/src/lib/standard-profile.ts index ae8458b5..a5443e1b 100644 --- a/packages/api/src/lib/standard-profile.ts +++ b/packages/api/src/lib/standard-profile.ts @@ -1,6 +1,6 @@ import { ProfileMap, Profile, ApiMap } from '@remixproject/plugin-utils' import { compilerProfile, ICompiler } from './compiler' -import { filSystemProfile, IFileSystem } from './file-system' +import { filSystemProfile, IFileSystem } from './file-system/file-manager' import { editorProfile, IEditor } from './editor' import { networkProfile, INetwork } from './network' import { udappProfile, IUdapp } from './udapp' diff --git a/packages/api/src/lib/terminal/api.ts b/packages/api/src/lib/terminal/api.ts new file mode 100644 index 00000000..7e19f7df --- /dev/null +++ b/packages/api/src/lib/terminal/api.ts @@ -0,0 +1,9 @@ +import { StatusEvents } from '@remixproject/plugin-utils' +import { TerminalMessage } from './type'; +export interface ITerminal { + events: { + } & StatusEvents + methods: { + log(message: TerminalMessage): void + } +} diff --git a/packages/api/src/lib/terminal/index.ts b/packages/api/src/lib/terminal/index.ts new file mode 100644 index 00000000..ceeeb68f --- /dev/null +++ b/packages/api/src/lib/terminal/index.ts @@ -0,0 +1,3 @@ +export * from './api' +export * from './type' +export * from './profile' \ No newline at end of file diff --git a/packages/api/src/lib/terminal/profile.ts b/packages/api/src/lib/terminal/profile.ts new file mode 100644 index 00000000..2bc02c1d --- /dev/null +++ b/packages/api/src/lib/terminal/profile.ts @@ -0,0 +1,7 @@ +import { ITerminal } from './api' +import { LibraryProfile } from '@remixproject/plugin-utils' + +export const terminalProfile: LibraryProfile = { + name: 'terminal', + methods: ['log'], +} diff --git a/packages/api/src/lib/terminal/type.ts b/packages/api/src/lib/terminal/type.ts new file mode 100644 index 00000000..4429116e --- /dev/null +++ b/packages/api/src/lib/terminal/type.ts @@ -0,0 +1,4 @@ +export type TerminalMessage = { + value: any, + type: 'html' | 'log' | 'info' | 'warn' | 'error' +} \ No newline at end of file diff --git a/packages/api/src/lib/udapp/profile.ts b/packages/api/src/lib/udapp/profile.ts index 6f71367b..b87a847d 100644 --- a/packages/api/src/lib/udapp/profile.ts +++ b/packages/api/src/lib/udapp/profile.ts @@ -3,6 +3,6 @@ import { LibraryProfile } from '@remixproject/plugin-utils' export const udappProfile: LibraryProfile = { name: 'udapp', - methods: ['createVMAccount', 'getAccounts', 'sendTransaction', 'getSettings'], + methods: ['createVMAccount', 'getAccounts', 'sendTransaction', 'getSettings', 'setEnvironmentMode'], events: ['newTransaction'] } diff --git a/packages/api/src/lib/vscextapi/api.ts b/packages/api/src/lib/vscextapi/api.ts new file mode 100644 index 00000000..dbd73e0c --- /dev/null +++ b/packages/api/src/lib/vscextapi/api.ts @@ -0,0 +1,9 @@ +import { StatusEvents } from '@remixproject/plugin-utils' + +export interface IVScodeExtAPI { + events: { + } & StatusEvents + methods: { + executeCommand(extension: string, command: string, payload?: any[]): any + } +} diff --git a/packages/api/src/lib/vscextapi/index.ts b/packages/api/src/lib/vscextapi/index.ts new file mode 100644 index 00000000..35157069 --- /dev/null +++ b/packages/api/src/lib/vscextapi/index.ts @@ -0,0 +1,2 @@ +export * from './api' +export * from './profile' diff --git a/packages/api/src/lib/vscextapi/profile.ts b/packages/api/src/lib/vscextapi/profile.ts new file mode 100644 index 00000000..f96d3aad --- /dev/null +++ b/packages/api/src/lib/vscextapi/profile.ts @@ -0,0 +1,7 @@ +import { IVScodeExtAPI } from './api' +import { LibraryProfile } from '@remixproject/plugin-utils' + +export const vscodeExtProfile: LibraryProfile = { + name: 'vscodeExtAPI', + methods: ['executeCommand'] +} diff --git a/packages/engine/core/package.json b/packages/engine/core/package.json index 6ba9646f..7078f545 100644 --- a/packages/engine/core/package.json +++ b/packages/engine/core/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/engine", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/engine/core#readme", "repository": { "type": "git", diff --git a/packages/engine/core/src/lib/abstract.ts b/packages/engine/core/src/lib/abstract.ts index 7c9aa51d..e7a4bd3c 100644 --- a/packages/engine/core/src/lib/abstract.ts +++ b/packages/engine/core/src/lib/abstract.ts @@ -11,12 +11,14 @@ import type { PluginApi, PluginBase, IPluginService, + PluginOptions, } from '@remixproject/plugin-utils' -import { +import { createService, activateService, getMethodPath, + PluginQueueItem, } from '@remixproject/plugin-utils' export interface RequestParams { @@ -25,18 +27,15 @@ export interface RequestParams { payload: any[] } -export interface PluginOptions { - /** The time to wait for a call to be executed before going to next call in the queue */ - queueTimeout?: number -} + export class Plugin implements PluginBase { activateService: Record Promise> = {} - protected requestQueue: Array<() => Promise> = [] protected currentRequest: PluginRequest /** Give access to all the plugins registered by the engine */ protected app: PluginApi protected options: PluginOptions = {} + protected queue: PluginQueueItem[] = [] // Lifecycle hooks onRegistration?(): void onActivation?(): void @@ -67,7 +66,7 @@ export class Plugin implements Pl this.options = { ...this.options, ...options } } - /** Call a method from this plugin */ + /** Call a method on this plugin */ protected callPluginMethod(key: string, args: any[]) { const path = this.currentRequest?.path const method = getMethodPath(key, path) @@ -77,48 +76,39 @@ export class Plugin implements Pl return this[method](...args) } + protected setCurrentRequest(request: PluginRequest) { + this.currentRequest = request + } + + protected letContinue() { + delete this.currentRequest + this.queue = this.queue.filter((value) => { + return value.canceled === false && value.timedout === false && value.finished === false + }) + const next = this.queue.find((value) => { + return value.canceled === false && value.timedout === false && value.finished === false + }) + if (next) next.run() + } + /** Add a request to the list of current requests */ protected addRequest(request: PluginRequest, method: Profile['methods'][number], args: any[]) { return new Promise((resolve, reject) => { - // Add a new request to the queue - this.requestQueue.push(async () => { - this.currentRequest = request - let timedout = false - const letcontinue = () => { - if (timedout) { - const { from } = this.currentRequest - const params = args.map(arg => JSON.stringify(arg)).join(', ') - const error = `[TIMED OUT]: Call to method "${method}" from "${from}" to plugin "${this.profile.name}" has timed out with arguments ${params}."` - reject(error) - } - // Remove current request and call next - delete this.currentRequest - this.requestQueue.shift() - if (this.requestQueue.length !== 0) this.requestQueue[0]() - } - - const ref = setTimeout(() => { - timedout = true - letcontinue() - }, this.options.queueTimeout || 10000) - - try { - const result = await this.callPluginMethod(method, args) - delete this.currentRequest - if (timedout) return - resolve(result) - } catch (err) { - delete this.currentRequest - reject(err) - } - clearTimeout(ref) - letcontinue() - }) - // If there is only one request waiting, call it - if (this.requestQueue.length === 1) { - this.requestQueue[0]() - } - }) + const queue = new PluginQueueItem(resolve, reject, request, method, this.options, args) + queue['setCurrentRequest'] = (request: PluginRequest) => this.setCurrentRequest(request) + queue['callMethod'] = async (method: string, args: any[]) => this.callPluginMethod(method, args) + queue['letContinue'] = () => this.letContinue() + this.queue.push(queue) + if (this.queue.length === 1) + this.queue[0].run(); + } + ) + } + + protected cancelRequests(request: PluginRequest, method: Profile['methods'][number]) { + for (const queue of this.queue) { + if (queue.request.from == request.from && (method ? queue.method == method : true)) queue.cancel() + } } @@ -230,6 +220,14 @@ export class Plugin implements Pl throw new Error(`Cannot use method "call" from plugin "${this.name}". It is not registered in the engine yet.`) } + /** Cancel a method of another plugin */ + async cancel, Key extends MethodKey>( + name: Name, + key: Key, + ): Promise> { + throw new Error(`Cannot use method "cancel" from plugin "${this.name}". It is not registered in the engine yet.`) + } + /** Emit an event */ emit>(key: Key, ...payload: EventParams): void { throw new Error(`Cannot use method "emit" from plugin "${this.name}". It is not registered in the engine yet.`) diff --git a/packages/engine/core/src/lib/connector.ts b/packages/engine/core/src/lib/connector.ts index dcc4d75d..6654a558 100644 --- a/packages/engine/core/src/lib/connector.ts +++ b/packages/engine/core/src/lib/connector.ts @@ -1,5 +1,5 @@ -import type { ExternalProfile, Profile, Message } from '@remixproject/plugin-utils' -import { Plugin, PluginOptions } from './abstract' +import type { ExternalProfile, Profile, Message, PluginOptions } from '@remixproject/plugin-utils' +import { Plugin } from './abstract' /** List of available gateways for decentralised storage */ export const defaultGateways = { @@ -17,6 +17,7 @@ export interface PluginConnectorOptions extends PluginOptions { /** Usally used to reload the plugin on changes */ devMode?: boolean transformUrl?: (profile: Profile & ExternalProfile) => string + engine?:string } @@ -81,18 +82,18 @@ export abstract class PluginConnector extends Plugin { this.loaded = true let methods: string[]; try { - methods = await this.callPluginMethod('handshake', [this.profile.name]) + methods = await this.callPluginMethod('handshake', [this.profile.name, this.options?.engine]) } catch (err) { this.loaded = false throw err; } if (methods) { this.profile.methods = methods - await this.call('manager', 'updateProfile', this.profile) + this.call('manager', 'updateProfile', this.profile) } } else { // If there is a broken connection we want send back the handshake to the plugin client - return this.callPluginMethod('handshake', [this.profile.name]) + return this.callPluginMethod('handshake', [this.profile.name, this.options?.engine]) } } @@ -148,6 +149,10 @@ export abstract class PluginConnector extends Plugin { } break } + case 'cancel': { + const payload = this.cancel(message.name, message.key) + break; + } // Return result from exposed method case 'response': { const { id, payload, error } = message @@ -160,4 +165,4 @@ export abstract class PluginConnector extends Plugin { } } } -} \ No newline at end of file +} diff --git a/packages/engine/core/src/lib/engine.ts b/packages/engine/core/src/lib/engine.ts index 9f0d71e3..c50dc761 100644 --- a/packages/engine/core/src/lib/engine.ts +++ b/packages/engine/core/src/lib/engine.ts @@ -1,7 +1,7 @@ -import type { PluginApi, Profile } from '@remixproject/plugin-utils' +import type { PluginApi, Profile, PluginOptions } from '@remixproject/plugin-utils' import { listenEvent } from '@remixproject/plugin-utils' import { BasePluginManager } from "./manager" -import { Plugin, PluginOptions } from './abstract' +import { Plugin } from './abstract' export class Engine { private plugins: Record = {} @@ -136,6 +136,44 @@ export class Engine { return this.plugins[target]['addRequest'](request, method, payload) } + /** + * Cancels calls from a plugin to another + * @param caller The name of the plugin that calls the method + * @param path The path of the plugin that manages the method + * @param method The name of the method to be cancelled, if is empty cancels all calls from plugin + */ + private async cancelMethod(caller: string, path: string, method: string) { + const target = path.split('.').shift() + if (!this.plugins[target]) { + throw new Error(`Cannot cancel ${method} on ${target} from ${caller}, because ${target} is not registered`) + } + + // Get latest version of the profiles + const [to, from] = await Promise.all([ + this.manager.getProfile(target), + this.manager.getProfile(caller), + ]) + + // Check if plugin FROM can activate plugin TO + const isActive = await this.manager.isActive(target) + + if (!isActive) { + throw new Error(`${from.name} cannot cancel ${method?`${method} of `:'calls on'}${target}, because ${target} is not activated`) + } + + // Check if method is exposed + // note: native methods go here + const methods = [...(to.methods || []), 'canDeactivate'] + if (!methods.includes(method) && method) { + const notExposedMsg = `Cannot cancel "${method}" of "${target}" from "${caller}", because "${method}" is not exposed.` + const exposedMethodsMsg = `Here is the list of exposed methods: ${methods.map(m => `"${m}"`).join(',')}` + throw new Error(`${notExposedMsg} ${exposedMethodsMsg}`) + } + + const request = { from: caller, path } + return this.plugins[target]['cancelRequests'](request, method) + } + /** * Create an object to easily access any registered plugin * @param name Name of the caller plugin @@ -187,6 +225,9 @@ export class Engine { plugin['call'] = (target: string, method: string, ...payload: any[]): Promise => { return this.callMethod(name, target, method, ...payload) } + plugin['cancel'] = (target: string, method: string): Promise => { + return this.cancelMethod(name, target, method) + } // GIVE ACCESS TO APP plugin['app'] = await this.createApp(name) @@ -248,6 +289,9 @@ export class Engine { plugin['call'] = (target: string, key: string, ...payload: any[]) => { throw new Error(deactivatedWarning(`It cannot call method ${key} of plugin ${target}.`)) } + plugin['cancel'] = (target: string, key: string, ...payload: any[]) => { + throw new Error(deactivatedWarning(`It cannot cancel method ${key} of plugin ${target}.`)) + } plugin['on'] = (target: string, event: string) => { throw new Error(deactivatedWarning(`It cannot listen on event ${event} of plugin ${target}.`)) } diff --git a/packages/engine/core/tests/abstract.spec.ts b/packages/engine/core/tests/abstract.spec.ts index f4676850..72c6858e 100644 --- a/packages/engine/core/tests/abstract.spec.ts +++ b/packages/engine/core/tests/abstract.spec.ts @@ -1,6 +1,8 @@ import { Plugin } from '../src/lib/abstract' -const profile = { name: 'mock', methods: ['mockMethod'] } +const profile = { name: 'mock', methods: ['mockMethod', 'slowMockMethod', 'slowMockMethodTwo','failingMockMethod'] } + +jest.setTimeout(10000) class MockPlugin extends Plugin { mockRequest = jest.fn() // Needed because we delete the currentRequest key each time @@ -20,6 +22,25 @@ class MockPlugin extends Plugin { } mockMethod = jest.fn(() => true) + failingMockMethod = jest.fn(()=> { + return new Promise((resolve, reject) => { + reject('fail') + }) + }) + slowMockMethod = jest.fn((num: number) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true) + }, num || 1000) + }) + }) + slowMockMethodTwo = jest.fn((num: number) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true) + }, num || 1000) + }) + }) onActivation = jest.fn() onDeactivation = jest.fn() } @@ -79,4 +100,158 @@ describe('Abstract Plugin', () => { expect(plugin.mockRequest.mock.calls[1][0]).toEqual({ from: 'caller2' }) expect(plugin.mockRequest.mock.calls[2][0]).toEqual({ from: 'caller3' }) }) + + test('addRequest should timeout', async (done) => { + plugin.setOptions({ queueTimeout: 10 }) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []).catch((err) => { + expect(err).toBe('[TIMEOUT] Timeout for call slowMockMethod from fake') + done() + }) + }); + + test('addRequest should not timeout', async () => { + plugin.setOptions({ queueTimeout: 1000 }) + const result = await plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', [500]) + expect(result).toBeTruthy() + }); + + + test('first addRequest should timeout, second one should succeed', async (done) => { + plugin.setOptions({ queueTimeout: 10 }) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []).catch((err) => { + expect(err).toBe('[TIMEOUT] Timeout for call slowMockMethod from fake') + done() + }) + plugin['addRequest']({ from: 'fake' }, 'mockMethod', []).then((x) => { + expect(x).toBeTruthy() + }) + }); + + test('addRequest should be canceled', async () => { + try { + setTimeout(() => { + plugin['cancelRequests']({ from: 'fake' }, 'slowMockMethod') + }, 500) + await plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []) + } catch (err) { + expect(err).toBe('[CANCEL] Canceled call slowMockMethod from fake') + } + }) + + test('addRequest should be canceled', async () => { + try { + setTimeout(() => { + plugin['cancelRequests']({ from: 'fake' }, '') + }, 500) + await plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []) + } catch (err) { + expect(err).toBe('[CANCEL] Canceled call slowMockMethod from fake') + } + }) + + test('addRequest should be not canceled', async () => { + setTimeout(() => { + plugin['cancelRequests']({ from: 'fake' }, 'slowMockMethod') + }, 500) + const result = await plugin['addRequest']({ from: 'fake' }, 'slowMockMethodTwo', []) + expect(result).toBeTruthy() + }) + + + test('two simultaneously queued requests should return true', async (done) => { + plugin['addRequest']({ from: 'fake' }, 'mockMethod', []).then((x) => { + expect(x).toBeTruthy() + }) + plugin['addRequest']({ from: 'fake' }, 'mockMethod', []).then((x) => { + expect(x).toBeTruthy() + done() + }) + }) + + test('two simultaneously queued requests should be canceled', async (done) => { + setTimeout(() => { + plugin['cancelRequests']({ from: 'fake' },'') + }, 500) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []).catch((err) => { + expect(err).toBe('[CANCEL] Canceled call slowMockMethod from fake') + }) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethodTwo', []).catch((err) => { + expect(err).toBe('[CANCEL] Canceled call slowMockMethodTwo from fake') + done() + }) + }) + + test('3 simultaneously queued requestsm 2 should be canceled', async (done) => { + setTimeout(() => { + plugin['cancelRequests']({ from: 'fake' },'') + }, 500) + plugin['addRequest']({ from: 'fake2' }, 'slowMockMethodTwo', []).then((x) => { + expect(x).toBeTruthy() + }) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []).catch((err) => { + expect(err).toBe('[CANCEL] Canceled call slowMockMethod from fake') + }) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethodTwo', []).catch((err) => { + expect(err).toBe('[CANCEL] Canceled call slowMockMethodTwo from fake') + done() + }) + }) + + test('request should be rejected', async (done) => { + plugin['addRequest']({ from: 'fake' }, 'failingMockMethod', []).catch((err) => { + expect(err).toBe('fail') + done() + }) + }) + + test('of two simultaneously queued requests 1 should return true other should be canceled', async (done) => { + setTimeout(() => { + plugin['cancelRequests']({ from: 'fake' }, 'slowMockMethod') + }, 500) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []).catch((err) => { + expect(err).toBe('[CANCEL] Canceled call slowMockMethod from fake') + }) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethodTwo', []).then((x) => { + expect(x).toBeTruthy() + done() + }) + }) + + test('one should timeout, one is canceled, and one succeeds', async (done) => { + plugin.setOptions({ queueTimeout: 500 }) + setTimeout(() => { + plugin['cancelRequests']({ from: 'fake' }, 'slowMockMethod') + }, 200) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []).catch((err) => { + expect(err).toBe('[CANCEL] Canceled call slowMockMethod from fake') + }) + plugin['addRequest']({ from: 'fake3' }, 'slowMockMethodTwo', [100]).then((x) => { + expect(x).toBeTruthy() + }) + plugin['addRequest']({ from: 'fake2' }, 'slowMockMethod', [600]).catch((err) => { + expect(err).toBe('[TIMEOUT] Timeout for call slowMockMethod from fake2') + done() + }) + }); + + test('one should timeout, one is canceled, and one succeeds, one fails', async (done) => { + plugin.setOptions({ queueTimeout: 500 }) + plugin['addRequest']({ from: 'fake' }, 'failingMockMethod', []).catch((err) => { + expect(err).toBe('fail') + }) + setTimeout(() => { + plugin['cancelRequests']({ from: 'fake' }, 'slowMockMethod') + }, 200) + plugin['addRequest']({ from: 'fake' }, 'slowMockMethod', []).catch((err) => { + expect(err).toBe('[CANCEL] Canceled call slowMockMethod from fake') + }) + plugin['addRequest']({ from: 'fake3' }, 'slowMockMethodTwo', [100]).then((x) => { + expect(x).toBeTruthy() + }) + plugin['addRequest']({ from: 'fake2' }, 'slowMockMethod', [600]).catch((err) => { + expect(err).toBe('[TIMEOUT] Timeout for call slowMockMethod from fake2') + done() + }) + }); + }) \ No newline at end of file diff --git a/packages/engine/core/tests/engine.spec.ts b/packages/engine/core/tests/engine.spec.ts index f12651b0..6f94eede 100644 --- a/packages/engine/core/tests/engine.spec.ts +++ b/packages/engine/core/tests/engine.spec.ts @@ -29,9 +29,16 @@ export class MockSolidity extends Plugin { onDeactivation = jest.fn() onRegistration = jest.fn() compile = jest.fn() + slowMockMethod = jest.fn((num: number) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true) + }, num || 1000) + }) + }) getCompilationResult = jest.fn() constructor() { - super({ ...compilerProfile, name: 'solidity' }) + super({ ...compilerProfile, name: 'solidity', methods:['slowMockMethod', ...compilerProfile.methods] }) } } @@ -208,6 +215,45 @@ describe('Plugin interaction', () => { expect(solidity['currentRequest']).toBeUndefined() }) + test('Plugin can cancel another plugin method', async (done) => { + await manager.activatePlugin(['solidity', 'fileManager']) + fileManager.call('solidity', 'slowMockMethod', 500).catch((err) => { + expect(solidity['currentRequest']).toBeUndefined() + expect(err).toBe('[CANCEL] Canceled call slowMockMethod from fileManager') + done() + }) + setTimeout(() => { + fileManager.cancel('solidity', 'slowMockMethod') + },250) + }) + + test('Plugin cannot cancel another plugin that is not activated', async () => { + await manager.activatePlugin(['fileManager']) + try { + await fileManager.cancel('solidity', 'slowMockMethod') + } catch(err) { + expect(err.message).toBe('fileManager cannot cancel slowMockMethod of solidity, because solidity is not activated') + } + }) + + test('Plugin cannot cancel an unknown method on another plugin', async () => { + await manager.activatePlugin(['solidity', 'fileManager']) + try { + await fileManager.cancel('solidity', 'unknownMethod') + } catch (err) { + expect(err.message).toBe('Cannot cancel "unknownMethod" of "solidity" from "fileManager", because "unknownMethod" is not exposed. Here is the list of exposed methods: "slowMockMethod","compile","getCompilationResult","compileWithParameters","setCompilerConfig","canDeactivate"') + } + }) + + test('Plugin cannot cancel another plugin that is not activated without a method specified', async () => { + await manager.activatePlugin(['fileManager']) + try { + await fileManager.cancel('solidity', '') + } catch(err) { + expect(err.message).toBe('fileManager cannot cancel calls onsolidity, because solidity is not activated') + } + }) + test('Current Request has been updated during call', async () => { await manager.activatePlugin(['solidity', 'fileManager']) let _currentRequest: Plugin['currentRequest'] diff --git a/packages/engine/electron/.eslintrc.json b/packages/engine/electron/.eslintrc.json new file mode 100644 index 00000000..dc9422ec --- /dev/null +++ b/packages/engine/electron/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../.eslintrc", + "rules": {}, + "ignorePatterns": ["!**/*"] +} diff --git a/packages/engine/electron/README.md b/packages/engine/electron/README.md new file mode 100644 index 00000000..cd42ea97 --- /dev/null +++ b/packages/engine/electron/README.md @@ -0,0 +1,3 @@ +# engine-electron + +This library was generated with [Nx](https://nx.dev). diff --git a/packages/engine/electron/jest.config.js b/packages/engine/electron/jest.config.js new file mode 100644 index 00000000..6307c17c --- /dev/null +++ b/packages/engine/electron/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + coverageDirectory: '../../../coverage/packages/engine/node', + globals: { 'ts-jest': { tsConfig: '/tsconfig.spec.json' } }, + displayName: 'engine-node', +}; diff --git a/packages/engine/electron/package.json b/packages/engine/electron/package.json new file mode 100644 index 00000000..7cc4864f --- /dev/null +++ b/packages/engine/electron/package.json @@ -0,0 +1,20 @@ +{ + "name": "@remixproject/engine-electron", + "version": "0.3.37", + "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/engine/node#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ethereum/remix-plugin.git" + }, + "author": { + "name": "bunsenstraat", + "email": "filip.mertens@ethereum.org" + }, + "contributors": [ + ], + "bugs": { + "url": "https://github.com/ethereum/remix-plugin/issues" + }, + "license": "MIT", + "gitHead": "ca5c69be64ec4eaf7fe5d1d362726e75cb3b5726" +} diff --git a/packages/engine/electron/src/global.d.ts b/packages/engine/electron/src/global.d.ts new file mode 100644 index 00000000..672fea24 --- /dev/null +++ b/packages/engine/electron/src/global.d.ts @@ -0,0 +1,14 @@ +export interface IElectronAPI { + activatePlugin: (name: string) => Promise + plugins: { + name: string + on: (cb: any) => void + send: (message: Partial) => void + }[] +} + +declare global { + interface Window { + electronAPI: IElectronAPI + } +} \ No newline at end of file diff --git a/packages/engine/electron/src/index.ts b/packages/engine/electron/src/index.ts new file mode 100644 index 00000000..edf26230 --- /dev/null +++ b/packages/engine/electron/src/index.ts @@ -0,0 +1 @@ +export * from './lib/electronPlugin'; diff --git a/packages/engine/electron/src/lib/electronPlugin.ts b/packages/engine/electron/src/lib/electronPlugin.ts new file mode 100644 index 00000000..2bdfa065 --- /dev/null +++ b/packages/engine/electron/src/lib/electronPlugin.ts @@ -0,0 +1,175 @@ +import type { Profile, Message } from '@remixproject/plugin-utils' +import { Plugin } from '@remixproject/engine'; + +export abstract class ElectronPlugin extends Plugin { + protected loaded: boolean + protected id = 0 + protected pendingRequest: Record void> = {} + protected api: { + send: (message: Partial) => void + on: (cb: (event: any, message: any) => void) => void + } + + profile: Profile + constructor(profile: Profile) { + super(profile) + this.loaded = false + + if(!window.electronAPI) throw new Error('ElectronPluginConnector requires window.api') + if(!window.electronAPI.plugins) throw new Error('ElectronPluginConnector requires window.api.plugins') + + window.electronAPI.plugins.find((plugin: any) => { + if(plugin.name === profile.name){ + this.api = plugin + return true + } + }) + + if(!this.api) throw new Error(`ElectronPluginConnector requires window.api.plugins.${profile.name} to be defined in preload.ts`) + + this.api.on((event: any, message: any) => { + this.getMessage(message) + }) + + + } + + /** + * Send a message to the external plugin + * @param message the message passed to the plugin + */ + protected send(message: Partial): void { + if(this.loaded) + this.api.send(message) + } + /** + * Open connection with the plugin + * @param name The name of the plugin should connect to + */ + protected async connect(name: string) { + const connected = await window.electronAPI.activatePlugin(name) + if(connected && !this.loaded){ + this.handshake() + } + + } + /** Close connection with the plugin */ + protected disconnect(): any | Promise { + // TODO: Disconnect from the plugin + } + + async activate() { + await this.connect(this.profile.name) + return super.activate() + } + + async deactivate() { + this.loaded = false + await this.disconnect() + return super.deactivate() + } + + /** Call a method from this plugin */ + protected callPluginMethod(key: string, payload: any[] = []): Promise { + const action = 'request' + const id = this.id++ + const requestInfo = this.currentRequest + const name = this.name + const promise = new Promise((res, rej) => { + this.pendingRequest[id] = (result: any[], error: Error | string) => error ? rej (error) : res(result) + }) + this.send({ id, action, key, payload, requestInfo, name }) + return promise + } + + /** Perform handshake with the client if not loaded yet */ + protected async handshake() { + if (!this.loaded) { + this.loaded = true + let methods: string[]; + try { + methods = await this.callPluginMethod('handshake', [this.profile.name]) + } catch (err) { + this.loaded = false + throw err; + } + this.emit('loaded', this.name) + if (methods) { + this.profile.methods = methods + this.call('manager', 'updateProfile', this.profile) + } + } else { + // If there is a broken connection we want send back the handshake to the plugin client + return this.callPluginMethod('handshake', [this.profile.name]) + } + } + + /** + * React when a message comes from client + * @param message The message sent by the client + */ + protected async getMessage(message: Message) { + // Check for handshake request from the client + if (message.action === 'request' && message.key === 'handshake') { + return this.handshake() + } + + switch (message.action) { + // Start listening on an event + case 'on': + case 'listen': { + const { name, key } = message + const action = 'notification' + this.on(name, key, (...payload: any[]) => this.send({ action, name, key, payload })) + break + } + case 'off': { + const { name, key } = message + this.off(name, key) + break + } + case 'once': { + const { name, key } = message + const action = 'notification' + this.once(name, key, (...payload: any) => this.send({ action, name, key, payload })) + break + } + // Emit an event + case 'emit': + case 'notification': { + if (!message.payload) break + this.emit(message.key, ...message.payload) + break + } + // Call a method + case 'call': + case 'request': { + const action = 'response' + try { + const payload = await this.call(message.name, message.key, ...message.payload) + const error: any = undefined + this.send({ ...message, action, payload, error }) + } catch (err) { + const payload: any = undefined + const error = err.message || err + this.send({ ...message, action, payload, error }) + } + break + } + case 'cancel': { + const payload = this.cancel(message.name, message.key) + break; + } + // Return result from exposed method + case 'response': { + const { id, payload, error } = message + this.pendingRequest[id](payload, error) + delete this.pendingRequest[id] + break + } + default: { + throw new Error('Message should be a notification, request or response') + } + } + } +} \ No newline at end of file diff --git a/packages/engine/electron/tsconfig.json b/packages/engine/electron/tsconfig.json new file mode 100644 index 00000000..16ab493f --- /dev/null +++ b/packages/engine/electron/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] + } + \ No newline at end of file diff --git a/packages/engine/electron/tsconfig.lib.json b/packages/engine/electron/tsconfig.lib.json new file mode 100644 index 00000000..0d097133 --- /dev/null +++ b/packages/engine/electron/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/engine/node/package.json b/packages/engine/node/package.json index 9c631ca5..682a260d 100644 --- a/packages/engine/node/package.json +++ b/packages/engine/node/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/engine-node", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/engine/node#readme", "repository": { "type": "git", diff --git a/packages/engine/theia/package.json b/packages/engine/theia/package.json index a04c4938..e557461a 100644 --- a/packages/engine/theia/package.json +++ b/packages/engine/theia/package.json @@ -1,4 +1,4 @@ { "name": "@remixproject/engine-theia", - "version": "0.3.8" + "version": "0.3.37" } diff --git a/packages/engine/vscode/package.json b/packages/engine/vscode/package.json index 5bc29a96..6099be1c 100644 --- a/packages/engine/vscode/package.json +++ b/packages/engine/vscode/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/engine-vscode", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/engine/vscode#readme", "repository": { "type": "git", diff --git a/packages/engine/vscode/src/index.ts b/packages/engine/vscode/src/index.ts index cf63fdc4..ab153604 100644 --- a/packages/engine/vscode/src/index.ts +++ b/packages/engine/vscode/src/index.ts @@ -6,4 +6,6 @@ export * from './lib/webview'; export * from './lib/window'; export * from './lib/filemanager'; export * from './lib/editor'; -export * from './lib/terminal'; \ No newline at end of file +export * from './lib/terminal'; +export * from './lib/contentimport'; +export * from './lib/appmanager'; \ No newline at end of file diff --git a/packages/engine/vscode/src/lib/appmanager.ts b/packages/engine/vscode/src/lib/appmanager.ts new file mode 100644 index 00000000..767ab891 --- /dev/null +++ b/packages/engine/vscode/src/lib/appmanager.ts @@ -0,0 +1,21 @@ +import { PluginManager } from "@remixproject/engine" +import axios from 'axios' +export class VscodeAppManager extends PluginManager { + pluginsDirectory:string + target:string + constructor () { + super() + this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json' + this.target = "vscode" + } + + async registeredPluginData () { + let plugins + try { + plugins = await axios.get(this.pluginsDirectory) + return plugins.data.filter((p:any)=>(p.targets && p.targets.includes(this.target))) + } catch (e) { + throw new Error("Could not fetch plugin profiles.") + } + } +} \ No newline at end of file diff --git a/packages/engine/vscode/src/lib/command.ts b/packages/engine/vscode/src/lib/command.ts index 4ef7b410..e2703a9e 100644 --- a/packages/engine/vscode/src/lib/command.ts +++ b/packages/engine/vscode/src/lib/command.ts @@ -1,5 +1,5 @@ -import { Plugin, PluginOptions } from '@remixproject/engine' -import { Profile } from '@remixproject/plugin-utils' +import { Plugin } from '@remixproject/engine' +import { Profile, PluginOptions } from '@remixproject/plugin-utils' import { Disposable, commands } from 'vscode' export const transformCmd = (name: string, method: string) => diff --git a/packages/engine/vscode/src/lib/contentimport.ts b/packages/engine/vscode/src/lib/contentimport.ts new file mode 100644 index 00000000..dbd2f58a --- /dev/null +++ b/packages/engine/vscode/src/lib/contentimport.ts @@ -0,0 +1,29 @@ +import { contentImportProfile, IContentImport } from '@remixproject/plugin-api'; +import { ContentImport } from '@remixproject/plugin-api'; +import { MethodApi } from '@remixproject/plugin-utils'; +import { CommandPlugin } from './command'; +import { RemixURLResolver } from '@remix-project/remix-url-resolver'; + +export class ContentImportPlugin extends CommandPlugin + implements MethodApi { + urlResolver: RemixURLResolver; + constructor() { + super(contentImportProfile); + this.urlResolver = new RemixURLResolver(); + } + + async resolve(path: string): Promise { + let resolved: any; + try { + resolved = await this.urlResolver.resolve(path); + const { content, cleanUrl, type } = resolved; + return { content, cleanUrl, type, url: path }; + } catch (e) { + throw Error(e.message); + } + } + // TODO: implement this method + async resolveAndSave(url: string, targetPath: string): Promise { + return ''; + } +} diff --git a/packages/engine/vscode/src/lib/dynamic-list.ts b/packages/engine/vscode/src/lib/dynamic-list.ts index 984a53a6..a35ea6d5 100644 --- a/packages/engine/vscode/src/lib/dynamic-list.ts +++ b/packages/engine/vscode/src/lib/dynamic-list.ts @@ -1,4 +1,5 @@ -import { Plugin, PluginOptions } from '@remixproject/engine' +import { Plugin } from '@remixproject/engine' +import { PluginOptions } from '@remixproject/plugin-utils' import { window, Disposable, TreeDataProvider, commands, EventEmitter, TreeView, TreeItem } from 'vscode' type ID = string | number diff --git a/packages/engine/vscode/src/lib/editor.ts b/packages/engine/vscode/src/lib/editor.ts index 3354d7b5..f29e92a6 100644 --- a/packages/engine/vscode/src/lib/editor.ts +++ b/packages/engine/vscode/src/lib/editor.ts @@ -93,4 +93,7 @@ export class EditorPlugin extends CommandPlugin implements MethodApi { async clearAnnotations(): Promise { return this.diagnosticCollection.clear(); } -} \ No newline at end of file + + // eslint-disable-next-line @typescript-eslint/no-empty-function + async gotoLine(line: number, col: number) { } +} diff --git a/packages/engine/vscode/src/lib/filemanager.ts b/packages/engine/vscode/src/lib/filemanager.ts index ab6d4287..3fa86ed7 100644 --- a/packages/engine/vscode/src/lib/filemanager.ts +++ b/packages/engine/vscode/src/lib/filemanager.ts @@ -1,8 +1,9 @@ import { filSystemProfile, IFileSystem } from '@remixproject/plugin-api' import { MethodApi } from '@remixproject/plugin-utils' -import { window, workspace, Uri, commands } from 'vscode' +import { window, workspace, Uri, commands, ViewColumn } from 'vscode' import { CommandPlugin } from './command' import { absolutePath, relativePath } from '../util/path' +import { getOpenedTextEditor } from '../util/editor' export class FileManagerPlugin extends CommandPlugin implements MethodApi { constructor() { @@ -12,7 +13,7 @@ export class FileManagerPlugin extends CommandPlugin implements MethodApi { const absPath = absolutePath(path) const uri = Uri.file(absPath) - return commands.executeCommand('vscode.open', uri) + return commands.executeCommand('vscode.open', uri, { viewColumn: ( getOpenedTextEditor()?.viewColumn || ViewColumn.One ) }) } /** Set the content of a specific file */ async writeFile(path: string, data: string): Promise { @@ -20,6 +21,7 @@ export class FileManagerPlugin extends CommandPlugin implements MethodApi { const absPath = absolutePath(path) const uri = Uri.file(absPath) + this.logMessage(' is removing ' + path) return workspace.fs.delete(uri) } /** Change the path of a file */ async rename(oldPath: string, newPath: string): Promise { const source = Uri.file(absolutePath(oldPath)) const target = Uri.file(absolutePath(newPath)) + this.logMessage(' is renaming ' + oldPath + ' to ' + newPath) return workspace.fs.rename(source, target) } /** Upsert a file with the content of the source file */ @@ -49,6 +53,7 @@ export class FileManagerPlugin extends CommandPlugin implements MethodApi { const uri = Uri.file(absolutePath(path)) + this.logMessage(' is creating ' + path) return workspace.fs.createDirectory(uri) } /** Get the list of files in the directory */ @@ -59,9 +64,23 @@ export class FileManagerPlugin extends CommandPlugin implements MethodApi { if (this.options.context) { - this.panel = createWebview(this.profile, url, this.options) + this.panel = await createWebview(this.profile, url, this.options) this.listeners = [ this.panel.webview.onDidReceiveMessage(msg => this.getMessage(msg)), this.panel.onDidDispose(_ => this.call('manager', 'deactivatePlugin', this.name)), @@ -45,6 +46,13 @@ export class WebviewPlugin extends PluginConnector { } } + async getMessage(message: Message) { + if(message.action == 'emit' && message.payload.href){ + env.openExternal(Uri.parse(message.payload.href)) + }else + super.getMessage(message) + } + protected disconnect(): void { this.listeners.forEach(disposable => disposable.dispose()); } @@ -55,14 +63,13 @@ function isHttpSource(protocol: string) { return protocol === 'https:' || protocol === 'http:' } - /** Create a webview */ -export function createWebview(profile: Profile, url: string, options: WebviewOptions) { +export async function createWebview(profile: Profile, url: string, options: WebviewOptions) { const { protocol, path } = parseUrl(url) const isRemote = isHttpSource(protocol) if (isRemote) { - return remoteHtml(url, profile, options) + return await getWebviewContent(url, profile, options) } else { const relativeTo = options.relativeTo || 'extension'; let fullPath: string; @@ -133,59 +140,87 @@ async function setLocalHtml(webview: Webview, baseUrl: string) { webview.html = html.replace(matchLinks, toUri) } - - - -//////////////// +/////////////// // REMOTE URL // -//////////////// -/** Create panel webview based on remote HTML source */ -function remoteHtml(url: string, profile: Profile, options: WebviewOptions) { - const { ext } = parsePath(url) - const baseUrl = ext === '.html' ? parsePath(url).dir : url +/////////////// +async function getWebviewContent(url: string, profile: Profile, options: WebviewOptions) { + // Use asExternalUri to get the URI for the web server + const uri = Uri.parse(url); + const serverUri = await env.asExternalUri(uri); + + // Create the webview const panel = window.createWebviewPanel( profile.name, profile.displayName || profile.name, options.column || window.activeTextEditor?.viewColumn || ViewColumn.One, - { enableScripts: true } - ) - setRemoteHtml(panel.webview, baseUrl) - return panel -} - - - -/** Fetch remote ressource with http */ -function fetch(url: string): Promise { - return new Promise((resolve, reject) => { - get(url, res => { - let text = '' - res.on('data', data => text += data) - res.on('end', _ => resolve(text)) - res.on('error', err => reject(err)) - }) - }) -} - - -/** Get code from remote source */ -async function setRemoteHtml(webview: Webview, baseUrl: string) { - const matchLinks = /(href|src)="([^"]*)"/g - const index = `${baseUrl}/index.html` - - - // Vscode requires URI format from the extension root to work - const toRemoteUrl = (original: any, prefix: 'href' | 'src', link: string) => { - // For: && remote url : - const isRemote = isHttpSource(parseUrl(link).protocol) - if (link === '#' || isRemote) { - return original + { + enableScripts: true } - // For scripts & links - const path = join(baseUrl, link) - return `${prefix}="${path}"` - } - - const html = await fetch(index) - webview.html = html.replace(matchLinks, toRemoteUrl) + ); + + const cspSource = panel.webview.cspSource; + const content = { + 'default-src': `none 'unsafe-inline'`, + 'frame-src': `${serverUri} ${cspSource} https`, + 'img-src': `${cspSource} https:`, + 'script-src': `${cspSource} ${serverUri} 'unsafe-inline'`, + 'style-src': `${cspSource} ${serverUri} 'unsafe-inline'`, + }; + const contentText = Object.entries(content).map(([key, value]) => `${key} ${value}`).join(';') + panel.webview.html = ` + + + + + + + + + `; + return panel } diff --git a/packages/engine/vscode/src/lib/window.ts b/packages/engine/vscode/src/lib/window.ts index 92e064ea..94adc83f 100644 --- a/packages/engine/vscode/src/lib/window.ts +++ b/packages/engine/vscode/src/lib/window.ts @@ -1,5 +1,5 @@ -import { Plugin, PluginOptions } from '@remixproject/engine' -import { Profile } from '@remixproject/plugin-utils' +import { Plugin } from '@remixproject/engine' +import { Profile, PluginOptions } from '@remixproject/plugin-utils' import { window, QuickPickOptions, InputBoxOptions } from 'vscode' export const windowProfile: Profile = { diff --git a/packages/engine/vscode/src/util/editor.ts b/packages/engine/vscode/src/util/editor.ts new file mode 100644 index 00000000..5e15747f --- /dev/null +++ b/packages/engine/vscode/src/util/editor.ts @@ -0,0 +1,21 @@ +import { TextEditor, window } from 'vscode'; + +export function getOpenedTextEditor() { + if (!window.activeTextEditor) { + return getTextEditorWithDocumentType('file'); + } else { + return window.activeTextEditor; + } +} + +export function getTextEditorWithDocumentType(type: string) { + const editors: any[] = window.visibleTextEditors as any; + const fileEditor: TextEditor = editors.find( + (editor) => + editor.document && + editor.document.uri && + editor.document.uri.scheme && + editor.document.uri.scheme == type + ); + return fileEditor; +} diff --git a/packages/engine/vscode/src/util/path.ts b/packages/engine/vscode/src/util/path.ts index f2634e67..11a6b092 100644 --- a/packages/engine/vscode/src/util/path.ts +++ b/packages/engine/vscode/src/util/path.ts @@ -2,6 +2,7 @@ import { join, relative } from 'path' import { workspace, } from 'vscode' export function absolutePath(path: string) { + path = path.replace(/^\/browser\//,"").replace(/^browser\//,"") const root = workspace.workspaceFolders[0].uri.fsPath // vscode API will never get permission to WriteFile outside of its workspace directory if(!root) { diff --git a/packages/engine/web/package.json b/packages/engine/web/package.json index 2da5bfaf..8ebb4e6e 100644 --- a/packages/engine/web/package.json +++ b/packages/engine/web/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/engine-web", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/engine/web#readme", "repository": { "type": "git", diff --git a/packages/engine/web/src/lib/view.ts b/packages/engine/web/src/lib/view.ts index 2147ef87..697c02d0 100644 --- a/packages/engine/web/src/lib/view.ts +++ b/packages/engine/web/src/lib/view.ts @@ -9,7 +9,7 @@ export function isView

(profile: Profile): profile is (ViewPro export type ViewProfile = Profile & LocationProfile export abstract class ViewPlugin extends Plugin { - abstract render(): Element + abstract render(): any constructor(public profile: ViewProfile) { super(profile) diff --git a/packages/engine/web/src/lib/web-worker.ts b/packages/engine/web/src/lib/web-worker.ts index f66abb0b..c7251ff6 100644 --- a/packages/engine/web/src/lib/web-worker.ts +++ b/packages/engine/web/src/lib/web-worker.ts @@ -1,5 +1,5 @@ -import type { Message, Profile, ExternalProfile } from '@remixproject/plugin-utils' -import { PluginConnector, PluginOptions } from '@remixproject/engine' +import type { Message, Profile, ExternalProfile, PluginOptions } from '@remixproject/plugin-utils' +import { PluginConnector } from '@remixproject/engine' type WebworkerOptions = WorkerOptions & PluginOptions diff --git a/packages/engine/web/src/lib/window.ts b/packages/engine/web/src/lib/window.ts index 0d8eeb42..271ac7e9 100644 --- a/packages/engine/web/src/lib/window.ts +++ b/packages/engine/web/src/lib/window.ts @@ -1,6 +1,6 @@ -import { Plugin, PluginOptions } from '@remixproject/engine' +import { Plugin } from '@remixproject/engine' import { IWindow, windowProfile } from '@remixproject/plugin-api' -import { MethodApi } from '@remixproject/plugin-utils'; +import { MethodApi, PluginOptions } from '@remixproject/plugin-utils'; export class WindowPlugin extends Plugin implements MethodApi { diff --git a/packages/engine/web/src/lib/ws.ts b/packages/engine/web/src/lib/ws.ts index c7772b19..fbb2b232 100644 --- a/packages/engine/web/src/lib/ws.ts +++ b/packages/engine/web/src/lib/ws.ts @@ -27,8 +27,12 @@ export class WebsocketPlugin extends PluginConnector { } private async getEvent(event: MessageEvent) { - const message: Message = JSON.parse(event.data) - this.getMessage(message) + try { + const message: Message = JSON.parse(event.data) + this.getMessage(message) + } catch (e) { + console.error(e) + } } diff --git a/packages/plugin/child-process/package.json b/packages/plugin/child-process/package.json index 00917e4d..ad26c405 100644 --- a/packages/plugin/child-process/package.json +++ b/packages/plugin/child-process/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-child-process", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/plugin/child_process#readme", "repository": { "type": "git", diff --git a/packages/plugin/child-process/src/lib/connector.ts b/packages/plugin/child-process/src/lib/connector.ts index 953d00d5..24107124 100644 --- a/packages/plugin/child-process/src/lib/connector.ts +++ b/packages/plugin/child-process/src/lib/connector.ts @@ -22,7 +22,13 @@ export class WebsocketConnector implements ClientConnector { /** Get messae from the engine */ on(cb: (message: Partial) => void) { - this.websocket.on('message', (event) => cb(JSON.parse(event))) + this.websocket.on('message', (event) => { + try { + cb(JSON.parse(event)) + } catch (e) { + console.error(e) + } + }) } } diff --git a/packages/plugin/core/package.json b/packages/plugin/core/package.json index c55e0c91..d9e01dca 100644 --- a/packages/plugin/core/package.json +++ b/packages/plugin/core/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin", - "version": "0.3.8", + "version": "0.3.37", "dependencies": { "events": "3.2.0" }, diff --git a/packages/plugin/core/src/lib/client.ts b/packages/plugin/core/src/lib/client.ts index 919313a8..6b4e69ed 100644 --- a/packages/plugin/core/src/lib/client.ts +++ b/packages/plugin/core/src/lib/client.ts @@ -138,6 +138,14 @@ export class PluginClient im }) } + public cancel, Key extends MethodKey>( + name: Name, + key?: Key | '', + ): void { + if (!this.isLoaded) handleConnectionError(this.options.devMode) + this.events.emit('send', { action: 'cancel', name, key }) + } + /** Listen on event from another plugin */ public on, Key extends EventKey>( name: Name, diff --git a/packages/plugin/electron/.eslintrc.json b/packages/plugin/electron/.eslintrc.json new file mode 100644 index 00000000..dc9422ec --- /dev/null +++ b/packages/plugin/electron/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../.eslintrc", + "rules": {}, + "ignorePatterns": ["!**/*"] +} diff --git a/packages/plugin/electron/README.md b/packages/plugin/electron/README.md new file mode 100644 index 00000000..c94eefa6 --- /dev/null +++ b/packages/plugin/electron/README.md @@ -0,0 +1,157 @@ +## Plugin electon + +How to use the plugin: + +In electron you +1. add the base plugin to a basic engine in electron: ElectronBasePlugin +2. define the clients: ElectronBasePluginClient + +3. In Remix you add a simple plugin: ElectronPlugin +4. You configer the preload script array to hold your plugin, see example below. If you don't do that you won't be able to call the plugin. + +The base plugin holds the clients, and each client holds a reference to the window it instantiated from. +More below about the engine. +The base plugin is called by the engine in Electron, you're not calling it from Remix. Only the ElectronBasePluginClient linked to a specific window is the one you are calling from Remix. So internal methods of the base plugin are used for example by the menu or you can call when something happens in electron, ie before the app is closed. + +``` +import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" + +import { Profile } from "@remixproject/plugin-utils"; + +const profile: Profile = { + displayName: 'exampleplugin', + name: 'exampleplugin', + description: 'Electron example plugin' +} + +export class ExamplePlugin extends ElectronBasePlugin { + clients: ExamplePluginClient[] = [] + constructor() { + super(profile, clientProfile, ExamplePluginClient) + this.methods = [...super.methods, 'internalMethod', 'doOnAllClients'] + } + + async internalMethod(data: any): Promise { + // do something + } + + // execute on all clients + async doOnAllClients(): Promise { + for (const client of this.clients) { + await client.doSomething() + } + } + +} + +const clientProfile: Profile = { + name: 'exampleplugin', + displayName: 'exampleplugin', + description: 'Electron example plugin', + methods: ['remixMethod'] +} + +class ExamplePluginClient extends ElectronBasePluginClient { + + constructor(webContentsId: number, profile: Profile) { + super(webContentsId, profile) + + this.window.on('close', async () => { + // do something on window close + }) + } + + async remixMethod(data: any): Promise { + // do something + } + + async doSomething(data: any): Promise { + } + + +} +``` + +On the side of Remix you define a plugin too. This is all you need to do + +``` +import { ElectronPlugin } from '@remixproject/engine-electron'; + +export class examplePlugin extends ElectronPlugin { + constructor() { + super({ + displayName: 'exampleplugin', + name: 'exampleplugin', + description: 'exampleplugin', + }) + this.methods = [] + + } +} +``` + + + +### The engine + +Here's an example. Important to note is the ipcMain handle which actually triggered by the peload script. +Check it out in remix: apps/remixdesktop/src/preload.ts +``` + +const engine = new Engine() +const appManager = new PluginManager() + +const examplePlugin = new ExamplePlugin() +engine.register(appManager) +engine.register(examplePlugin) + +ipcMain.handle('manager:activatePlugin', async (event, plugin) => { + return await appManager.call(plugin, 'createClient', event.sender.id) +}) + +app.on('before-quit', async (event) => { + await appManager.call('exampleplugin', 'doOnAllClients') +}) + +``` + +Preload script: +This script is included in the electron app and is loaded before the application. It is an isolated script that has access to the renderer process of electron and acts as the bridge between the application and the renderer. + +``` +import { Message } from '@remixproject/plugin-utils' +import { contextBridge, ipcRenderer } from 'electron' + +/* preload script needs statically defined API for each plugin */ + +const exposedPLugins = ['exampleplugin'] + +let webContentsId: number | undefined + +ipcRenderer.invoke('getWebContentsID').then((id: number) => { + webContentsId = id +}) + +contextBridge.exposeInMainWorld('electronAPI', { + activatePlugin: (name: string) => { + return ipcRenderer.invoke('manager:activatePlugin', name) + }, + + getWindowId: () => ipcRenderer.invoke('getWindowID'), + + plugins: exposedPLugins.map(name => { + return { + name, + on: (cb:any) => ipcRenderer.on(`${name}:send`, cb), + send: (message: Partial) => { + ipcRenderer.send(`${name}:on:${webContentsId}`, message) + } + } + }) +}) +``` + + + + + diff --git a/packages/plugin/electron/jest.config.js b/packages/plugin/electron/jest.config.js new file mode 100644 index 00000000..47c7cbc4 --- /dev/null +++ b/packages/plugin/electron/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: '../../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + coverageDirectory: '../../../coverage/packages/plugin/iframe', + globals: { 'ts-jest': { tsConfig: '/tsconfig.spec.json' } }, + displayName: 'plugin-iframe', +}; diff --git a/packages/plugin/electron/package.json b/packages/plugin/electron/package.json new file mode 100644 index 00000000..c21eeeb7 --- /dev/null +++ b/packages/plugin/electron/package.json @@ -0,0 +1,24 @@ +{ + "name": "@remixproject/plugin-electron", + "version": "0.3.37", + "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/plugin/iframe#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ethereum/remix-plugin.git" + }, + "author": { + "name": "GrandSchtroumpf", + "email": "francois.guezengar@hotmail.fr" + }, + "contributors": [ + { + "name": "Yann Levreau", + "email": "yann@ethdev.com" + } + ], + "bugs": { + "url": "https://github.com/ethereum/remix-plugin/issues" + }, + "license": "MIT", + "gitHead": "ca5c69be64ec4eaf7fe5d1d362726e75cb3b5726" +} diff --git a/packages/plugin/electron/src/index.ts b/packages/plugin/electron/src/index.ts new file mode 100644 index 00000000..86c762da --- /dev/null +++ b/packages/plugin/electron/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/electronPluginClient'; +export * from './lib/electronBasePlugin'; \ No newline at end of file diff --git a/packages/plugin/electron/src/lib/electronBasePlugin.ts b/packages/plugin/electron/src/lib/electronBasePlugin.ts new file mode 100644 index 00000000..991e3526 --- /dev/null +++ b/packages/plugin/electron/src/lib/electronBasePlugin.ts @@ -0,0 +1,54 @@ +import { Plugin } from "@remixproject/engine"; +import { PluginClient } from "@remixproject/plugin"; +import { Profile } from "@remixproject/plugin-utils"; +import { BrowserWindow } from "electron"; +import { createElectronClient } from "./electronPluginClient"; + +export interface ElectronBasePluginInterface { + createClient(windowId: number): Promise; + closeClient(windowId: number): Promise; +} + +export abstract class ElectronBasePlugin extends Plugin implements ElectronBasePluginInterface { + clients: ElectronBasePluginClient[] = []; + clientClass: any + clientProfile: Profile + constructor(profile: Profile, clientProfile: Profile, clientClass: any) { + super(profile); + this.methods = ['createClient', 'closeClient']; + this.clientClass = clientClass; + this.clientProfile = clientProfile; + } + + async createClient(webContentsId: number): Promise { + if (this.clients.find(client => client.webContentsId === webContentsId)) return true + const client = new this.clientClass(webContentsId, this.clientProfile); + this.clients.push(client); + return new Promise((resolve, reject) => { + client.onload(() => { + resolve(true) + }) + }) + } + async closeClient(windowId: number): Promise { + this.clients = this.clients.filter(client => client.webContentsId !== windowId) + return true; + } +} + +export class ElectronBasePluginClient extends PluginClient { + window: Electron.BrowserWindow; + webContentsId: number; + constructor(webcontentsid: number, profile: Profile, methods: string[] = []) { + super(); + this.methods = profile.methods; + this.webContentsId = webcontentsid; + BrowserWindow.getAllWindows().forEach((window) => { + if (window.webContents.id === webcontentsid) { + this.window = window; + } + }); + createElectronClient(this, profile, this.window); + } +} + diff --git a/packages/plugin/electron/src/lib/electronPluginClient.ts b/packages/plugin/electron/src/lib/electronPluginClient.ts new file mode 100644 index 00000000..f50ff0e8 --- /dev/null +++ b/packages/plugin/electron/src/lib/electronPluginClient.ts @@ -0,0 +1,34 @@ +import { ClientConnector, connectClient, applyApi, Client, PluginClient } from '@remixproject/plugin' +import type { Message, Api, ApiMap, Profile } from '@remixproject/plugin-utils' +import { IRemixApi } from '@remixproject/plugin-api' +import { ipcMain } from 'electron' + +export class ElectronPluginClientConnector implements ClientConnector { + + constructor(public profile: Profile, public browserWindow: Electron.BrowserWindow) { + } + + /** Send a message to the engine */ + send(message: Partial) { + this.browserWindow.webContents.send(this.profile.name + ':send', message) + } + + /** Listen to message from the engine */ + on(cb: (message: Partial) => void) { + ipcMain.on(this.profile.name + ':on:' + this.browserWindow.webContents.id, (event, message) => { + cb(message) + }) + } +} + +export const createElectronClient = < + P extends Api, + App extends ApiMap = Readonly +>(client: PluginClient = new PluginClient(), profile: Profile +, window: Electron.BrowserWindow +): Client => { + const c = client as any + connectClient(new ElectronPluginClientConnector(profile, window), c) + applyApi(c) + return c +} \ No newline at end of file diff --git a/packages/plugin/electron/tsconfig.json b/packages/plugin/electron/tsconfig.json new file mode 100644 index 00000000..f4023e39 --- /dev/null +++ b/packages/plugin/electron/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/plugin/electron/tsconfig.lib.json b/packages/plugin/electron/tsconfig.lib.json new file mode 100644 index 00000000..0d097133 --- /dev/null +++ b/packages/plugin/electron/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/plugin/iframe/package.json b/packages/plugin/iframe/package.json index 4468478e..47ec5dae 100644 --- a/packages/plugin/iframe/package.json +++ b/packages/plugin/iframe/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-iframe", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/plugin/iframe#readme", "repository": { "type": "git", diff --git a/packages/plugin/iframe/src/lib/connector.ts b/packages/plugin/iframe/src/lib/connector.ts index 7a83c23e..0847c788 100644 --- a/packages/plugin/iframe/src/lib/connector.ts +++ b/packages/plugin/iframe/src/lib/connector.ts @@ -67,6 +67,7 @@ export class IframeConnector implements ClientConnector { * const client = createClient(new MyPlugin()) * ``` */ + export const createClient = < P extends Api, App extends ApiMap = Readonly diff --git a/packages/plugin/iframe/src/lib/theme.ts b/packages/plugin/iframe/src/lib/theme.ts index abe64db9..3ab91728 100644 --- a/packages/plugin/iframe/src/lib/theme.ts +++ b/packages/plugin/iframe/src/lib/theme.ts @@ -17,6 +17,6 @@ export async function listenOnThemeChanged(client: PluginClient, options?: Parti function setTheme(cssLink: HTMLLinkElement, theme: Theme) { - cssLink.setAttribute('href', theme.url) + cssLink.setAttribute('href', theme.url.replace(/^http:/,"").replace(/^https:/,"")) document.documentElement.style.setProperty('--theme', theme.quality) } diff --git a/packages/plugin/theia/package.json b/packages/plugin/theia/package.json index 46ace88d..a30d63f7 100644 --- a/packages/plugin/theia/package.json +++ b/packages/plugin/theia/package.json @@ -1,4 +1,4 @@ { "name": "@remixproject/engine-plugin", - "version": "0.3.8" + "version": "0.3.37" } diff --git a/packages/plugin/vscode/package.json b/packages/plugin/vscode/package.json index 793995c2..dafe535b 100644 --- a/packages/plugin/vscode/package.json +++ b/packages/plugin/vscode/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-vscode", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/plugin/vscode#readme", "repository": { "type": "git", diff --git a/packages/plugin/webview/README.md b/packages/plugin/webview/README.md index a415314e..7149cafb 100644 --- a/packages/plugin/webview/README.md +++ b/packages/plugin/webview/README.md @@ -18,7 +18,7 @@ client.onload(async () => { If you need to expose an API to other plugin you need to extends the class: ```typescript import { createClient } from '@remixproject/plugin-webview' -import { PluginClient } from '@rexmixproject/plugin' +import { PluginClient } from '@remixproject/plugin' class MyPlugin extends PluginClient { methods = ['hello'] diff --git a/packages/plugin/webview/package.json b/packages/plugin/webview/package.json index 01839ded..9a70d530 100644 --- a/packages/plugin/webview/package.json +++ b/packages/plugin/webview/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-webview", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/plugin/webview#readme", "repository": { "type": "git", diff --git a/packages/plugin/webview/src/lib/connector.ts b/packages/plugin/webview/src/lib/connector.ts index 2b6fb6ef..f9d2aaba 100644 --- a/packages/plugin/webview/src/lib/connector.ts +++ b/packages/plugin/webview/src/lib/connector.ts @@ -9,13 +9,14 @@ import { checkOrigin, isPluginMessage } from '@remixproject/plugin' -import { RemixApi, Theme } from '@remixproject/plugin-api'; +import { IRemixApi, Theme } from '@remixproject/plugin-api'; +import axios from 'axios' /** Transform camelCase (JS) text into kebab-case (CSS) */ function toKebabCase(text: string) { return text.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase(); -}; +} declare global { function acquireTheiaApi(): any; @@ -32,11 +33,6 @@ export class WebviewConnector implements ClientConnector { constructor(private options: PluginOptions) { // @todo(#295) check if we can merge this statement in `this.isVscode = acquireVsCodeApi !== undefined` - if ('acquireVsCodeApi' in window) { - this.isVscode = true - this.source = window['acquireVsCodeApi']() - return - } try { this.isVscode = acquireTheiaApi !== undefined this.source = acquireTheiaApi() @@ -64,6 +60,23 @@ export class WebviewConnector implements ClientConnector { window.addEventListener('message', async (event: MessageEvent) => { if (!event.source) return if (!event.data) return + // copy paste events from vscode + if (event.origin.indexOf('vscode-webview:') > -1) { + if (event.data.action && event.data.action === 'paste') { + this.pasteClipBoard(event); + return; + } + if (event.data.action && event.data.action === 'copy') { + const selection = document.getSelection(); + const event = { + action: 'copy', + data: selection.toString() + } + window.parent.postMessage(event, '*') + return; + } + } + // plugin messages if (!isPluginMessage(event.data)) return // Support for iframe if (!this.isVscode) { @@ -73,12 +86,70 @@ export class WebviewConnector implements ClientConnector { if (isHandshake(event.data)) { this.origin = event.origin this.source = event.source as Window + if(event.data.payload[1] && event.data.payload[1] == 'vscode') this.forwardEvents() } } cb(event.data) }, false) } + + // vscode specific, webview iframe requires forwarding of keyboard events & links clicked + pasteClipBoard(event) { + this.insertAtCursor(document.activeElement, event.data.data); + } + + insertAtCursor(element:any, value:any) { + const lastValue:any = element.value; + if (element.selectionStart || element.selectionStart == '0') { + element.value = element.value.substring(0, element.selectionStart) + + value + + element.value.substring(element.selectionEnd, element.value.length); + } else { + element.value += value; + } + // this takes care of triggering the change on React components + const event:any = new Event('input', { bubbles: true }); + event.simulated = true; + const tracker:any = element._valueTracker; + if (tracker) { + tracker.setValue(lastValue); + } + element.dispatchEvent(event); + } + forwardEvents(){ + document.addEventListener('keydown', e => { + const obj = { + altKey: e.altKey, + code: e.code, + ctrlKey: e.ctrlKey, + isComposing: e.isComposing, + key: e.key, + location: e.location, + metaKey: e.metaKey, + repeat: e.repeat, + shiftKey: e.shiftKey, + action: 'keydown' + } + window.parent.postMessage( obj, '*') + }) + document.body.onclick = function (e:any) { + const closest = e.target?.closest("a"); + if (closest) { + const href = closest.getAttribute('href'); + if (href != '#') { + window.parent.postMessage({ + action: 'emit', + payload: { + href: href, + }, + }, '*'); + return false; + } + } + return true; + }; + } } /** @@ -87,18 +158,18 @@ export class WebviewConnector implements ClientConnector { */ export const createClient = < P extends Api = any, - App extends ApiMap = RemixApi, + App extends ApiMap = Readonly, C extends PluginClient = any >(client: C): C & PluginApi => { const c = client as any || new PluginClient() - const options = client.options + const options = c.options const connector = new WebviewConnector(options) connectClient(connector, c) applyApi(c) if (!options.customTheme) { listenOnThemeChanged(c) } - return client as any + return c as any } /** Set the theme variables in the :root */ @@ -136,10 +207,33 @@ async function listenOnThemeChanged(client: PluginClient) { return cssLink; } + const setAttribute = (url, backupUrl = null) => { + // there is no way to know if it will load unless it's loaded first + axios.get(url).then(() => { + getLink().setAttribute('href', url); + }).catch(() => { + if(backupUrl) getLink().setAttribute('href', backupUrl); + }); + } + // If there is a url in the theme, use it const setLink = (theme: Theme) => { if (theme.url) { - getLink().setAttribute('href', theme.url) + const url = theme.url.replace(/^http:/, "protocol:").replace(/^https:/, "protocol:"); + const regexp = /^https:/; + const httpsUrl = url.replace(/^protocol:/, "https:"); + const httpUrl = url.replace(/^protocol:/, "http:") + + // if host is https, https will always work + // if host is http, but plugin is https, try both but first https, http will always fail if https css is not found + // if host is localhost, https plugins can load http resource but will throw error for https first + if (regexp.test(theme.url) || (!regexp.test(theme.url) && regexp.test(window.location.href))) { + setAttribute(httpsUrl, httpUrl); + } + // both are http load http, ie localhost + if (!regexp.test(theme.url) && !regexp.test(window.location.href)) { + setAttribute(httpUrl); + } document.documentElement.style.setProperty('--theme', theme.quality) } } diff --git a/packages/plugin/webworker/package.json b/packages/plugin/webworker/package.json index d5e618d2..578a336c 100644 --- a/packages/plugin/webworker/package.json +++ b/packages/plugin/webworker/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-webworker", - "version": "0.3.8", + "version": "0.3.37", "homepage": "https://github.com/ethereum/remix-plugin/tree/master/packages/plugin/webworker#readme", "repository": { "type": "git", diff --git a/packages/plugin/ws/package.json b/packages/plugin/ws/package.json index e5692795..13f52b1f 100644 --- a/packages/plugin/ws/package.json +++ b/packages/plugin/ws/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-ws", - "version": "0.3.8", + "version": "0.3.37", "peerDependencies": { "ws": "^7.3.1" }, diff --git a/packages/plugin/ws/src/lib/ws.ts b/packages/plugin/ws/src/lib/ws.ts index da913ac5..a2240bfd 100644 --- a/packages/plugin/ws/src/lib/ws.ts +++ b/packages/plugin/ws/src/lib/ws.ts @@ -1,8 +1,19 @@ -import type { Message, Api, ApiMap } from '@remixproject/plugin-utils' -import { PluginClient, ClientConnector, connectClient, applyApi, Client } from '@remixproject/plugin' +import { + Message, + Api, + ApiMap, + getMethodPath, +} from '@remixproject/plugin-utils' +import { + PluginClient, + ClientConnector, + connectClient, + applyApi, + Client, + isHandshake, +} from '@remixproject/plugin' import { IRemixApi } from '@remixproject/plugin-api' - export interface WS { send(data: string): void on(type: 'message', cb: (event: string) => any): this @@ -12,17 +23,43 @@ export interface WS { * This Websocket connector works with the library `ws` */ export class WebsocketConnector implements ClientConnector { - + private client: PluginClient constructor(private websocket: WS) {} + setClient(client: PluginClient) { + this.client = client + } + /** Send a message to the engine */ send(message: Partial) { this.websocket.send(JSON.stringify(message)) } - /** Get messae from the engine */ + /** Get message from the engine */ on(cb: (message: Partial) => void) { - this.websocket.on('message', (event) => cb(JSON.parse(event))) + this.websocket.on('message', (event) => { + try { + const parsedEvent = JSON.parse(event) + if (!isHandshake(parsedEvent)) { + if ( + parsedEvent.action && + (parsedEvent.action === 'request' || parsedEvent.action === 'call') + ) { + const path = + parsedEvent.requestInfo && parsedEvent.requestInfo.path + const method = getMethodPath(parsedEvent.key, path) + if (this.client.methods && !this.client.methods.includes(method)) { + throw new Error( + `${method} is not in the list of allowed methods.` + ) + } + } + } + cb(JSON.parse(event)) + } catch (e) { + console.error(e) + } + }) } } @@ -56,9 +93,14 @@ export class WebsocketConnector implements ClientConnector { export const createClient = < P extends Api, App extends ApiMap = Readonly ->(websocket: WS, client: PluginClient = new PluginClient()): Client => { +>( + websocket: WS, + client: PluginClient = new PluginClient() +): Client => { const c = client as any - connectClient(new WebsocketConnector(websocket), c) + const websocketConnector:WebsocketConnector = new WebsocketConnector(websocket) + connectClient(websocketConnector, c) applyApi(c) + websocketConnector.setClient(c) return c } diff --git a/packages/utils/package.json b/packages/utils/package.json index 93da23ab..4fa206c9 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-utils", - "version": "0.3.8", + "version": "0.3.37", "dependencies": { "tslib": "2.0.1" }, diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 13589097..818ace8e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,10 +1,12 @@ export * from './lib/tools/event-name'; export * from './lib/tools/method-path'; export * from './lib/tools/service'; - +export * from './lib/tools/queue'; export * from './lib/types/api'; export * from './lib/types/message'; export * from './lib/types/plugin'; export * from './lib/types/profile'; export * from './lib/types/service'; -export * from './lib/types/status'; \ No newline at end of file +export * from './lib/types/status'; +export * from './lib/types/queue'; +export * from './lib/types/options'; \ No newline at end of file diff --git a/packages/utils/src/lib/tools/queue.ts b/packages/utils/src/lib/tools/queue.ts new file mode 100644 index 00000000..d010a7d8 --- /dev/null +++ b/packages/utils/src/lib/tools/queue.ts @@ -0,0 +1,82 @@ +import { PluginQueueInterface } from '../types/queue' +import type { + PluginRequest, +} from '../types/message' +import { Profile } from '../types/profile' +import { Api } from '../types/api' +import { PluginOptions } from '@remixproject/plugin-utils' + +export class PluginQueueItem implements PluginQueueInterface { + private resolve: (value:unknown) => void + private reject: (reason:any) => void + private timer: any + private running: boolean + private args: any[] + + public method: Profile['methods'][number] + public timedout: boolean + public canceled: boolean + public finished: boolean + public request: PluginRequest + private options: PluginOptions = {} + + constructor(resolve: (value:unknown) => void, reject: (reason:any) => void, request: PluginRequest, method: Profile['methods'][number], options: PluginOptions, args: any[]){ + this.resolve = resolve + this.reject = reject + this.method = method + this.request = request + this.timer = undefined + this.timedout = false + this.canceled = false + this.finished = false + this.running = false + this.args = args + this.options = options + } + + setCurrentRequest(request: PluginRequest): void { + throw new Error('Cannot call this directly') + } + + callMethod(method: string, args: any[]): void { + throw new Error('Cannot call this directly') + } + + letContinue(): void { + throw new Error('Cannot call this directly') + } + + cancel(): void { + this.canceled = true + clearTimeout(this.timer) + this.reject(`[CANCEL] Canceled call ${this.method} from ${this.request.from}`) + if(this.running) + this.letContinue() + } + + async run(){ + if(this.canceled) { + this.letContinue() + return + } + this.timer = setTimeout(()=>{ + this.timedout = true + this.reject(`[TIMEOUT] Timeout for call ${this.method} from ${this.request.from}`) + this.letContinue() + }, this.options.queueTimeout || 10000) + + this.running = true + this.setCurrentRequest(this.request) + try{ + const result = await this.callMethod(this.method, this.args) + if(this.timedout || this.canceled) return + this.resolve(result) + }catch(err){ + this.reject(err) + } + this.finished = true + this.running = false + clearTimeout(this.timer) + this.letContinue(); + } +} \ No newline at end of file diff --git a/packages/utils/src/lib/types/message.ts b/packages/utils/src/lib/types/message.ts index b366b420..b1897d92 100644 --- a/packages/utils/src/lib/types/message.ts +++ b/packages/utils/src/lib/types/message.ts @@ -10,7 +10,7 @@ export interface PluginRequest { path?: string } -type MessageActions = 'on' | 'off' | 'once' | 'call' | 'response' | 'emit' +type MessageActions = 'on' | 'off' | 'once' | 'call' | 'response' | 'emit' | 'cancel' /** @deprecated Use `MessageAcitons` instead */ type OldMessageActions = 'notification' | 'request' | 'response' | 'listen' diff --git a/packages/utils/src/lib/types/options.ts b/packages/utils/src/lib/types/options.ts new file mode 100644 index 00000000..24d052ee --- /dev/null +++ b/packages/utils/src/lib/types/options.ts @@ -0,0 +1,4 @@ +export interface PluginOptions { + /** The time to wait for a call to be executed before going to next call in the queue */ + queueTimeout?: number +} \ No newline at end of file diff --git a/packages/utils/src/lib/types/plugin.ts b/packages/utils/src/lib/types/plugin.ts index 3792f684..cdf6b504 100644 --- a/packages/utils/src/lib/types/plugin.ts +++ b/packages/utils/src/lib/types/plugin.ts @@ -2,7 +2,7 @@ import type { IPluginService } from './service' import { EventCallback, MethodParams, MethodKey, EventKey, Api, ApiMap, EventParams } from './api' export interface PluginBase { - methods: string[] + methods: string[], activateService: Record Promise> /** Listen on an event from another plugin */ on, Key extends EventKey>( @@ -30,6 +30,13 @@ export interface PluginBase { key: Key, ...payload: MethodParams ): Promise + + /** Clear calls in queue of a plugin called by plugin */ + cancel, Key extends MethodKey>( + name: Name, + key: Key, + ): void + /** Emit an event */ emit>(key: Key, ...payload: EventParams): void } diff --git a/packages/utils/src/lib/types/profile.ts b/packages/utils/src/lib/types/profile.ts index d738a91b..f6e0e4e1 100644 --- a/packages/utils/src/lib/types/profile.ts +++ b/packages/utils/src/lib/types/profile.ts @@ -5,11 +5,19 @@ export interface Profile { name: string displayName?: string methods?: MethodKey[] + events?: EventKey[] permission?: boolean hash?: string description?: string documentation?: string version?: string + kind?: string, + canActivate?: string[] + icon?: string + maintainedBy?: string, + author?: string + repo?: string + authorContact?: string } export interface LocationProfile { diff --git a/packages/utils/src/lib/types/queue.ts b/packages/utils/src/lib/types/queue.ts new file mode 100644 index 00000000..70482988 --- /dev/null +++ b/packages/utils/src/lib/types/queue.ts @@ -0,0 +1,10 @@ +import type { + PluginRequest, + } from './message' + +export interface PluginQueueInterface { + setCurrentRequest(request: PluginRequest): void + callMethod(method: string, args: any[]): void + letContinue(): void + cancel(): void +} \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index db670dcf..5250ca2a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,7 +27,9 @@ "packages/plugin/webworker/src/index.ts" ], "@remixproject/plugin-theia": ["packages/plugin/theia/src/index.ts"], - "@remixproject/engine-theia": ["packages/engine/theia/src/index.ts"] + "@remixproject/engine-theia": ["packages/engine/theia/src/index.ts"], + "@remixproject/engine-electron": ["packages/engine/electron/src/index.ts"], + "@remixproject/plugin-electron": ["packages/plugin/electron/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 48f22779..1e411a7d 100644 --- a/workspace.json +++ b/workspace.json @@ -36,7 +36,7 @@ "main": "packages/engine/core/src/index.ts", "assets": ["packages/engine/core/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/engine/core/src" + "srcRootForCompilationRoot": "packages/engine/core" } }, "publish": { @@ -83,7 +83,7 @@ "main": "packages/engine/node/src/index.ts", "assets": ["packages/engine/node/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/engine/node/src" + "srcRootForCompilationRoot": "packages/engine/node" } }, "publish": { @@ -130,7 +130,7 @@ "main": "packages/engine/web/src/index.ts", "assets": ["packages/engine/web/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/engine/web/src" + "srcRootForCompilationRoot": "packages/engine/web" } }, "publish": { @@ -177,7 +177,7 @@ "main": "packages/engine/vscode/src/index.ts", "assets": ["packages/engine/vscode/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/engine/vscode/src" + "srcRootForCompilationRoot": "packages/engine/vscode" } }, "publish": { @@ -224,7 +224,7 @@ "main": "packages/utils/src/index.ts", "assets": ["packages/utils/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/utils/src" + "srcRootForCompilationRoot": "packages/utils" } }, "publish": { @@ -271,7 +271,7 @@ "main": "packages/api/src/index.ts", "assets": ["packages/api/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/api/src" + "srcRootForCompilationRoot": "packages/api" } }, "publish": { @@ -318,7 +318,7 @@ "main": "packages/plugin/core/src/index.ts", "assets": ["packages/plugin/core/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/plugin/core/src" + "srcRootForCompilationRoot": "packages/plugin/core" } }, "publish": { @@ -365,7 +365,7 @@ "main": "packages/plugin/iframe/src/index.ts", "assets": ["packages/plugin/iframe/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/plugin/iframe/src" + "srcRootForCompilationRoot": "packages/plugin/iframe" } }, "publish": { @@ -412,7 +412,7 @@ "main": "packages/plugin/vscode/src/index.ts", "assets": ["packages/plugin/vscode/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/plugin/vscode/src" + "srcRootForCompilationRoot": "packages/plugin/vscode" } }, "publish": { @@ -459,7 +459,7 @@ "main": "packages/plugin/child-process/src/index.ts", "assets": ["packages/plugin/child-process/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/plugin/child-process/src" + "srcRootForCompilationRoot": "packages/plugin/child-process" } }, "publish": { @@ -499,7 +499,7 @@ "main": "packages/plugin/theia/src/index.ts", "assets": ["packages/plugin/theia/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/plugin/theia/src" + "srcRootForCompilationRoot": "packages/plugin/theia" } }, "publish": { @@ -546,7 +546,7 @@ "main": "packages/plugin/ws/src/index.ts", "assets": ["packages/plugin/ws/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/plugin/ws/src" + "srcRootForCompilationRoot": "packages/plugin/ws" } }, "publish": { @@ -593,7 +593,7 @@ "main": "packages/plugin/webview/src/index.ts", "assets": ["packages/plugin/webview/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/plugin/webview/src" + "srcRootForCompilationRoot": "packages/plugin/webview" } }, "publish": { @@ -633,7 +633,7 @@ "main": "packages/plugin/webworker/src/index.ts", "assets": ["packages/plugin/webworker/*.md"], "externalDependencies": "none", - "srcRootForCompilationRoot": "packages/plugin/webworker/src" + "srcRootForCompilationRoot": "packages/plugin/webworker" } }, "publish": { @@ -922,7 +922,7 @@ "packageJson": "packages/engine/theia/package.json", "main": "packages/engine/theia/src/index.ts", "assets": ["packages/engine/theia/*.md"], - "srcRootForCompilationRoot": "packages/engine/theia/src", + "srcRootForCompilationRoot": "packages/engine/theia", "externalDependencies": "none" } }, @@ -934,6 +934,72 @@ } } } + }, + "engine-electron": { + "root": "packages/engine/electron", + "sourceRoot": "packages/engine/electron/src", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["packages/engine/electron/**/*.ts"] + } + }, + "build": { + "builder": "@nrwl/node:package", + "options": { + "outputPath": "dist/packages/engine/electron", + "tsConfig": "packages/engine/electron/tsconfig.lib.json", + "packageJson": "packages/engine/electron/package.json", + "main": "packages/engine/electron/src/index.ts", + "assets": ["packages/engine/electron/*.md"], + "srcRootForCompilationRoot": "packages/engine/electron", + "externalDependencies": "none" + } + }, + "publish": { + "builder": "@nrwl/workspace:run-commands", + "options": { + "commands": ["npm publish --tag={tag}"], + "cwd": "dist/packages/engine/electron" + } + } + } + }, + "plugin-electron": { + "root": "packages/plugin/electron", + "sourceRoot": "packages/plugin/electron/src", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["packages/plugin/electron/**/*.ts"] + } + }, + "build": { + "builder": "@nrwl/node:package", + "options": { + "outputPath": "dist/packages/plugin/electron", + "tsConfig": "packages/plugin/electron/tsconfig.lib.json", + "packageJson": "packages/plugin/electron/package.json", + "main": "packages/plugin/electron/src/index.ts", + "assets": ["packages/plugin/electron/*.md"], + "srcRootForCompilationRoot": "packages/plugin/electron", + "externalDependencies": "none" + } + }, + "publish": { + "builder": "@nrwl/workspace:run-commands", + "options": { + "commands": ["npm publish --tag={tag}"], + "cwd": "dist/packages/plugin/electron" + } + } + } } }, "cli": {