diff --git a/.circleci/config.yml b/.circleci/config.yml index f08133f3..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:10 - 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/.gitignore b/.gitignore index 1cdafbd5..e1a3e635 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ testem.log # System Files .DS_Store Thumbs.db +remixplugin.code-workspace diff --git a/lerna.json b/lerna.json index 7e4c0a86..6696839c 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/**/*" ], - "version": "0.3.1", + "version": "0.3.5", "publishConfig": { "access": "public", "directory": "dist/packages" 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 e9ef614e..05151082 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "remix-plugin", - "version": "0.3.1", + "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", @@ -15085,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", @@ -15253,13 +15356,12 @@ "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.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", "dev": true }, "init-package-json": { @@ -18876,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" } @@ -19303,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", @@ -19410,9 +19509,9 @@ "dev": true }, "node-notifier": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz", - "integrity": "sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", + "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, "optional": true, "requires": { @@ -19434,17 +19533,30 @@ "is-docker": "^2.0.0" } }, + "lru-cache": { + "version": "6.0.0", + "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" + } + }, "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } }, "uuid": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "optional": true }, @@ -19457,6 +19569,13 @@ "requires": { "isexe": "^2.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true } } }, @@ -20032,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" } @@ -21578,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", @@ -21676,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", @@ -22715,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", @@ -23946,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" } @@ -24286,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", @@ -25234,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" @@ -25243,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=" } } }, @@ -25284,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", @@ -25337,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", @@ -26523,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 5a795f09..e5d82ea0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "remix-plugin", - "version": "0.3.1", + "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/compiler.md b/packages/api/doc/compiler.md new file mode 100644 index 00000000..372873f0 --- /dev/null +++ b/packages/api/doc/compiler.md @@ -0,0 +1,10 @@ +# Compile + +A plugin `compiler` compiles data into an Ethereum Virtual Machine readable bytecode and metadata. + +|Type |Name |Description | +|---------|-----------------------|------------| +|_method_ |`getCompilationResult` |Highlight a piece of code in the editor. +|_method_ |`compile` |Remove the highlight triggered by this plugin. +|_method_ |`setCompilerConfig` |Remove the highlight triggered by this plugin +|_method_ |`compileWithParameters` |Add annotation ( info, warning, error ) at a line, column position with a text \ No newline at end of file diff --git a/packages/api/doc/content-import.md b/packages/api/doc/content-import.md index 501a643e..ef13b074 100644 --- a/packages/api/doc/content-import.md +++ b/packages/api/doc/content-import.md @@ -6,7 +6,7 @@ |Type |Name |Description | |---------|-----------------------|------------| |_method_ |`resolve` |Resolve a file from github, ipfs, swarm, http or https - +|_method_ |`resolveAndSave` |Resolve a file from github, ipfs, swarm, http or https and save it in the file explorer ## Examples ### Methods @@ -19,6 +19,16 @@ const { content } = await client.call('contentImport', 'resolve', link) const { content } = await client.contentImport.resolve(link) ``` +`resolveAndSave`: Resolve and save a file from github, ipfs, swarm, http or https +```typescript +const link = "https://github.com/GrandSchtroumpf/solidity-school/blob/master/std-0/1_HelloWorld/HelloWorld.sol" +const targetPath = 'HelloWorld.sol' # optional + +const { content } = await client.call('contentImport', 'resolveAndSave', link, targetPath) +// OR +const { content } = await client.contentImport.resolveAndSave(link, targetPath) +``` + ## Types `ContentImport`: An object that describes the returned file ```typescript diff --git a/packages/api/doc/editor.md b/packages/api/doc/editor.md index 810d5e59..a39b0352 100644 --- a/packages/api/doc/editor.md +++ b/packages/api/doc/editor.md @@ -7,45 +7,11 @@ |---------|-----------------------|------------| |_method_ |`highlight` |Highlight a piece of code in the editor. |_method_ |`discardHighlight` |Remove the highlight triggered by this plugin. +|_method_ |`discardHighlightAt` |Remove the highlight triggered by this plugin +|_method_ |`addAnnotation` |Add annotation ( info, warning, error ) at a line, column position with a text +|_method_ |`clearAnnotations` | -## Examples -### Methods -`highlight`: Highlight a piece of code in the editor. -```typescript -const position = { // Range of code to highlight - start: { line: 1, column: 1 }, - end: { line: 1, column: 42 } -} -const file = 'browser/ballot.sol' // File to highlight -const color = '#e6e6e6' // Color of the highlight - -await client.call('editor', 'highlight', position, file, color) -// OR -await client.editor.highlight(position, file, color) -``` - -`discardHighlight`: Remove the highlight triggered by this plugin. -```typescript -await client.call('editor', 'discardHighlight') -// OR -await client.editor('discardHighlight') -``` - - -## Types -`HighlightPosition`: The positions where the highlight starts and ends. -```typescript -interface HighlightPosition { - start: { - line: number - column: number - } - end: { - line: number - column: number - } -} -``` +> Method Definitions can be found [here](../src/lib/editor/api.ts) > Type Definitions can be found [here](../src/lib/editor/type.ts) \ No newline at end of file diff --git a/packages/api/doc/file-system.md b/packages/api/doc/file-system.md index 075277ba..64f13275 100644 --- a/packages/api/doc/file-system.md +++ b/packages/api/doc/file-system.md @@ -6,7 +6,13 @@ |Type |Name |Description | |---------|-----------------------|------------| -|_event_ |`currentFileChanged` |Triggered when a file changes. +|_event_ |`currentFileChanged` |Triggered when the user opens another file. +|_event_ |`fileSaved` |Triggered when a file is saved. +|_event_ |`fileAdded` |Triggered when a file is added. +|_event_ |`fileRemoved` |Triggered when a file is removed. +|_event_ |`fileRenamed` |Triggered when a file is removed. +|_event_ |`fileClosed` |Triggered when a file is closed. +|_event_ |`noFileSelected` |Triggered when no file is selected. |_method_ |`getCurrentFile` |Get the name of the current file selected. |_method_ |`open` |Open the content of the file in the context (eg: Editor). |_method_ |`writeFile` |Set the content of a specific file. diff --git a/packages/api/doc/solidity.md b/packages/api/doc/solidity.md index 97b22ee4..aa4243a3 100644 --- a/packages/api/doc/solidity.md +++ b/packages/api/doc/solidity.md @@ -9,40 +9,10 @@ |_event_ |`compilationFinished` |Triggered when a compilation finishes. |_method_ |`getCompilationResult` |Get the current result of the compilation. |_method_ |`compile` |Run solidity compiler with a file. +|_method_ |`compileWithParameters`|Run solidity compiler with a map of source files and settings +|_method_ |`setCompilerConfig`|Set settings for the compiler, see types for more info -## Examples -### Events -`compilationFinished`: -```typescript -client.solidity.on('compilationFinished', (fileName: string, source: CompilationFileSources, languageVersion: string, data: CompilationResult) => { - // Do something -}) -// OR -client.on('solidity', 'compilationFinished', (fileName: string, source: CompilationFileSources, languageVersion: string, data: CompilationResult) => { - // Do something -}) -``` - -### Methods -`getCompilationResult`: -```typescript -const result = await client.solidity.getCompilationResult() -// OR -const result = await client.call('solidity', 'getCompilationResult') -``` - -`compile`: -```typescript -const fileName = 'browser/ballot.sol' -await client.solidity.compile(fileName) -// OR -await client.call('solidity', 'compile', 'fileName') -``` - -## Types -`CompilationFileSources`: A map with the file name as the key and the content as the value. - -`CompilationResult`: The result of the compilation matches the [Solidity Compiler Output documentation](https://solidity.readthedocs.io/en/latest/using-the-compiler.html#output-description). +> Method Definitions can be found [here](../src/lib/compiler/api.ts) > Type Definitions can be found [here](../src/lib/compiler/type) \ No newline at end of file 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 e8a098dc..224edc7e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-api", - "version": "0.3.3", + "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/api.ts b/packages/api/src/lib/compiler/api.ts index ff60c88a..81fe19a9 100644 --- a/packages/api/src/lib/compiler/api.ts +++ b/packages/api/src/lib/compiler/api.ts @@ -1,4 +1,4 @@ -import { CompilationResult, CompilationFileSources } from './type' +import { CompilationResult, CompilationFileSources, lastCompilationResult, CondensedCompilationInput, SourcesInput } from './type' import { StatusEvents, Api } from '@remixproject/plugin-utils' export interface ICompiler extends Api { @@ -11,7 +11,9 @@ export interface ICompiler extends Api { ) => void } & StatusEvents methods: { - getCompilationResult(): CompilationResult + getCompilationResult(): lastCompilationResult compile(fileName: string): void + setCompilerConfig(settings: CondensedCompilationInput): void + compileWithParameters(targets: SourcesInput, settings: CondensedCompilationInput): lastCompilationResult } } 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/compiler/type/input.ts b/packages/api/src/lib/compiler/type/input.ts index f090b772..de324d49 100644 --- a/packages/api/src/lib/compiler/type/input.ts +++ b/packages/api/src/lib/compiler/type/input.ts @@ -1,11 +1,19 @@ export interface CompilationInput { /** Source code language */ - language: 'Solidity' | 'Vyper' | 'lll' | 'assembly' + language: 'Solidity' | 'Vyper' | 'lll' | 'assembly' | 'yul' sources: SourcesInput settings?: CompilerSettings outputSelection?: CompilerOutputSelection } +export interface CondensedCompilationInput { + language: 'Solidity' | 'Vyper' | 'lll' | 'assembly' | 'yul' + optimize: boolean + /** e.g: 0.6.8+commit.0bbfe453 */ + version: string + evmVersion?: 'istanbul' | 'petersburg' | 'constantinople' | 'byzantium' | 'spuriousDragon' | 'tangerineWhistle' | 'homestead' +} + ///////////// // SOURCES // ///////////// @@ -50,7 +58,7 @@ export interface CompilerSettings { /** Metadata settings */ metadata?: CompilerMetadata /** Addresses of the libraries. If not all libraries are given here, it can result in unlinked objects whose output data is different. */ - libraries: CompilerLibrarie + libraries: CompilerLibraries } export interface CompilerOptimizer { @@ -73,7 +81,7 @@ export interface CompilerMetadata { * If remappings are used, this source file should match the global path after remappings were applied. * If this key is an empty string, that refers to a global level. */ -export interface CompilerLibrarie { +export interface CompilerLibraries { [contractName: string]: { [libName: string]: string } @@ -85,7 +93,6 @@ export interface CompilerLibrarie { export type OutputType = | 'abi' | 'ast' - | 'legacyAST' | 'devdoc' | 'userdoc' | 'metadata' @@ -104,13 +111,13 @@ export type OutputType = /** * The following can be used to select desired outputs. * If this field is omitted, then the compiler loads and does type checking, but will not generate any outputs apart from errors. - * The first level key is the file name and the second is the contract name. An empty contract name refers to the file itself, - * while an astrisk (star) refers to all of the contracts. - * Note that using `evm`, `evm.bytecode`, `ewasm`, etc. will select every + * The first level key is the file name and the second is the contract name, where empty contract name refers to the file itself, + * while the star refers to all of the contracts. + * Note that using a using `evm`, `evm.bytecode`, `ewasm`, etc. will select every * target part of that output. Additionally, `*` can be used as a wildcard to request everything. */ export interface CompilerOutputSelection { [file: string]: { [contract: string]: OutputType[] } -} +} \ No newline at end of file diff --git a/packages/api/src/lib/compiler/type/output.ts b/packages/api/src/lib/compiler/type/output.ts index 3ba46017..b3041648 100644 --- a/packages/api/src/lib/compiler/type/output.ts +++ b/packages/api/src/lib/compiler/type/output.ts @@ -2,9 +2,20 @@ // SOURCES // ///////////// export interface CompilationFileSources { - [fileName: string]: string + [fileName: string]: + { + // Optional: keccak256 hash of the source file + keccak256?: string, + // Required (unless "urls" is used): literal contents of the source file + content: string, + urls?: string[] + } } +export interface SourceWithTarget { + sources?: CompilationFileSources, + target?: string | null | undefined +} //////////// // RESULT // @@ -26,6 +37,11 @@ export interface CompilationResult { } } +export interface lastCompilationResult { + data: CompilationResult | null + source: SourceWithTarget | null | undefined +} + /////////// // ERROR // /////////// diff --git a/packages/api/src/lib/content-import/api.ts b/packages/api/src/lib/content-import/api.ts index 0edb0974..4712c510 100644 --- a/packages/api/src/lib/content-import/api.ts +++ b/packages/api/src/lib/content-import/api.ts @@ -5,5 +5,6 @@ export interface IContentImport { events: {} & StatusEvents methods: { resolve(path: string): ContentImport + resolveAndSave (url:string, targetPath: string): string } } 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/content-import/type.ts b/packages/api/src/lib/content-import/type.ts index c5f7cf89..e5a375fb 100644 --- a/packages/api/src/lib/content-import/type.ts +++ b/packages/api/src/lib/content-import/type.ts @@ -1,6 +1,6 @@ export interface ContentImport { - content: string + content: any cleanUrl: string type: 'github' | 'http' | 'https' | 'swarm' | 'ipfs' url: string -} \ No newline at end of file +} 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 db20dd1e..e9b6a31d 100644 --- a/packages/api/src/lib/editor/api.ts +++ b/packages/api/src/lib/editor/api.ts @@ -1,15 +1,21 @@ -import { HighlightPosition } from './type' +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 69173b52..bfb2eb19 100644 --- a/packages/api/src/lib/editor/type.ts +++ b/packages/api/src/lib/editor/type.ts @@ -9,9 +9,13 @@ export interface HighlightPosition { } } -export interface Annotation extends Error { - row: number; - column: number; - text: string; - type: "error" | "warning" | "information"; -} \ No newline at end of file +export interface HighLightOptions { + focus: boolean +} + +export interface Annotation { + row: number; + column: number; + text: string; + type: "error" | "warning" | "info"; +} diff --git a/packages/api/src/lib/file-system/api.ts b/packages/api/src/lib/file-system/file-manager/api.ts similarity index 55% rename from packages/api/src/lib/file-system/api.ts rename to packages/api/src/lib/file-system/file-manager/api.ts index 9d628837..54961e7c 100644 --- a/packages/api/src/lib/file-system/api.ts +++ b/packages/api/src/lib/file-system/file-manager/api.ts @@ -4,24 +4,37 @@ import { StatusEvents } from '@remixproject/plugin-utils' export interface IFileSystem { events: { 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 + fileRenamed: (oldName: string, newName:string, isFolder: boolean) => void } & StatusEvents methods: { /** Open the content of the file in the context (eg: Editor) */ - open(path: string): void; + open(path: string): void /** Set the content of a specific file */ - writeFile(path: string, data: string): void; + writeFile(path: string, data: string): void /** Return the content of a specific file */ - readFile(path: string): string; + readFile(path: string): string /** Change the path of a file */ - rename(oldPath: string, newPath: string): void; + rename(oldPath: string, newPath: string): void /** Upsert a file with the content of the source file */ - copyFile(src: string, dest: string): void; + copyFile(src: string, dest: string): void /** Create a directory */ - mkdir(path: string): void; + mkdir(path: string): void /** Get the list of files in the directory */ - readdir(path: string): string[]; + readdir(path: string): string[] + /** Removes a file or directory recursively */ + 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/network/api.ts b/packages/api/src/lib/network/api.ts index c2dc9c44..332be1ea 100644 --- a/packages/api/src/lib/network/api.ts +++ b/packages/api/src/lib/network/api.ts @@ -1,6 +1,7 @@ -import { Network, CustomNetwork, NetworkProvider } from './type' -import { StatusEvents } from '@remixproject/plugin-utils' +import { StatusEvents } from '@remixproject/plugin-utils'; +import { NetworkProvider, Network, CustomNetwork } from './type'; +/** @deprecated: current version in Remix IDE. To improve to match standard JSON RPC methods */ export interface INetwork { events: { providerChanged: (provider: NetworkProvider) => void @@ -12,4 +13,4 @@ export interface INetwork { addNetwork(network: CustomNetwork): void removeNetwork(name: string): void } -} +} \ No newline at end of file diff --git a/packages/api/src/lib/network/profile.ts b/packages/api/src/lib/network/profile.ts index dac27d9c..868f6caa 100644 --- a/packages/api/src/lib/network/profile.ts +++ b/packages/api/src/lib/network/profile.ts @@ -5,4 +5,4 @@ export const networkProfile: LibraryProfile = { name: 'network', methods: ['addNetwork', 'detectNetwork', 'getEndpoint', 'getNetworkProvider', 'removeNetwork'], events: ['providerChanged'] -} +} \ No newline at end of file diff --git a/packages/api/src/lib/network/type.ts b/packages/api/src/lib/network/type.ts index 15a0fca1..9376bb6a 100644 --- a/packages/api/src/lib/network/type.ts +++ b/packages/api/src/lib/network/type.ts @@ -1,14 +1,17 @@ +/** @deprecated: current version in Remix IDE. To improve to match standard JSON RPC methods */ export interface CustomNetwork { id?: string name: string url: string } +/** @deprecated: current version in Remix IDE. To improve to match standard JSON RPC methods */ +export type NetworkProvider = 'vm' | 'injected' | 'web3' + export type Network = | { id: '1', name: 'Main' } | { id: '2', name: 'Morden (deprecated)' } | { id: '3', name: 'Ropsten' } | { id: '4', name: 'Rinkeby' } - | { id: '42', name: 'Kovan' } - -export type NetworkProvider = 'vm' | 'injected' | 'web3' \ No newline at end of file + | { id: '5', name: 'Goerli' } + | { id: '42', name: 'Kovan' } \ 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/theme/types.ts b/packages/api/src/lib/theme/types.ts index e6cd7097..93112290 100644 --- a/packages/api/src/lib/theme/types.ts +++ b/packages/api/src/lib/theme/types.ts @@ -1,5 +1,4 @@ export interface Theme { - /** @deprecated Use colors directly instead */ url?: string /** @deprecated Use brightness instead */ quality?: 'dark' | 'light' @@ -30,4 +29,9 @@ export interface Theme { fontFamily: string /** A unit to multiply for margin & padding */ space: number -} \ No newline at end of file +} + +export interface ThemeUrls { + light: string; + dark: string; +} diff --git a/packages/api/src/lib/udapp/api.ts b/packages/api/src/lib/udapp/api.ts index 54c7a9e3..c059ba4b 100644 --- a/packages/api/src/lib/udapp/api.ts +++ b/packages/api/src/lib/udapp/api.ts @@ -1,6 +1,5 @@ -import { RemixTx, RemixTxReceipt, RemixTxEvent, VMAccount } from './type' +import { RemixTx, RemixTxReceipt, RemixTxEvent, VMAccount, UdappSettings } from './type' import { StatusEvents } from '@remixproject/plugin-utils' - export interface IUdapp { events: { newTransaction: (transaction: RemixTxEvent) => void @@ -9,5 +8,7 @@ export interface IUdapp { sendTransaction(tx: RemixTx): RemixTxReceipt getAccounts(): string[] createVMAccount(vmAccount: VMAccount): string + getSettings(): UdappSettings + setEnvironmentMode(env: 'vm' | 'injected' | 'web3'): void } } diff --git a/packages/api/src/lib/udapp/profile.ts b/packages/api/src/lib/udapp/profile.ts index e165ae31..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'], + methods: ['createVMAccount', 'getAccounts', 'sendTransaction', 'getSettings', 'setEnvironmentMode'], events: ['newTransaction'] } diff --git a/packages/api/src/lib/udapp/type.ts b/packages/api/src/lib/udapp/type.ts index 435231a4..218a294e 100644 --- a/packages/api/src/lib/udapp/type.ts +++ b/packages/api/src/lib/udapp/type.ts @@ -55,4 +55,10 @@ export interface RemixTxReceipt { export interface VMAccount { privateKey: string balance: string -} \ No newline at end of file +} + +export interface UdappSettings { + selectedAccount:string + selectedEnvMode: 'vm' | 'injected' | 'web3' + networkEnvironment: string +} 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/README.md b/packages/engine/core/README.md index 9174675d..0186ae56 100644 --- a/packages/engine/core/README.md +++ b/packages/engine/core/README.md @@ -143,4 +143,75 @@ manager.activatePlugin(['empty', 'console']) // Plugin communication emptyPlugin.call('console', 'print', 'My message') -``` \ No newline at end of file +``` + +-------------- + +## Permission +The Engine comes with a permission system to protect the user from hostile plugins. +There are two levels: +- **Global**: at the `PluginManager` level. +- **Local**: at the `Plugin` level. + +### Global Permission +Communication between plugins goes through the `PluginManager`'s permission system : + +```typescript +canActivatePlugin(from: Profile, to: Profile): Promise +``` +Used when a plugin attempts to activate another one. By default when plugin "A" calls plugin "B", if "B" is not deactivated, "A" will attempt to active it before performing the call. + +```typescript +canDeactivatePlugin(from: Profile, to: Profile): Promise +``` +Used when a plugin attempts to deactivate another one. By default only the `manager` and the plugin itself can deactivate a plugin. + +```typescript +canCall(from: Profile, to: Profile, method: string, message: string): Promise +``` +Used by a plugin to protect a method (see Local Permission below). + +**Tip**: Each method returns a `Promise`. It's good practice to ask the user's permission through a GUI. + + +### Local Permission +A plugin can protect some critical API by asking for user's permission: + +```typescript +askUserPermission(method: string, message: string): Promise +``` +This method will call the `canCall` method from the `PluginManager` under the hood with the right params. + +In this example, a FileSystem plugin protects the `write` method : +```typescript +class FileSystemPlugin extends Plugin { + + async write() { + const from = this.currentRequest + const canCall = await this.askUserPermission('write') + if (!canCall) { + throw new Error('You do not have the permission to call method "canCall" from "fs"') + } + } +} +``` + +### ⚠️ When currentRequest is Mistaken ⚠️ +The permission system heavily relies on a queue of calls managed by the `Engine` and the property `currentRequest`. +If you're calling a method from the plugin directly (without using the `Engine`) it will bypass the permission system. In this case, the results of `currentRequest` may **NOT** be correct. + +Example : +```typescript +const fs = new FileSystemPlugin() +const manager = new PluginManager() +... +fs.call('manager', 'activatePlugin', 'terminal') // At this point `currentRequest` in manager is "fs" +manager.deactivatePlugin('editor') // This will fail +``` + +In the code above : +1. call to "activatePlugin" to enter the queue of the manager. +2. manager's `currentRequest` is "fs". +3. manager calls its own `deactivatePlugin` method. +4. **as the call doesn't use the Engine, it doesn't enter in the queue**: so `currentRequest` is still "fs". +5. `deactivatePlugin` checks the `currentRequest`. So now `currentRequest` incorrectly thinks that "fs" is trying to deactivate "terminal" and will not allow it. \ No newline at end of file diff --git a/packages/engine/core/package.json b/packages/engine/core/package.json index 7fb86b09..7078f545 100644 --- a/packages/engine/core/package.json +++ b/packages/engine/core/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/engine", - "version": "0.3.3", + "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 db7d7c72..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,47 +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 = () => { - delete this.currentRequest - 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 - 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) { - 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() + } } @@ -229,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/src/lib/manager.ts b/packages/engine/core/src/lib/manager.ts index 52dadec0..f79e4c49 100644 --- a/packages/engine/core/src/lib/manager.ts +++ b/packages/engine/core/src/lib/manager.ts @@ -186,13 +186,17 @@ export class PluginManager extends Plugin implements BasePluginManager { if (from.name === 'manager') { return this.toggleActive(name) } + // Check manager rules const managerCanDeactivate = await this.canDeactivatePlugin(from, to) - const pluginCanDeactivate = await this.call(to.name, 'canDeactivate', from) - if (managerCanDeactivate && pluginCanDeactivate) { - return this.toggleActive(name) - } else { + if (!managerCanDeactivate) { + throw new Error(`Plugin ${this.requestFrom} has no right to deactivate plugin ${name}`) + } + // Ask plugin, if it wasn't the one which called on the first place + const pluginCanDeactivate = from.name !== to.name ? await this.call(to.name, 'canDeactivate', from) : true + if (!pluginCanDeactivate) { throw new Error(`Plugin ${this.requestFrom} has no right to deactivate plugin ${name}`) } + return this.toggleActive(name) } return Array.isArray(names) ? catchAllPromises(names.map(deactivate)) : deactivate(names) } 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/core/tests/library.spec.ts b/packages/engine/core/tests/library.spec.ts deleted file mode 100644 index 986b182d..00000000 --- a/packages/engine/core/tests/library.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Engine, PluginManager, LibraryPlugin, LibraryApi } from '../src' -import { pluginManagerProfile } from '@remixproject/plugin-api' - -const createLib = () => ({ - events: { - on: jest.fn(), - once: jest.fn(), - off: jest.fn(), - emit: jest.fn() - }, - mockMethod: jest.fn(), - fakeMethod: jest.fn() -}) - -type Lib = ReturnType - -const profile = { - name: 'library', - methods: ['mockMethod'], - events: ['event'], - notifications: { - manager: ['profileUpdated'] - } -} - -class MockLibrary extends LibraryPlugin { - call: jest.Mock - on: jest.Mock - off: jest.Mock - emit: jest.Mock - onDeactivation = jest.fn() - onRegistration = jest.fn() - onActivation = jest.fn().mockImplementation(() => this.createMock()) - constructor(library: LibraryApi) { - super(library, profile) - } - createMock() { - this.call = jest.fn() - this.on = jest.fn() - this.once = jest.fn() - this.off = jest.fn() - this.emit = jest.fn() - } - -} - -// Library without UI -describe('Library Plugin', () => { - let manager: PluginManager - let library: MockLibrary - let lib: Lib - - beforeEach(() => { - lib = createLib() - manager = new PluginManager(pluginManagerProfile) - library = new MockLibrary(lib) - const engine = new Engine() - engine.register([ manager, library ]) - }) - - test('Activation', async () => { - await library.activate() - expect(library.onActivation).toHaveBeenCalled() - // Listen on manager profileUpdated - expect(library.on.mock.calls[0][0]).toEqual('manager') - expect(library.on.mock.calls[0][1]).toEqual('profileUpdated') - // Listen on events from library - expect(lib.events.on.mock.calls[0][0]).toEqual('event') - }) - - test('Throw if library event does not match interface for notifications', async () => { - try { - library['library']['events'] = { notifications: {} } as any - await library.activate() - } catch (err) { - expect(err.message).toEqual('"events" object from Library of plugin library should implement "emit"') - } - }) - - test('Throw if library event does not match interface for events', async () => { - try { - library['library']['events'] = { events: {} } as any - await library.activate() - } catch (err) { - expect(err.message).toEqual('"events" object from Library of plugin library should implement "emit"') - } - }) - - test('Call library method', () => { - library['callPluginMethod']('mockMethod', [true]) - expect(lib.mockMethod).toHaveBeenCalledWith(true) - }) - - test('Call library method not exposed should fail', () => { - try { - library['callPluginMethod']('fakeMethod', [true]) - } catch (err) { - expect(err.message).toEqual("LibraryPlugin library doesn't expose method fakeMethod") - } - }) - - test('Deactivation', async () => { - library.createMock() // Make sure methods are mocked - library.deactivate() - expect(library.onDeactivation).toHaveBeenCalled() - // Stop listening on manager profileUpdated - expect(library.off.mock.calls[0][0]).toEqual('manager') - expect(library.off.mock.calls[0][1]).toEqual('profileUpdated') - // Stop listening on events from library - expect(lib.events.off.mock.calls[0][0]).toEqual('event') - }) -}) \ No newline at end of file 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 992e4d83..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.3", + "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 2296d83f..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.3" + "version": "0.3.37" } diff --git a/packages/engine/vscode/package.json b/packages/engine/vscode/package.json index 36d807bd..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.3", + "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 c202415a..e2703a9e 100644 --- a/packages/engine/vscode/src/lib/command.ts +++ b/packages/engine/vscode/src/lib/command.ts @@ -1,8 +1,9 @@ -import { Plugin, PluginOptions } from "@remixproject/engine" -import { Profile } from '@remixproject/plugin-utils' -import { Disposable, commands } from "vscode" +import { Plugin } from '@remixproject/engine' +import { Profile, PluginOptions } from '@remixproject/plugin-utils' +import { Disposable, commands } from 'vscode' -export const transformCmd = (name: string, method: string) => `${name}.${method}` +export const transformCmd = (name: string, method: string) => + `remix.${name}.${method}` export interface CommandOptions extends PluginOptions { transformCmd: (name: string, method: string) => string @@ -18,7 +19,9 @@ export class CommandPlugin extends Plugin { constructor(profile: Profile) { super(profile) - this.setOptions({ transformCmd }) + this.setOptions({ + transformCmd, + }) } setOptions(options: Partial) { @@ -26,15 +29,25 @@ export class CommandPlugin extends Plugin { } activate() { - this.subscriptions = this.profile.methods.map(method => { - const cmd = this.options.transformCmd(this.profile.name, method) - return commands.registerCommand(cmd, (...args) => this.callPluginMethod(method, args)) - }) + this.subscriptions = this.profile.methods + .map((method) => { + try { + const cmd = this.options.transformCmd(this.profile.name, method) + return commands.registerCommand(cmd, (...args) => + this.callPluginMethod(method, args) + ) + } catch (err) { + console.log(err) + } + }) + .filter((command) => { + return command + }) super.activate() } deactivate() { super.deactivate() - this.subscriptions.forEach(sub => sub.dispose()) + this.subscriptions.forEach((sub) => sub.dispose()) } -} +} \ No newline at end of file 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 11632d9d..f29e92a6 100644 --- a/packages/engine/vscode/src/lib/editor.ts +++ b/packages/engine/vscode/src/lib/editor.ts @@ -1,40 +1,46 @@ import { editorProfile, IEditor, Annotation, HighlightPosition } from '@remixproject/plugin-api'; import { MethodApi } from '@remixproject/plugin-utils'; -import { window, Range, TextEditorDecorationType, Position, languages, DiagnosticCollection, Diagnostic, Uri, DiagnosticSeverity, TextEditor } from "vscode"; +import { window, Range, TextEditorDecorationType, Position, languages, DiagnosticCollection, Diagnostic, Uri, DiagnosticSeverity, TextEditor, ThemeColor } from "vscode"; import { CommandPlugin, CommandOptions } from "./command"; +import { absolutePath } from '../util/path' function getEditor(filePath?: string): TextEditor { const editors = window.visibleTextEditors; return filePath ? editors.find(editor => editor.document.uri.path === Uri.parse(filePath).path) : window.activeTextEditor } +function extractColor(themeColor: string): string { + const [content] = themeColor.match(/(?<=\().+?(?=\))/g); + const value = content.substring(2); + return value.split('-').join('.').replace('vscode.', ''); +} + export interface EditorOptions extends CommandOptions { language: string; } export class EditorPlugin extends CommandPlugin implements MethodApi { private decoration: TextEditorDecorationType; + private decorations: Array; private diagnosticCollection: DiagnosticCollection; public options: EditorOptions; constructor(options: EditorOptions) { super(editorProfile); super.setOptions(options); + this.decorations = []; } setOptions(options: EditorOptions) { super.setOptions(options); } onActivation() { - this.decoration = window.createTextEditorDecorationType({ - backgroundColor: 'editor.lineHighlightBackground', - isWholeLine: true, - }); this.diagnosticCollection = languages.createDiagnosticCollection(this.options.language); } onDeactivation() { this.decoration.dispose(); } - async highlight(position: HighlightPosition, filePath: string, hexColor: string): Promise { + async highlight(position: HighlightPosition, filePath: string, themeColor: string): Promise { + filePath = absolutePath(filePath) const editors = window.visibleTextEditors; // Parse `filePath` to ensure if a valid file path was supplied const editor = editors.find(editor => editor.document.uri.path === Uri.parse(filePath).path); @@ -42,19 +48,21 @@ export class EditorPlugin extends CommandPlugin implements MethodApi { const start: Position = new Position(position.start.line, position.start.column); const end: Position = new Position(position.end.line, position.end.column); const newDecoration = { range: new Range(start, end) }; - if (hexColor) { - this.decoration = window.createTextEditorDecorationType({ - backgroundColor: hexColor, - isWholeLine: true, - }); - } + this.decoration = window.createTextEditorDecorationType({ + backgroundColor: new ThemeColor('editor.wordHighlightStrongBackground'), + isWholeLine: true, + }); + this.decorations.push(this.decoration); editor.setDecorations(this.decoration, [newDecoration]); } else { throw new Error(`Could not find file ${filePath}`); } } + async discardDecorations(): Promise { + return this.decorations?.forEach(decoration => decoration.dispose()); + } async discardHighlight(): Promise { - return this.decoration.dispose(); + return this.decorations?.forEach(decoration => decoration.dispose()); } /** * Alisas of discardHighlight @@ -67,6 +75,7 @@ export class EditorPlugin extends CommandPlugin implements MethodApi { // This function should append to existing map // Ref: https://code.visualstudio.com/api/language-extensions/programmatic-language-features#provide-diagnostics // const fileUri = window.activeTextEditor ? window.activeTextEditor.document.uri : undefined; // TODO: we might want to supply path to addAnnotation function + filePath = absolutePath(filePath) const editor = getEditor(filePath); const canonicalFile: string = editor.document.uri.fsPath; const diagnostics: Diagnostic[] = []; @@ -84,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 5349c6f5..3fa86ed7 100644 --- a/packages/engine/vscode/src/lib/filemanager.ts +++ b/packages/engine/vscode/src/lib/filemanager.ts @@ -1,77 +1,94 @@ import { filSystemProfile, IFileSystem } from '@remixproject/plugin-api' -import { MethodApi } from '@remixproject/plugin-utils'; -import { isAbsolute, join } from 'path'; -import { window, workspace, Uri, commands } from 'vscode'; -import { CommandPlugin } from './command'; - -function absolutePath(path: string) { - const root = workspace.workspaceFolders[0].uri.fsPath; - if (isAbsolute(path) || !root) { - return path; - } - return join(root, path); -} +import { MethodApi } from '@remixproject/plugin-utils' +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() { - super(filSystemProfile); + super(filSystemProfile) } /** Open the content of the file in the context (eg: Editor) */ async open(path: string): Promise { - const absPath = absolutePath(path); - const uri = Uri.file(absPath); - return commands.executeCommand('vscode.open', uri); + const absPath = absolutePath(path) + const uri = Uri.file(absPath) + 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 { - const absPath = absolutePath(path); - const uri = Uri.file(absPath); - const encoder = new TextEncoder(); - const uint8Array = encoder.encode(data); - return workspace.fs.writeFile(uri, Uint8Array.from(uint8Array)); + const absPath = absolutePath(path) + const uri = Uri.file(absPath) + const encoder = new TextEncoder() + const uint8Array = encoder.encode(data) + this.logMessage(' is modifying ' + path) + return workspace.fs.writeFile(uri, Uint8Array.from(uint8Array)) } /** Return the content of a specific file */ async readFile(path: string): Promise { - const absPath = absolutePath(path); - const uri = Uri.file(absPath); - return workspace.fs.readFile(uri).then(content => Buffer.from(content).toString("utf-8")); + const absPath = absolutePath(path) + const uri = Uri.file(absPath) + return workspace.fs.readFile(uri).then(content => Buffer.from(content).toString("utf-8")) + } + /** Remove a file */ + async remove(path: string): Promise { + 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)); - return workspace.fs.rename(source, target); + 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 */ async copyFile(src: string, dest: string): Promise { - const source = Uri.file(absolutePath(src)); - const target = Uri.file(absolutePath(dest)); - return workspace.fs.copy(source, target); + const source = Uri.file(absolutePath(src)) + const target = Uri.file(absolutePath(dest)) + return workspace.fs.copy(source, target) } /** Create a directory */ async mkdir(path: string): Promise { - const uri = Uri.file(absolutePath(path)); - return workspace.fs.createDirectory(uri); + const uri = Uri.file(absolutePath(path)) + this.logMessage(' is creating ' + path) + return workspace.fs.createDirectory(uri) } /** Get the list of files in the directory */ async readdir(path: string): Promise { - const absPath = absolutePath(path); - const uri = Uri.file(absPath); - return workspace.fs.readDirectory(uri).then(data => data.map(([path]) => path)); + const absPath = absolutePath(path) + const uri = Uri.file(absPath) + return workspace.fs.readDirectory(uri).then(data => data.map(([path]) => path)) } async getCurrentFile() { - const fileName = window.activeTextEditor ? window.activeTextEditor.document.fileName : undefined; - return fileName; + const fileName = (getOpenedTextEditor()?.document?.fileName || undefined) + if(!fileName) throw new Error("No current file found.") + return relativePath(fileName) + } + + async closeFile() { + return null + } + + async closeAllFiles() { + return null + } + + logMessage(message){ + if(this.currentRequest && this.currentRequest.from) + window.showInformationMessage(this.currentRequest.from + message); } // ------------------------------------------ // Legacy API. To be removed. // ------------------------------------------ getFile = this.readFile - setFile = this.writeFile; - switchFile = this.open; + setFile = this.writeFile + switchFile = this.open /** @deprecated Use readdir instead */ getFolder(path: string): Promise { throw new Error('Get folder is not supported anymore') } -} \ No newline at end of file +} diff --git a/packages/engine/vscode/src/lib/theme.ts b/packages/engine/vscode/src/lib/theme.ts index 3a9dbc9c..b96329be 100644 --- a/packages/engine/vscode/src/lib/theme.ts +++ b/packages/engine/vscode/src/lib/theme.ts @@ -1,10 +1,14 @@ import { Plugin } from '@remixproject/engine' -import { API } from '@remixproject/plugin-utils' -import { ITheme, Theme, themeProfile } from '@remixproject/plugin-api' +import { API, PluginOptions } from '@remixproject/plugin-utils' +import { ITheme, Theme, ThemeUrls, themeProfile } from '@remixproject/plugin-api' import { window, ColorThemeKind, Disposable, ColorTheme } from 'vscode' +export interface ThemeOptions extends PluginOptions { + urls?: Partial +} + // There is no way to get the value from the theme so the best solution is to reference the css varibles in webview -function getTheme(color: ColorTheme): Theme { +export function getVscodeTheme(color: ColorTheme, urls: Partial = {}): Theme { const brightness = color.kind === ColorThemeKind.Dark ? 'dark' : 'light'; return { brightness, @@ -32,18 +36,28 @@ function getTheme(color: ColorTheme): Theme { xl: 1920 }, fontFamily: 'Segoe WPC,Segoe UI,sans-serif', - space: 5, + space: 1, + url: urls[brightness] } } export class ThemePlugin extends Plugin implements API { + protected getTheme = getVscodeTheme; + protected options: ThemeOptions listener: Disposable - constructor() { + constructor(options: Partial = {}) { super(themeProfile) + super.setOptions(options) + } + + setOptions(options: Partial) { + super.setOptions(options) } onActivation() { - this.listener = window.onDidChangeActiveColorTheme(color => this.emit('themeChanged', getTheme(color))) + this.listener = window.onDidChangeActiveColorTheme(color => { + this.emit('themeChanged', this.getTheme(color, this.options.urls)) + }) } onDeactivation() { @@ -51,7 +65,7 @@ export class ThemePlugin extends Plugin implements API { } currentTheme(): Theme { - return getTheme(window.activeColorTheme) + return this.getTheme(window.activeColorTheme, this.options.urls) } } diff --git a/packages/engine/vscode/src/lib/webview.ts b/packages/engine/vscode/src/lib/webview.ts index 3fe60041..a409536f 100644 --- a/packages/engine/vscode/src/lib/webview.ts +++ b/packages/engine/vscode/src/lib/webview.ts @@ -1,11 +1,11 @@ import { PluginConnector, PluginConnectorOptions} from '@remixproject/engine' import { Message, Profile, ExternalProfile } from '@remixproject/plugin-utils' -import { ExtensionContext, ViewColumn, Webview, WebviewPanel, window, Uri, Disposable, workspace } from 'vscode' +import { ExtensionContext, ViewColumn, Webview, WebviewPanel, window, Uri, Disposable, workspace, env } from 'vscode' import { join, isAbsolute, parse as parsePath } from 'path' import { promises as fs, watch } from 'fs' -import { get } from 'https' import { parse as parseUrl } from 'url' + interface WebviewOptions extends PluginConnectorOptions { /** Extension Path */ context: ExtensionContext @@ -21,6 +21,7 @@ export class WebviewPlugin extends PluginConnector { constructor(profile: Profile & ExternalProfile, options: WebviewOptions) { super(profile) + options.engine = 'vscode' this.setOptions(options) } @@ -29,14 +30,12 @@ export class WebviewPlugin extends PluginConnector { } protected send(message: Partial): void { - if (this.panel) { - this.panel.webview.postMessage(message) - } + this.panel?.webview.postMessage(message) } - protected connect(url: string): void { + protected async connect(url: string): Promise { 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)), @@ -47,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()); } @@ -57,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; @@ -135,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 new file mode 100644 index 00000000..11a6b092 --- /dev/null +++ b/packages/engine/vscode/src/util/path.ts @@ -0,0 +1,29 @@ +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) { + return path + } + if(path.startsWith(root)) { + path = relative(root, path) + } + const result = join(root, path) + if (!result.startsWith(root)) { + throw new Error(`Resolved path is should be inside the open workspace : "${root}". Got "${result}`) + } + return result +} + +export function relativePath(path) { + const root = workspace.workspaceFolders[0].uri.fsPath + // vscode API will never get permission to WriteFile outside of its workspace directory + if (!root) { + return path + } + return relative(root, path) +} + diff --git a/packages/engine/web/package.json b/packages/engine/web/package.json index 819852bd..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.3", + "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/theme.ts b/packages/engine/web/src/lib/theme.ts index ff312d14..5513d625 100644 --- a/packages/engine/web/src/lib/theme.ts +++ b/packages/engine/web/src/lib/theme.ts @@ -49,14 +49,16 @@ export function createTheme(params: DeepPartial = {}): Theme { } export class ThemePlugin extends Plugin implements MethodApi { - protected theme: Theme = createTheme() + protected getTheme = createTheme + protected theme: Theme constructor() { super(themeProfile) + this.theme = this.getTheme() } /** Internal API to set the current theme */ setTheme(theme: DeepPartial) { - this.theme = createTheme(theme) + this.theme = this.getTheme(theme) this.emit('themeChanged', this.theme) } 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/engine/web/tests/iframe.spec.ts b/packages/engine/web/tests/iframe.spec.ts index 4d7faef1..47370eda 100644 --- a/packages/engine/web/tests/iframe.spec.ts +++ b/packages/engine/web/tests/iframe.spec.ts @@ -1,4 +1,5 @@ -import { Engine, PluginManager, HostPlugin } from '@remixproject/engine' +import { Engine, PluginManager } from '@remixproject/engine' +import { HostPlugin } from '../src' import { pluginManagerProfile } from '@remixproject/plugin-api' import { IframePlugin } from '../src' diff --git a/packages/engine/core/tests/view.spec.ts b/packages/engine/web/tests/view.spec.ts similarity index 94% rename from packages/engine/core/tests/view.spec.ts rename to packages/engine/web/tests/view.spec.ts index 66678f6a..ea48cb67 100644 --- a/packages/engine/core/tests/view.spec.ts +++ b/packages/engine/web/tests/view.spec.ts @@ -1,4 +1,5 @@ -import { Engine, ViewPlugin, PluginManager, HostPlugin } from '../src' +import { Engine, PluginManager } from '../../core/src' +import { HostPlugin, ViewPlugin } from '../src' import { pluginManagerProfile } from '@remixproject/plugin-api' export class MockEngine extends Engine { diff --git a/packages/plugin/child-process/package.json b/packages/plugin/child-process/package.json index ea21cf92..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.3", + "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 7dbfde60..d9e01dca 100644 --- a/packages/plugin/core/package.json +++ b/packages/plugin/core/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin", - "version": "0.3.3", + "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..68360902 100644 --- a/packages/plugin/core/src/lib/client.ts +++ b/packages/plugin/core/src/lib/client.ts @@ -131,13 +131,21 @@ export class PluginClient im const callName = callEvent(name, key, this.id) this.events.once(callName, (result: any, error) => { error - ? rej(new Error(`Error from IDE : ${error}`)) + ? rej(new Error(error)) : res(result) }) this.events.emit('send', { action: 'request', name, key, payload, id: this.id }) }) } + 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/core/tests/client.spec.ts b/packages/plugin/core/tests/client.spec.ts index f064371b..8ee220ec 100644 --- a/packages/plugin/core/tests/client.spec.ts +++ b/packages/plugin/core/tests/client.spec.ts @@ -43,7 +43,7 @@ describe('Client is not loaded yet', () => { test('Call should throw when client is not loaded', async () => { expect(() => client.call('fileManager', 'getFile', 'browser/ballot.sol')) - .toThrow('Not connected to the IDE. Make sure the port of the IDE is 8080') + .toThrow('Not connected to the IDE. If you are using a local IDE, make sure to add devMode in client options') }) }) @@ -80,7 +80,7 @@ describe('Client is loaded', () => { test('Call should throw an error', (done) => { const name = 'fileManager', key = 'getFile', payload = 'browser/ballot.sol' client.call(name, key, payload).catch((error) => { - expect(error.message).toBe('Error from IDE : error') + expect(error.message).toBe('error') done() }) client.events.emit(callEvent(name, key, 1), undefined, 'error') @@ -133,4 +133,4 @@ describe('Client is loaded', () => { client.emit(key, payload) }) -}) \ No newline at end of file +}) 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/README.md b/packages/plugin/iframe/README.md index 34498bac..9b980c5c 100644 --- a/packages/plugin/iframe/README.md +++ b/packages/plugin/iframe/README.md @@ -1,6 +1,6 @@ # Plugin frame -**Except if you want your plugin to ONLY work on the web, prefer [@remixproject/plugin-webview](../webview/readme.md)** +**Except if you want your plugin to ONLY work on the web, prefer [@remixproject/plugin-webview](../webview)** This library provides connectors to connect a plugin to an engine running in a web environment. ``` @@ -32,4 +32,4 @@ const client = createClient() client.onload(async () => { const data = client.call('filemanager', 'readFile', 'ballot.sol') }) -``` \ No newline at end of file +``` diff --git a/packages/plugin/iframe/package.json b/packages/plugin/iframe/package.json index c18ec85a..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.3", + "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 912448bc..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.3" + "version": "0.3.37" } diff --git a/packages/plugin/vscode/package.json b/packages/plugin/vscode/package.json index 0a333557..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.3", + "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 55147586..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.3", + "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 4cbbe3d9..f9d2aaba 100644 --- a/packages/plugin/webview/src/lib/connector.ts +++ b/packages/plugin/webview/src/lib/connector.ts @@ -9,13 +9,19 @@ 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; +} + /** * This Webview connector @@ -26,9 +32,16 @@ export class WebviewConnector implements ClientConnector { isVscode: boolean constructor(private options: PluginOptions) { - this.isVscode = ('acquireVsCodeApi' in window) - // Check the parent source here - this.source = this.isVscode ? window['acquireVsCodeApi']() : window.parent + // @todo(#295) check if we can merge this statement in `this.isVscode = acquireVsCodeApi !== undefined` + try { + this.isVscode = acquireTheiaApi !== undefined + this.source = acquireTheiaApi() + return + } catch (e) { + this.isVscode = false + } + // fallback to window parent (iframe) + this.source = window.parent } @@ -47,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) { @@ -56,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; + }; + } } /** @@ -70,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 */ @@ -119,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 62a6fc3f..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.3", + "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 1fafde6f..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.3", + "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 3adafc52..4fa206c9 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@remixproject/plugin-utils", - "version": "0.3.3", + "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": {