Merge pull request #4919 from ethereum/desktop-master

Desktop master
pull/5209/head
bunsenstraat 2 months ago committed by GitHub
commit 06512f3a2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 368
      .circleci/config.yml
  2. 2
      .gitignore
  3. 2
      apps/remix-ide-e2e/src/commands/addLocalPlugin.ts
  4. 7
      apps/remix-ide-e2e/src/tests/dgit_github.test.ts
  5. 1
      apps/remix-ide-e2e/src/tests/dgit_local.test.ts
  6. 5
      apps/remix-ide-e2e/src/tests/workspace_git.test.ts
  7. 242
      apps/remix-ide/ci/update_desktop_release_assets.ts
  8. 126
      apps/remix-ide/src/app.js
  9. 621
      apps/remix-ide/src/app/files/dgitProvider.ts
  10. 6
      apps/remix-ide/src/app/panels/file-panel.js
  11. 54
      apps/remix-ide/src/app/plugins/electron/appUpdaterPlugin.ts
  12. 13
      apps/remix-ide/src/app/plugins/electron/foundryPlugin.ts
  13. 13
      apps/remix-ide/src/app/plugins/electron/hardhatPlugin.ts
  14. 13
      apps/remix-ide/src/app/plugins/electron/slitherPlugin.ts
  15. 2
      apps/remix-ide/src/app/plugins/permission-handler-plugin.tsx
  16. 23
      apps/remix-ide/src/app/plugins/remix-templates.ts
  17. 2
      apps/remix-ide/src/app/plugins/remixGuide.tsx
  18. 2
      apps/remix-ide/src/app/plugins/remixGuideData.json
  19. 6
      apps/remix-ide/src/app/tabs/compile-tab.js
  20. 4
      apps/remix-ide/src/app/tabs/locales/en/electron.json
  21. 3
      apps/remix-ide/src/app/tabs/locales/en/gitui.json
  22. 47
      apps/remix-ide/src/assets/js/loader.js
  23. 4
      apps/remix-ide/src/remixAppManager.js
  24. 152
      apps/remixdesktop/README.md
  25. 14
      apps/remixdesktop/after-pack.js
  26. 30
      apps/remixdesktop/afterbuild.js
  27. 104
      apps/remixdesktop/aftersign.js
  28. 61
      apps/remixdesktop/alpha.json
  29. 61
      apps/remixdesktop/beta.json
  30. 13
      apps/remixdesktop/entitlements.mac.plist
  31. 12
      apps/remixdesktop/esbuild.js
  32. 62
      apps/remixdesktop/insiders.json
  33. 61
      apps/remixdesktop/latest.json
  34. 344
      apps/remixdesktop/nightwatch.conf.js
  35. 39
      apps/remixdesktop/notarizedmg.sh
  36. 114
      apps/remixdesktop/package.json
  37. 19
      apps/remixdesktop/run_ci_test.sh
  38. 14
      apps/remixdesktop/run_git_ui_isogit_tests.sh
  39. 24
      apps/remixdesktop/rundist.bash
  40. 24
      apps/remixdesktop/rundist_esbuild.bash
  41. 24
      apps/remixdesktop/rundist_tsc.bash
  42. 24
      apps/remixdesktop/rundist_webpack.bash
  43. 35
      apps/remixdesktop/splice_tests.js
  44. 31
      apps/remixdesktop/src/engine.ts
  45. 37
      apps/remixdesktop/src/lib/remixd.ts
  46. 24
      apps/remixdesktop/src/lib/utils.ts
  47. 73
      apps/remixdesktop/src/main.ts
  48. 31
      apps/remixdesktop/src/menus/file.ts
  49. 122
      apps/remixdesktop/src/plugins/appUpdater.ts
  50. 49
      apps/remixdesktop/src/plugins/compilerLoader.ts
  51. 248
      apps/remixdesktop/src/plugins/foundryPlugin.ts
  52. 130
      apps/remixdesktop/src/plugins/fsPlugin.ts
  53. 220
      apps/remixdesktop/src/plugins/hardhatPlugin.ts
  54. 285
      apps/remixdesktop/src/plugins/isoGitPlugin.ts
  55. 29
      apps/remixdesktop/src/plugins/ripgrepPlugin.ts
  56. 197
      apps/remixdesktop/src/plugins/slitherPlugin.ts
  57. 24
      apps/remixdesktop/src/plugins/templates.ts
  58. 92
      apps/remixdesktop/src/plugins/xtermPlugin.ts
  59. 18
      apps/remixdesktop/src/preload.ts
  60. 84
      apps/remixdesktop/src/tools/git.ts
  61. 9
      apps/remixdesktop/src/types/index.ts
  62. 2
      apps/remixdesktop/src/utils/config.ts
  63. 51
      apps/remixdesktop/src/utils/matamo.ts
  64. 2
      apps/remixdesktop/test/cache_dir/remixdesktop.json
  65. 195
      apps/remixdesktop/test/lib/git.ts
  66. 104
      apps/remixdesktop/test/nighwatch.app.ts
  67. 37
      apps/remixdesktop/test/tests/app/compiler.test.ts
  68. 122
      apps/remixdesktop/test/tests/app/externaleditor.test.ts
  69. 157
      apps/remixdesktop/test/tests/app/foundry.test.ts
  70. 36
      apps/remixdesktop/test/tests/app/gist.test.ts
  71. 203
      apps/remixdesktop/test/tests/app/git-ui.test.ts
  72. 181
      apps/remixdesktop/test/tests/app/git-ui_2.test.ts
  73. 153
      apps/remixdesktop/test/tests/app/git-ui_3.test.ts
  74. 200
      apps/remixdesktop/test/tests/app/git-ui_4.test.ts
  75. 28
      apps/remixdesktop/test/tests/app/git.test.ts
  76. 255
      apps/remixdesktop/test/tests/app/github.test.ts
  77. 190
      apps/remixdesktop/test/tests/app/github_2.test.ts
  78. 180
      apps/remixdesktop/test/tests/app/github_3.test.ts
  79. 90
      apps/remixdesktop/test/tests/app/hardhat.test.ts
  80. 48
      apps/remixdesktop/test/tests/app/offline.test.ts
  81. 269
      apps/remixdesktop/test/tests/app/search.test.ts
  82. 107
      apps/remixdesktop/test/tests/app/slitherlinux.test.ts
  83. 35
      apps/remixdesktop/test/tests/app/templates.test.ts
  84. 246
      apps/remixdesktop/test/tests/app/xterm.test.ts
  85. 249
      apps/remixdesktop/test/tests/app/xtermwin.test.ts
  86. 108
      apps/remixdesktop/test/types/index.d.ts
  87. 8
      apps/remixdesktop/tsconfig.e2e.json
  88. 40
      apps/remixdesktop/tsconfig.json
  89. 44
      apps/remixdesktop/webpack.config.js
  90. 3375
      apps/remixdesktop/yarn.lock
  91. 4
      apps/solidity-compiler/src/app/compiler.ts
  92. 3
      libs/remix-api/src/index.ts
  93. 2
      libs/remix-api/src/lib/plugins/filePanel-api.ts
  94. 5
      libs/remix-api/src/lib/plugins/fileSystem-api.ts
  95. 13
      libs/remix-api/src/lib/plugins/fs-api.ts
  96. 43
      libs/remix-api/src/lib/plugins/git-api.ts
  97. 10
      libs/remix-api/src/lib/plugins/terminal-api.ts
  98. 8
      libs/remix-api/src/lib/remix-api.ts
  99. 206
      libs/remix-api/src/lib/types/git.ts
  100. 61
      libs/remix-core-plugin/src/lib/gist-handler.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -105,6 +105,51 @@ jobs:
paths: paths:
- "persist" - "persist"
test-remixdesktop-linux:
machine:
image: ubuntu-2004:current
resource_class:
xlarge
working_directory: ~/remix-project
parallelism: 10
steps:
- run: ldd --version
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
nvm install 20.2
nvm use 20.2
node -v
npm install --global yarn node-gyp
python -m pip install --upgrade pip
pip install setuptools
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/
yarn add node-pty
yarn --ignore-optional
yarn add @remix-project/remix-ws-templates
./rundist.bash
- run:
name: "Run tests"
command: |
nvm use 20.2
cd apps/remixdesktop/
./run_ci_test.sh
- run:
name: "Run isogit tests"
command: |
nvm use 20.2
cd apps/remixdesktop/
./run_git_ui_isogit_tests.sh
- store_test_results:
path: ./apps/remixdesktop/reports/tests
- store_artifacts:
path: ./apps/remixdesktop/reports/screenshots
build-remixdesktop-linux: build-remixdesktop-linux:
machine: machine:
image: ubuntu-2004:current image: ubuntu-2004:current
@ -119,22 +164,37 @@ jobs:
- run: unzip ./persist/desktopbuild.zip - run: unzip ./persist/desktopbuild.zip
- run: - run:
command: | command: |
nvm install 20.2
nvm use 20.2
node -v node -v
npm install --global yarn node-gyp
python -m pip install --upgrade pip
pip install setuptools
mkdir apps/remixdesktop/build mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/ cd apps/remixdesktop/
yarn add node-pty yarn add node-pty
yarn --ignore-optional yarn --ignore-optional
yarn add @remix-project/remix-ws-templates yarn add @remix-project/remix-ws-templates
PUBLISH_FOR_PULL_REQUEST='true' yarn dist ./rundist.bash
rm -rf release/*-unpacked rm -rf release/*-unpacked
- save_cache: - save_cache:
key: remixdesktop-linux-deps-{{ checksum "apps/remixdesktop/yarn.lock" }} key: remixdesktop-linux-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths: paths:
- apps/remixdesktop/node_modules - apps/remixdesktop/node_modules
- run:
name: "remove unnecessary files"
command: |
rm -rf ~/remix-project/apps/remixdesktop/release/.icon*
rm -rf ~/remix-project/apps/remixdesktop/release/builder*
- store_artifacts: - store_artifacts:
path: apps/remixdesktop/release/ path: apps/remixdesktop/release/
destination: remixdesktop-linux destination: remixdesktop-linux
- persist_to_workspace:
root: apps/remixdesktop
paths:
- "release"
build-remixdesktop-windows: build-remixdesktop-windows:
executor: executor:
@ -147,39 +207,107 @@ jobs:
- attach_workspace: - attach_workspace:
at: . at: .
- run: unzip ./persist/desktopbuild.zip - run: unzip ./persist/desktopbuild.zip
- restore_cache:
key: node-20-windows-v3
- run: - run:
command: | command: |
nvm install 20.0.0 nvm install 20.2
nvm use 20.0.0 nvm use 20.2
node -v node -v
npx -v npx -v
npm install --global yarn npm install --global yarn
npm install --global node-gyp
yarn -v yarn -v
- save_cache:
key: node-20-windows-v3
paths:
- /ProgramData/nvm/v20.0.0
- restore_cache:
keys:
- remixdesktop-windows-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run: - run:
command: | command: |
mkdir apps/remixdesktop/build mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/ cd apps/remixdesktop/
python -m pip install --upgrade pip
pip install setuptools
yarn yarn
PUBLISH_FOR_PULL_REQUEST='true' yarn dist ./rundist.bash
rm -rf release/*-unpacked rm -rf release/*-unpacked
- save_cache:
key: remixdesktop-windows-deps-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
- persist_to_workspace: - persist_to_workspace:
root: apps/remixdesktop root: apps/remixdesktop
paths: paths:
- "release" - "release"
test-remixdesktop-windows:
executor:
name: win/default # executor type
size: xlarge # can be medium, large, xlarge, 2xlarge
shell: bash.exe
parallelism: 10
working_directory: ~/remix-project
steps:
- run:
name: Restart local mstsc
command: psexec64.exe -accepteula -nobanner -i 0 mstsc /v:localhost /w:2560 /h:1140
background: true
shell: powershell.exe
- run:
name: Naive impl to wait until the screen stretches
command: Start-Sleep 5
shell: powershell.exe
- run:
name: Get screen info
command: |
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Screen]::AllScreens | fl *
shell: powershell.exe
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
nvm install 20.2
nvm use 20.2
node -v
npx -v
npm install --global yarn
npm install --global node-gyp
yarn -v
- run:
name: start selenium
command: |
cd "apps/remixdesktop/"
yarn -v
shell: powershell.exe
- run:
command: |
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop/
nvm use 20.2
node -v
python -m pip install --upgrade pip
pip install setuptools
yarn
./rundist.bash
- run:
name: run tests
command: |
cd "apps/remixdesktop/"
yarn -v
sleep 15
./run_ci_test.sh
- run:
name: "Run isogit tests"
command: |
cd apps/remixdesktop/
yarn -v
sleep 15
./run_git_ui_isogit_tests.sh
- store_test_results:
path: ./apps/remixdesktop/reports/tests
- store_artifacts:
path: ./apps/remixdesktop/reports/screenshots
# see https://docs.digicert.com/en/software-trust-manager/ci-cd-integrations/script-integrations/github-integration-ksp.html # see https://docs.digicert.com/en/software-trust-manager/ci-cd-integrations/script-integrations/github-integration-ksp.html
sign-remixdesktop-windows: sign-remixdesktop-windows:
executor: win/default # executor type executor: win/default # executor type
@ -230,28 +358,66 @@ jobs:
command: | command: |
Get-ChildItem -Path 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit' -Filter signtool.exe -Recurse Get-ChildItem -Path 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit' -Filter signtool.exe -Recurse
- run: - run:
name: "Signtool-Signing" name: read env
shell: powershell.exe shell: powershell.exe
command: | command: |
& $env:Signtool sign /sha1 $env:SM_CODE_SIGNING_CERT_SHA1_HASH /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 $env:RemixSetupExe # Specify the path to your package.json file
$packageJsonPath = "C:\Users\circleci\remix-project\apps\remixdesktop\package.json"
# Check if the file exists
if (Test-Path $packageJsonPath) {
# Read the content of the package.json file
$packageJsonContent = Get-Content $packageJsonPath -Raw | ConvertFrom-Json
# Check if the 'version' field exists in the package.json
if ($packageJsonContent.'version' -ne $null) {
# Store the version value in an environment variable
$version = $packageJsonContent.version
$file = "C:\Users\circleci\remix-project\release\Remix-Desktop-Setup-$($version).exe"
Write-Host "Version $(file) stored in PACKAGE_VERSION environment variable."
"Set-Variable -Name 'PACKAGE_VERSION' -Value '$file' -Scope Global" > SetEnvVars.ps1
dir Env:
} else {
Write-Host "Error: 'version' field not found in package.json."
}
} else {
Write-Host "Error: package.json file not found at $packageJsonPath."
}
- run:
name: "Signtool-Signing"
shell: powershell.exe
command: |
. .\SetEnvVars.ps1
dir Env:
& $env:Signtool sign /sha1 $env:SM_CODE_SIGNING_CERT_SHA1_HASH /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 $PACKAGE_VERSION
- run: - run:
name: "Signtool-Verification" name: "Signtool-Verification"
shell: powershell.exe shell: powershell.exe
command: | command: |
$verify_output = $(& $env:Signtool verify /v /pa $env:RemixSetupExe) . .\SetEnvVars.ps1
$verify_output = $(& $env:Signtool verify /v /pa $PACKAGE_VERSION)
echo ${verify_output} echo ${verify_output}
if (!$verify_output.Contains("Number of files successfully Verified: 1")) { if (!$verify_output.Contains("Number of files successfully Verified: 1")) {
echo 'Verification failed' echo 'Verification failed'
exit 1 exit 1
} }
- run:
name: "remove unnecessary files"
shell: bash.exe
command: |
rm -rf ~/remix-project/release/.icon*
rm -rf ~/remix-project/release/builder*
- store_artifacts: - store_artifacts:
path: ~/remix-project/release/ path: ~/remix-project/release/
destination: remixdesktop-windows destination: remixdesktop-windows
- persist_to_workspace:
root: ~/remix-project/
paths:
- "release"
environment: environment:
SM_CLIENT_CERT_FILE: 'C:\Certificate_pkcs12.p12' SM_CLIENT_CERT_FILE: 'C:\Certificate_pkcs12.p12'
Signtool: 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe' Signtool: 'C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe'
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools' SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
RemixSetupExe: 'C:\Users\circleci\remix-project\release\Remix IDE.exe'
build-remixdesktop-mac: build-remixdesktop-mac:
macos: macos:
@ -259,23 +425,41 @@ jobs:
resource_class: resource_class:
macos.m1.large.gen1 macos.m1.large.gen1
working_directory: ~/remix-project working_directory: ~/remix-project
parameters:
arch:
type: string
steps: steps:
- checkout - checkout
- attach_workspace: - attach_workspace:
at: . at: .
- run:
name: Install Apple Certificate
command: |
echo $APPLE_CERTIFICATE_BASE64 | base64 --decode > /tmp/certificate.p12
security create-keychain -p ci-password build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p ci-password build.keychain
curl -o DeveloperIDG2CA.cer "https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer"
sudo security import DeveloperIDG2CA.cer -k /Library/Keychains/System.keychain -T /usr/bin/codesign
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain DeveloperIDG2CA.cer
security import /tmp/certificate.p12 -k build.keychain -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k ci-password build.keychain
security find-identity -v -p codesigning
- run: unzip ./persist/desktopbuild.zip - run: unzip ./persist/desktopbuild.zip
- run: - run:
command: | command: |
ls -la dist/apps/remix-ide ls -la dist/apps/remix-ide
nvm install 20.0.0 nvm install 20.2
nvm use 20.0.0 nvm use 20.2
- restore_cache: - restore_cache:
keys: keys:
- remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }} - remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run: - run:
command: | command: |
nvm use 20.0.0 nvm use 20.2
cd apps/remixdesktop && yarn cd apps/remixdesktop
yarn || yarn
find ./node_modules
yarn add @remix-project/remix-ws-templates yarn add @remix-project/remix-ws-templates
- save_cache: - save_cache:
key: remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }} key: remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
@ -284,20 +468,120 @@ jobs:
# use USE_HARD_LINK=false https://github.com/electron-userland/electron-builder/issues/3179 # use USE_HARD_LINK=false https://github.com/electron-userland/electron-builder/issues/3179
- run: - run:
command: | command: |
nvm use 20.0.0 nvm use 20.2
mkdir apps/remixdesktop/build mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop cd apps/remixdesktop
yarn yarn
yarn installRipGrepMacOXarm64 - run:
PUBLISH_FOR_PULL_REQUEST='true' USE_HARD_LINKS=false yarn dist --mac --arm64 command: |
yarn installRipGrepMacOXx64 nvm use 20.2
PUBLISH_FOR_PULL_REQUEST='true' USE_HARD_LINKS=false yarn dist --mac --x64 cd apps/remixdesktop
rm -rf release/mac* yarn installRipGrepMacOX<< parameters.arch >>
PUBLISH_FOR_PULL_REQUEST='false' USE_HARD_LINKS=false ./rundist.bash --<< parameters.arch >>
if [ -f release/latest-mac.yml ]; then
cat release/latest-mac.yml
mv release/latest-mac.yml release/latest-mac-<< parameters.arch >>.yml
fi
find build
- run:
name: Notarize the app
command: |
brew install jq
cd apps/remixdesktop
zsh notarizedmg.sh
- run:
name: "remove unnecessary files"
command: |
rm -rf ~/remix-project/apps/remixdesktop/release/.icon*
rm -rf ~/remix-project/apps/remixdesktop/release/builder*
rm -rf ~/remix-project/apps/remixdesktop/release/*.blockmap
rm -rf ~/remix-project/apps/remixdesktop/release/_.*
- store_artifacts: - store_artifacts:
path: apps/remixdesktop/release/ path: apps/remixdesktop/release/
destination: remixdesktop-mac destination: remixdesktop-mac
- persist_to_workspace:
root: apps/remixdesktop
paths:
- "release"
test-remixdesktop-mac:
macos:
xcode: 14.2.0
resource_class:
macos.m1.large.gen1
working_directory: ~/remix-project
parallelism: 10
steps:
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/desktopbuild.zip
- run:
command: |
ls -la dist/apps/remix-ide
nvm install 20.2
nvm use 20.2
- restore_cache:
keys:
- remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
- run:
command: |
nvm use 20.2
cd apps/remixdesktop
yarn || yarn
yarn add @remix-project/remix-ws-templates
- save_cache:
key: remixdesktop-deps-mac-{{ checksum "apps/remixdesktop/yarn.lock" }}
paths:
- apps/remixdesktop/node_modules
- run:
command: |
nvm use 20.2
mkdir apps/remixdesktop/build
cp -r dist/apps/remix-ide apps/remixdesktop/build
cd apps/remixdesktop
yarn
- run:
command: |
nvm use 20.2
cd apps/remixdesktop
yarn installRipGrepMacOXarm64
PUBLISH_FOR_PULL_REQUEST='false' DO_NOT_NOTARIZE='true' USE_HARD_LINKS=false ./rundist.bash --arm64
find build
- run:
name: "Run tests"
command: |
nvm use 20.2
cd apps/remixdesktop
./run_ci_test.sh
- run:
name: "Run isogit tests"
command: |
nvm use 20.2
cd apps/remixdesktop
./run_git_ui_isogit_tests.sh
- store_test_results:
path: ./apps/remixdesktop/reports/tests
- store_artifacts:
path: ./apps/remixdesktop/reports/screenshots
uploadartifacts:
docker:
- image: cimg/node:20.0.0-browsers
resource_class:
xlarge
working_directory: ~/remix-project
steps:
- checkout
- attach_workspace:
at: .
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- run: yarn
- run:
name: "Upload Artifacts"
command: npx ts-node apps/remix-ide/ci/update_desktop_release_assets.ts
lint: lint:
docker: docker:
- image: cimg/node:20.0.0-browsers - image: cimg/node:20.0.0-browsers
@ -508,6 +792,12 @@ workflows:
- build-remixdesktop-mac: - build-remixdesktop-mac:
requires: requires:
- build-desktop - build-desktop
matrix:
parameters:
arch: ["arm64", "x64"]
- test-remixdesktop-mac:
requires:
- build-desktop
- build-remixdesktop-windows: - build-remixdesktop-windows:
requires: requires:
- build-desktop - build-desktop
@ -517,6 +807,20 @@ workflows:
- build-remixdesktop-linux: - build-remixdesktop-linux:
requires: requires:
- build-desktop - build-desktop
- test-remixdesktop-linux:
requires:
- build-desktop
- test-remixdesktop-windows:
requires:
- build-desktop
- uploadartifacts:
requires:
- build-remixdesktop-mac
- build-remixdesktop-linux
- sign-remixdesktop-windows
- test-remixdesktop-windows
- test-remixdesktop-linux
- test-remixdesktop-mac
- build-plugin: - build-plugin:
matrix: matrix:
parameters: parameters:

2
.gitignore vendored

@ -15,6 +15,7 @@ soljson.js
*_group*.*.ts *_group*.*.ts
*_group*.ts *_group*.ts
stats.json stats.json
release
# compiled output # compiled output
@ -61,6 +62,7 @@ testem.log
apps/remixdesktop/.webpack apps/remixdesktop/.webpack
apps/remixdesktop/out apps/remixdesktop/out
apps/remixdesktop/release/ apps/remixdesktop/release/
apps/remixdesktop/build*/
apps/remix-ide/src/assets/list.json apps/remix-ide/src/assets/list.json
apps/remix-ide/src/assets/esbuild.wasm apps/remix-ide/src/assets/esbuild.wasm
apps/remixdesktop/build* apps/remixdesktop/build*

@ -24,7 +24,7 @@ function addLocalPlugin (browser: NightwatchBrowser, profile: Profile & Location
browser.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]') browser.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.execute(function () { .execute(function () {
window.testmode = true (window as any).testmode = true
}) })
.click('*[data-id="pluginManagerComponentPluginSearchButton"]') .click('*[data-id="pluginManagerComponentPluginSearchButton"]')
.waitForElementVisible('*[data-id="pluginManagerLocalPluginModalDialogModalDialogContainer-react"]') .waitForElementVisible('*[data-id="pluginManagerLocalPluginModalDialogModalDialogContainer-react"]')

@ -218,6 +218,7 @@ module.exports = {
'disconnect github #group1': function (browser: NightwatchBrowser) { 'disconnect github #group1': function (browser: NightwatchBrowser) {
browser browser
.waitForElementVisible('*[data-id="github-panel"]') .waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]') .click('*[data-id="github-panel"]')
.waitForElementVisible('*[data-id="disconnect-github"]') .waitForElementVisible('*[data-id="disconnect-github"]')
.pause(1000) .pause(1000)
@ -370,11 +371,17 @@ module.exports = {
browser. browser.
clickLaunchIcon('dgit') clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="github-panel"]') .waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]') .click('*[data-id="github-panel"]')
.pause(1000)
.setValue('*[data-id="githubToken"]', 'invalidtoken') .setValue('*[data-id="githubToken"]', 'invalidtoken')
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git') .setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com') .setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]') .click('*[data-id="saveGitHubCredentials"]')
.pause(1000)
.modalFooterOKClick('github-credentials-error') .modalFooterOKClick('github-credentials-error')
}, },
'check the commits panel for pagination #group3': function (browser: NightwatchBrowser) { 'check the commits panel for pagination #group3': function (browser: NightwatchBrowser) {

@ -37,6 +37,7 @@ module.exports = {
.waitForElementVisible('*[data-id="initgit-btn"]') .waitForElementVisible('*[data-id="initgit-btn"]')
.click('*[data-id="initgit-btn"]') .click('*[data-id="initgit-btn"]')
.waitForElementVisible('*[data-id="github-panel"]') .waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]') .click('*[data-id="github-panel"]')
.waitForElementVisible('*[data-id="gitubUsername"]') .waitForElementVisible('*[data-id="gitubUsername"]')
.setValue('*[data-id="gitubUsername"]', 'git') .setValue('*[data-id="gitubUsername"]', 'git')

@ -181,7 +181,7 @@ module.exports = {
.waitForElementVisible('[data-id="workspaceGitPanel"]') .waitForElementVisible('[data-id="workspaceGitPanel"]')
.waitForElementVisible('[data-id="workspaceGitBranchesDropdown"]') .waitForElementVisible('[data-id="workspaceGitBranchesDropdown"]')
.click('[data-id="workspaceGitBranchesDropdown"]') .click('[data-id="workspaceGitBranchesDropdown"]')
.pause() .pause(1000)
.waitForElementVisible('[data-id="custom-dropdown-menu"]') .waitForElementVisible('[data-id="custom-dropdown-menu"]')
.waitForElementContainsText('[data-id="custom-dropdown-items"]', 'origin/dev') .waitForElementContainsText('[data-id="custom-dropdown-items"]', 'origin/dev')
.waitForElementContainsText('[data-id="custom-dropdown-items"]', 'origin/production') .waitForElementContainsText('[data-id="custom-dropdown-items"]', 'origin/production')
@ -404,7 +404,7 @@ module.exports = {
.getEditorValue((content) => { .getEditorValue((content) => {
browser.assert.ok(content.indexOf(`contract Counter is BaseHook {`) !== -1, browser.assert.ok(content.indexOf(`contract Counter is BaseHook {`) !== -1,
'Incorrect content') 'Incorrect content')
}).pause() })
}, },
'Should create Remix default workspace with files #group5': function (browser: NightwatchBrowser) { 'Should create Remix default workspace with files #group5': function (browser: NightwatchBrowser) {
@ -426,6 +426,7 @@ module.exports = {
.waitForElementVisible('*[data-id="initgit-btn"]') .waitForElementVisible('*[data-id="initgit-btn"]')
.click('*[data-id="initgit-btn"]') .click('*[data-id="initgit-btn"]')
.waitForElementVisible('*[data-id="github-panel"]') .waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]') .click('*[data-id="github-panel"]')
.waitForElementVisible('*[data-id="gitubUsername"]') .waitForElementVisible('*[data-id="gitubUsername"]')
.setValue('*[data-id="gitubUsername"]', 'git') .setValue('*[data-id="gitubUsername"]', 'git')

@ -0,0 +1,242 @@
import { Octokit } from 'octokit'
import * as fs from 'fs'
import * as path from 'path'
import YAML from 'yaml'
import crypto from 'crypto'
const owner = 'remix-project-org'
let repo = 'remix-desktop'
const headers = {
'X-GitHub-Api-Version': '2022-11-28',
}
const version = getVersionFromPackageJson()
let channel = 'latest'
if (version.includes('beta')) {
channel = 'beta'
}
if (version.includes('alpha')) {
channel = 'alpha'
}
if (version.includes('insiders')) {
channel = 'insiders'
}
if (channel !== 'latest') repo = `remix-desktop-${channel}`
const octokit = new Octokit({
auth: process.env.GH_TOKEN_DESKTOP_PUBLISH,
})
async function getAllReleases() {
const releases = await octokit.request('GET /repos/{owner}/{repo}/releases', {
owner: owner,
repo: repo,
headers: headers,
})
return releases.data
}
async function uploadReleaseAsset(release, name, file) {
const upload_url = release.upload_url
console.log(`Uploading ${name} to ${upload_url}`)
if (fs.existsSync(file)) {
octokit.request({
method: "POST",
url: upload_url,
headers: {
"content-type": "text/plain",
},
data: fs.readFileSync(file),
name,
label: name
});
} else {
console.log(`File ${file} does not exist. Skipping...`)
}
}
function getVersionFromPackageJson() {
// ignore ts error
// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require(__dirname + '/../../../apps/remixdesktop/package.json')
return packageJson.version
}
async function readReleaseFilesFromLocalDirectory() {
const directoryPath = path.join(__dirname, '../../../release')
const files = fs.readdirSync(directoryPath)
return files
}
async function removeAsset(asset) {
await octokit.request('DELETE /repos/{owner}/{repo}/releases/assets/{asset_id}', {
owner: owner,
repo: repo,
asset_id: asset.id,
headers: headers,
})
}
async function hashFile(file): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha512').setEncoding('base64');
// hash.on('error', reject).setEncoding(encoding);
fs.createReadStream(
file,
Object.assign({}, {}, {
highWaterMark: 1024 * 1024,
/* better to use more memory but hash faster */
})
)
.on('error', reject)
.on('end', () => {
hash.end();
console.log('hash done');
console.log(hash.read());
resolve(hash.digest('base64'));
})
.pipe(
hash,
{
end: false,
}
);
});
}
async function main() {
const allReleases = await getAllReleases()
console.log(`preparing release version: ${version}`)
let release
allReleases.find((r) => {
if (r.tag_name === `v${version}`) {
release = r
}
})
if (!release) {
console.log('No release found.')
// create release
console.log(`Creating release ${version}`)
const r = await octokit.request('POST /repos/{owner}/{repo}/releases', {
owner: owner,
repo: repo,
tag_name: `v${version}`,
name: `${version}`,
draft: true,
prerelease: true,
headers: headers,
})
release = r.data
}
let ymlFiles = await readReleaseFilesFromLocalDirectory()
ymlFiles = ymlFiles.filter((file) => file.endsWith('.yml') && file.startsWith('latest'))
console.log(`Found ${ymlFiles.length} yml files to upload`)
// read and parse yml latest files
// the yml files contain the sha512 hash and file size of the executable
// we need to recalculate the hash and file size of the executable
// and update the yml files
// this is because the executable is resigned after the yml files are created
for (const file of ymlFiles) {
const content = fs.readFileSync(path.join(__dirname, '../../../release', file), 'utf8')
const parsed = YAML.parse(content)
const hashes: {
url: string,
sha512: string,
size: number
}[] = []
if (parsed.files) {
console.log(`Found`, parsed.files)
for (const f of parsed.files) {
const executable = f.url
const exists = fs.existsSync(path.join(__dirname, '../../../release', executable))
if (!exists) {
console.log(`File ${executable} does not exist on local fs. Skipping...`)
continue
} else {
console.log(`File ${executable} exists on local fs. Recalculating hash...`)
// calculate sha512 hash of executable
const hash: string = await hashFile(path.join(__dirname, '../../../release', executable))
console.log(hash)
// calculate file size of executable
const stats = fs.statSync(path.join(__dirname, '../../../release', executable))
const fileSizeInBytes = stats.size
console.log(fileSizeInBytes)
hashes.push({
url: executable,
sha512: hash,
size: fileSizeInBytes
})
if (parsed.path === executable) {
parsed.sha512 = hash
parsed.size = fileSizeInBytes
}
}
}
}
console.log(hashes)
parsed.files = hashes
const newYml = YAML.stringify(parsed)
fs.writeFileSync(path.join(__dirname, '../../../release', file), newYml)
}
let files = await readReleaseFilesFromLocalDirectory()
try {
if (fs.existsSync(path.join(__dirname, '../../../release', `latest-mac-arm64.yml`)) && fs.existsSync(path.join(__dirname, '../../../release', `latest-mac-x64.yml`))) {
// combine the two files
const macArm64 = fs.readFileSync(path.join(__dirname, '../../../release', `latest-mac-arm64.yml`), 'utf8')
const mac = fs.readFileSync(path.join(__dirname, '../../../release', `latest-mac-x64.yml`), 'utf8')
const parsedMacArm64 = YAML.parse(macArm64)
const parsedMac = YAML.parse(mac)
console.log(parsedMacArm64)
console.log(parsedMac)
const combined = {
...parsedMac,
files: [
...parsedMac.files,
...parsedMacArm64.files
]
}
console.log(combined)
const newYml = YAML.stringify(combined)
fs.writeFileSync(path.join(__dirname, '../../../release', `latest-mac.yml`), newYml)
// remove the arm64 file
fs.unlinkSync(path.join(__dirname, '../../../release', `latest-mac-arm64.yml`))
fs.unlinkSync(path.join(__dirname, '../../../release', `latest-mac-x64.yml`))
}
} catch (e) {
console.log(e)
}
files = await readReleaseFilesFromLocalDirectory()
files = files.
filter((file) => file.endsWith('.zip') || file.endsWith('.dmg') || file.endsWith('.exe') || file.endsWith('.AppImage') || file.endsWith('.snap') || file.endsWith('.deb') || file.startsWith(`latest`))
.filter((file) => !file.startsWith('._'))
console.log(`Found ${files.length} files to upload`)
console.log(files)
if (!release.draft) {
console.log(`Release ${version} is not a draft. Aborting...`)
return
}
// upload files
for (const file of files) {
// check if it is already uploaded
const asset = release.assets.find((a) => a.label === file)
if (asset) {
console.log(`Asset ${file} already uploaded... replacing it`)
// remove it first
await removeAsset(asset)
}
await uploadReleaseAsset(release, file, path.join(__dirname, '../../../release', file))
}
}
main()

@ -1,29 +1,29 @@
'use strict' 'use strict'
import {RunTab, makeUdapp} from './app/udapp' import { RunTab, makeUdapp } from './app/udapp'
import {RemixEngine} from './remixEngine' import { RemixEngine } from './remixEngine'
import {RemixAppManager} from './remixAppManager' import { RemixAppManager } from './remixAppManager'
import {ThemeModule} from './app/tabs/theme-module' import { ThemeModule } from './app/tabs/theme-module'
import {LocaleModule} from './app/tabs/locale-module' import { LocaleModule } from './app/tabs/locale-module'
import {NetworkModule} from './app/tabs/network-module' import { NetworkModule } from './app/tabs/network-module'
import {Web3ProviderModule} from './app/tabs/web3-provider' import { Web3ProviderModule } from './app/tabs/web3-provider'
import {CompileAndRun} from './app/tabs/compile-and-run' import { CompileAndRun } from './app/tabs/compile-and-run'
import {PluginStateLogger} from './app/tabs/state-logger' import { PluginStateLogger } from './app/tabs/state-logger'
import {SidePanel} from './app/components/side-panel' import { SidePanel } from './app/components/side-panel'
import {StatusBar} from './app/components/status-bar' import { StatusBar } from './app/components/status-bar'
import {HiddenPanel} from './app/components/hidden-panel' import { HiddenPanel } from './app/components/hidden-panel'
import {PinnedPanel} from './app/components/pinned-panel' import { PinnedPanel } from './app/components/pinned-panel'
import {VerticalIcons} from './app/components/vertical-icons' import { VerticalIcons } from './app/components/vertical-icons'
import {LandingPage} from './app/ui/landing-page/landing-page' import { LandingPage } from './app/ui/landing-page/landing-page'
import {MainPanel} from './app/components/main-panel' import { MainPanel } from './app/components/main-panel'
import {PermissionHandlerPlugin} from './app/plugins/permission-handler-plugin' import { PermissionHandlerPlugin } from './app/plugins/permission-handler-plugin'
import {AstWalker} from '@remix-project/remix-astwalker' import { AstWalker } from '@remix-project/remix-astwalker'
import {LinkLibraries, DeployLibraries, OpenZeppelinProxy} from '@remix-project/core-plugin' import { LinkLibraries, DeployLibraries, OpenZeppelinProxy } from '@remix-project/core-plugin'
import {CodeParser} from './app/plugins/parser/code-parser' import { CodeParser } from './app/plugins/parser/code-parser'
import {SolidityScript} from './app/plugins/solidity-script' import { SolidityScript } from './app/plugins/solidity-script'
import {WalkthroughService} from './walkthroughService' import { WalkthroughService } from './walkthroughService'
import {OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports, GistHandler} from '@remix-project/core-plugin' import { OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports, GistHandler } from '@remix-project/core-plugin'
import {Registry} from '@remix-project/remix-lib' import {Registry} from '@remix-project/remix-lib'
import {ConfigPlugin} from './app/plugins/config' import {ConfigPlugin} from './app/plugins/config'
@ -56,10 +56,19 @@ import { electronTemplates } from './app/plugins/electron/templatesPlugin'
import { xtermPlugin } from './app/plugins/electron/xtermPlugin' import { xtermPlugin } from './app/plugins/electron/xtermPlugin'
import { ripgrepPlugin } from './app/plugins/electron/ripgrepPlugin' import { ripgrepPlugin } from './app/plugins/electron/ripgrepPlugin'
import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins/electron/compilerLoaderPlugin' import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins/electron/compilerLoaderPlugin'
import { appUpdaterPlugin } from './app/plugins/electron/appUpdaterPlugin'
import { SlitherHandleDesktop } from './app/plugins/electron/slitherPlugin'
import { SlitherHandle } from './app/files/slither-handle'
import { FoundryHandle } from './app/files/foundry-handle'
import { FoundryHandleDesktop } from './app/plugins/electron/foundryPlugin'
import { HardhatHandle } from './app/files/hardhat-handle'
import { HardhatHandleDesktop } from './app/plugins/electron/hardhatPlugin'
import { SolCoder } from './app/plugins/solcoderAI'
import { GitPlugin } from './app/plugins/git' import { GitPlugin } from './app/plugins/git'
import { Matomo } from './app/plugins/matomo' import { Matomo } from './app/plugins/matomo'
import {SolCoder} from './app/plugins/solcoderAI'
import { TemplatesSelectionPlugin } from './app/plugins/templates-selection/templates-selection-plugin' import { TemplatesSelectionPlugin } from './app/plugins/templates-selection/templates-selection-plugin'
@ -78,6 +87,7 @@ const Config = require('./config')
const FileManager = require('./app/files/fileManager') const FileManager = require('./app/files/fileManager')
import FileProvider from "./app/files/fileProvider" import FileProvider from "./app/files/fileProvider"
import { appPlatformTypes } from '@remix-ui/app' import { appPlatformTypes } from '@remix-ui/app'
const DGitProvider = require('./app/files/dgitProvider') const DGitProvider = require('./app/files/dgitProvider')
const WorkspaceFileProvider = require('./app/files/workspaceFileProvider') const WorkspaceFileProvider = require('./app/files/workspaceFileProvider')
@ -86,19 +96,19 @@ const PluginManagerComponent = require('./app/components/plugin-manager-componen
const CompileTab = require('./app/tabs/compile-tab') const CompileTab = require('./app/tabs/compile-tab')
const SettingsTab = require('./app/tabs/settings-tab') const SettingsTab = require('./app/tabs/settings-tab')
const AnalysisTab = require('./app/tabs/analysis-tab') const AnalysisTab = require('./app/tabs/analysis-tab')
const {DebuggerTab} = require('./app/tabs/debugger-tab') const { DebuggerTab } = require('./app/tabs/debugger-tab')
const TestTab = require('./app/tabs/test-tab') const TestTab = require('./app/tabs/test-tab')
const FilePanel = require('./app/panels/file-panel') const FilePanel = require('./app/panels/file-panel')
const Editor = require('./app/editor/editor') const Editor = require('./app/editor/editor')
const Terminal = require('./app/panels/terminal') const Terminal = require('./app/panels/terminal')
const {TabProxy} = require('./app/panels/tab-proxy.js') const { TabProxy } = require('./app/panels/tab-proxy.js')
export class platformApi { export class platformApi {
get name () { get name() {
return isElectron() ? appPlatformTypes.desktop : appPlatformTypes.web return isElectron() ? appPlatformTypes.desktop : appPlatformTypes.web
} }
isDesktop () { isDesktop() {
return isElectron() return isElectron()
} }
} }
@ -118,7 +128,7 @@ class AppComponent {
// load app config // load app config
const config = new Config(configStorage) const config = new Config(configStorage)
Registry.getInstance().put({api: config, name: 'config'}) Registry.getInstance().put({ api: config, name: 'config' })
// load file system // load file system
this._components.filesProviders = {} this._components.filesProviders = {}
@ -170,7 +180,14 @@ class AppComponent {
this.matomoConfAlreadySet = Registry.getInstance().get('config').api.exists('settings/matomo-analytics') this.matomoConfAlreadySet = Registry.getInstance().get('config').api.exists('settings/matomo-analytics')
this.matomoCurrentSetting = Registry.getInstance().get('config').api.get('settings/matomo-analytics') this.matomoCurrentSetting = Registry.getInstance().get('config').api.get('settings/matomo-analytics')
this.showMatamo = matomoDomains[window.location.hostname] && !this.matomoConfAlreadySet
let electronTracking = false
if (window.electronAPI) {
electronTracking = await window.electronAPI.canTrackMatomo()
}
this.showMatamo = (matomoDomains[window.location.hostname] || electronTracking) && !this.matomoConfAlreadySet
this.walkthroughService = new WalkthroughService(appManager) this.walkthroughService = new WalkthroughService(appManager)
@ -192,12 +209,12 @@ class AppComponent {
this.themeModule = new ThemeModule() this.themeModule = new ThemeModule()
// ----------------- locale service --------------------------------- // ----------------- locale service ---------------------------------
this.localeModule = new LocaleModule() this.localeModule = new LocaleModule()
Registry.getInstance().put({api: this.themeModule, name: 'themeModule'}) Registry.getInstance().put({ api: this.themeModule, name: 'themeModule' })
Registry.getInstance().put({api: this.localeModule, name: 'localeModule'}) Registry.getInstance().put({ api: this.localeModule, name: 'localeModule' })
// ----------------- editor service ---------------------------- // ----------------- editor service ----------------------------
const editor = new Editor() // wrapper around ace editor const editor = new Editor() // wrapper around ace editor
Registry.getInstance().put({api: editor, name: 'editor'}) Registry.getInstance().put({ api: editor, name: 'editor' })
editor.event.register('requiringToSaveCurrentfile', (currentFile) => { editor.event.register('requiringToSaveCurrentfile', (currentFile) => {
fileManager.saveCurrentFile() fileManager.saveCurrentFile()
if (currentFile.endsWith('.circom')) this.appManager.activatePlugin(['circuit-compiler']) if (currentFile.endsWith('.circom')) this.appManager.activatePlugin(['circuit-compiler'])
@ -205,7 +222,7 @@ class AppComponent {
// ----------------- fileManager service ---------------------------- // ----------------- fileManager service ----------------------------
const fileManager = new FileManager(editor, appManager) const fileManager = new FileManager(editor, appManager)
Registry.getInstance().put({api: fileManager, name: 'filemanager'}) Registry.getInstance().put({ api: fileManager, name: 'filemanager' })
// ----------------- dGit provider --------------------------------- // ----------------- dGit provider ---------------------------------
const dGitProvider = new DGitProvider() const dGitProvider = new DGitProvider()
@ -292,7 +309,7 @@ class AppComponent {
// -------------------Terminal---------------------------------------- // -------------------Terminal----------------------------------------
makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl)) makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl))
const terminal = new Terminal( const terminal = new Terminal(
{appManager, blockchain}, { appManager, blockchain },
{ {
getPosition: (event) => { getPosition: (event) => {
const limitUp = 36 const limitUp = 36
@ -388,14 +405,28 @@ class AppComponent {
this.engine.register([xterm]) this.engine.register([xterm])
const ripgrep = new ripgrepPlugin() const ripgrep = new ripgrepPlugin()
this.engine.register([ripgrep]) this.engine.register([ripgrep])
const appUpdater = new appUpdaterPlugin()
this.engine.register([appUpdater])
} }
const compilerloader = isElectron()? new compilerLoaderPluginDesktop(): new compilerLoaderPlugin() const compilerloader = isElectron() ? new compilerLoaderPluginDesktop() : new compilerLoaderPlugin()
this.engine.register([compilerloader]) this.engine.register([compilerloader])
// slither analyzer plugin (remixd / desktop)
const slitherPlugin = isElectron() ? new SlitherHandleDesktop() : new SlitherHandle()
this.engine.register([slitherPlugin])
//foundry plugin
const foundryPlugin = isElectron() ? new FoundryHandleDesktop() : new FoundryHandle()
this.engine.register([foundryPlugin])
// hardhat plugin
const hardhatPlugin = isElectron() ? new HardhatHandleDesktop() : new HardhatHandle()
this.engine.register([hardhatPlugin])
// LAYOUT & SYSTEM VIEWS // LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel() const appPanel = new MainPanel()
Registry.getInstance().put({api: this.mainview, name: 'mainview'}) Registry.getInstance().put({ api: this.mainview, name: 'mainview' })
const tabProxy = new TabProxy(fileManager, editor) const tabProxy = new TabProxy(fileManager, editor)
this.engine.register([appPanel, tabProxy]) this.engine.register([appPanel, tabProxy])
@ -447,10 +478,7 @@ class AppComponent {
analysis, analysis,
test, test,
filePanel.remixdHandle, filePanel.remixdHandle,
filePanel.hardhatHandle,
filePanel.foundryHandle,
filePanel.truffleHandle, filePanel.truffleHandle,
filePanel.slitherHandle,
linkLibraries, linkLibraries,
deployLibraries, deployLibraries,
openZeppelinProxy, openZeppelinProxy,
@ -458,10 +486,10 @@ class AppComponent {
]) ])
this.layout.panels = { this.layout.panels = {
tabs: {plugin: tabProxy, active: true}, tabs: { plugin: tabProxy, active: true },
editor: {plugin: editor, active: true}, editor: { plugin: editor, active: true },
main: {plugin: appPanel, active: false}, main: { plugin: appPanel, active: false },
terminal: {plugin: terminal, active: true, minimized: false} terminal: { plugin: terminal, active: true, minimized: false }
} }
} }
@ -474,7 +502,7 @@ class AppComponent {
} catch (e) { } catch (e) {
console.log("couldn't register iframe plugins", e.message) console.log("couldn't register iframe plugins", e.message)
} }
if (isElectron()){ if (isElectron()) {
await this.appManager.activatePlugin(['fs']) await this.appManager.activatePlugin(['fs'])
} }
await this.appManager.activatePlugin(['layout']) await this.appManager.activatePlugin(['layout'])
@ -517,8 +545,8 @@ class AppComponent {
await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder', 'dgitApi', 'dgit']) await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder', 'dgitApi', 'dgit'])
await this.appManager.activatePlugin(['solidity-script', 'remix-templates']) await this.appManager.activatePlugin(['solidity-script', 'remix-templates'])
if (isElectron()){ if (isElectron()) {
await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep']) await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater', 'slither', 'foundry', 'hardhat'])
} }
this.appManager.on( this.appManager.on(

@ -4,26 +4,16 @@ import {
Plugin Plugin
} from '@remixproject/engine' } from '@remixproject/engine'
import git, { ReadBlobResult, ReadCommitResult, StatusRow } from 'isomorphic-git' import git, { ReadBlobResult, ReadCommitResult, StatusRow } from 'isomorphic-git'
import IpfsHttpClient from 'ipfs-http-client'
import {
saveAs
} from 'file-saver'
import http from 'isomorphic-git/http/web'
import JSZip from 'jszip'
import path from 'path' import path from 'path'
import FormData from 'form-data'
import axios from 'axios' import axios from 'axios'
import { Registry } from '@remix-project/remix-lib' import { Registry } from '@remix-project/remix-lib'
import { Octokit, App } from "octokit" import { Octokit } from "octokit"
import { OctokitResponse } from '@octokit/types'
import { Endpoints } from "@octokit/types"
import { IndexedDBStorage } from './filesystems/indexedDB' import { IndexedDBStorage } from './filesystems/indexedDB'
import { GitHubUser, branch, commitChange, remote, pagedCommits, remoteCommitsInputType, cloneInputType, fetchInputType, pullInputType, pushInputType, currentBranchInput, branchInputType, addInput, rmInput, resolveRefInput, readBlobInput, repositoriesInput, commitInput, branchDifference, compareBranchesInput, initInput, userEmails, checkoutInput } from '@remix-ui/git' import { branch, commitChange, remote } from '@remix-ui/git'
import { LibraryProfile, StatusEvents } from '@remixproject/plugin-utils' import { checkoutInputType, statusInput, logInputType, author, pagedCommits, remoteCommitsInputType, cloneInputType, fetchInputType, pullInputType, pushInputType, currentBranchInput, branchInputType, addInputType, rmInputType, resolveRefInput, readBlobInput, repositoriesInput, commitInputType, branchDifference, compareBranchesInput, initInputType, isoGitFSConfig, GitHubUser, userEmails } from '@remix-api'
import { ITerminal } from '@remixproject/plugin-api/src/lib/terminal' import { LibraryProfile } from '@remixproject/plugin-utils'
import { partial } from 'lodash' import { CustomRemixApi } from '@remix-api'
import { isoGit } from "@remix-git"
declare global { declare global {
interface Window { remixFileSystemCallback: IndexedDBStorage; remixFileSystem: any; } interface Window { remixFileSystemCallback: IndexedDBStorage; remixFileSystem: any; }
} }
@ -34,49 +24,17 @@ const profile: LibraryProfile = {
description: 'Decentralized git provider', description: 'Decentralized git provider',
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
version: '0.0.1', version: '0.0.1',
methods: ['init', 'localStorageUsed', 'addremote', 'delremote', 'remotes', 'fetch', 'clone', 'export', 'import', 'status', 'log', 'commit', 'add', 'remove', 'reset', 'rm', 'lsfiles', 'readblob', 'resolveref', 'branches', 'branch', 'checkout', 'currentbranch', 'push', 'pull', 'setIpfsConfig', 'zip', 'setItem', 'getItem', 'version', 'updateSubmodules' methods: ['init', 'addremote', 'delremote', 'remotes', 'fetch', 'clone', 'status', 'log', 'commit', 'add', 'remove', 'rm', 'readblob', 'resolveref', 'branches', 'branch', 'checkout', 'currentbranch', 'push', 'pull', 'version', 'updateSubmodules'
, 'getGitHubUser', 'remotebranches', 'remotecommits', 'repositories', 'getCommitChanges', 'compareBranches'], , 'getGitHubUser', 'remotebranches', 'remotecommits', 'repositories', 'getCommitChanges', 'compareBranches'],
kind: 'file-system' kind: 'file-system'
} }
class DGitProvider extends Plugin { class DGitProvider extends Plugin<any, CustomRemixApi> {
ipfsconfig: { host: string; port: number; protocol: string; ipfsurl: string }
globalIPFSConfig: { host: string; port: number; protocol: string; ipfsurl: string }
remixIPFS: { host: string; port: number; protocol: string; ipfsurl: string }
ipfsSources: any[]
ipfs: any
filesToSend: any[]
constructor() { constructor() {
super(profile) super(profile)
this.ipfsconfig = {
host: 'jqgt.remixproject.org',
port: 443,
protocol: 'https',
ipfsurl: 'https://jqgt.remixproject.org/ipfs/'
}
this.globalIPFSConfig = {
host: 'ipfs.io',
port: 443,
protocol: 'https',
ipfsurl: 'https://ipfs.io/ipfs/'
}
this.remixIPFS = {
host: 'jqgt.remixproject.org',
port: 443,
protocol: 'https',
ipfsurl: 'https://jqgt.remixproject.org/ipfs/'
}
this.ipfsSources = [this.remixIPFS, this.globalIPFSConfig, this.ipfsconfig]
} }
async addIsomorphicGitConfigFS(dir = '') { async addIsomorphicGitConfigFS(dir = '') {
if ((Registry.getInstance().get('platform').api.isDesktop())) {
return {
fs: window.remixFileSystem,
dir: '/'
}
}
const workspace = await this.call('filePanel', 'getCurrentWorkspace') const workspace = await this.call('filePanel', 'getCurrentWorkspace')
if (!workspace) return if (!workspace) return
@ -86,62 +44,12 @@ class DGitProvider extends Plugin {
} }
} }
async addIsomorphicGitConfig(input) { async getToken() {
return await this.call('config' as any, 'getAppParameter', 'settings/gist-access-token')
const token = await this.call('config' as any, 'getAppParameter', 'settings/gist-access-token')
let config = {
corsProxy: 'https://corsproxy.remixproject.org/',
http,
onAuth: url => {
url
const auth = {
username: input.token || token,
password: ''
}
return auth
}
}
if (input.url) {
const url = new URL(input.url)
if (url.hostname.includes('localhost')) {
config = {
...config,
corsProxy: null
}
}
}
if ((input.remote && input.remote.url)) {
const url = new URL(input.remote.url)
if (url.hostname.includes('localhost')) {
config = {
...config,
corsProxy: null,
}
}
}
if (input.provider && input.provider === 'github') {
config = {
...config,
corsProxy: 'https://corsproxy.remixproject.org/',
}
}
if (input.provider && input.provider === 'localhost') {
config = {
...config,
corsProxy: null
}
}
return config
} }
async getCommandUser(input) { async getAuthor(input) {
const author = { const author: author = {
name: '', name: '',
email: '' email: ''
} }
@ -167,7 +75,7 @@ class DGitProvider extends Plugin {
return author return author
} }
async init(input?: initInput): Promise<void> { async init(input?: initInputType): Promise<void> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
await this.call('isogit', 'init', { await this.call('isogit', 'init', {
defaultBranch: (input && input.defaultBranch) || 'main' defaultBranch: (input && input.defaultBranch) || 'main'
@ -192,11 +100,10 @@ class DGitProvider extends Plugin {
return version return version
} }
async status(cmd): Promise<Array<StatusRow>> { async status(cmd: statusInput): Promise<Array<StatusRow>> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
const status = await this.call('isogit', 'status', cmd) const status = await this.call('isogit', 'status', cmd)
return status return status
} }
@ -208,7 +115,7 @@ class DGitProvider extends Plugin {
return status return status
} }
async add(cmd: addInput): Promise<void> { async add(cmd: addInputType): Promise<void> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
await this.call('isogit', 'add', cmd) await this.call('isogit', 'add', cmd)
@ -222,7 +129,7 @@ class DGitProvider extends Plugin {
this.emit('add') this.emit('add')
} }
async rm(cmd: rmInput) { async rm(cmd: rmInputType) {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
await this.call('isogit', 'rm', cmd) await this.call('isogit', 'rm', cmd)
@ -231,26 +138,11 @@ class DGitProvider extends Plugin {
...await this.addIsomorphicGitConfigFS(), ...await this.addIsomorphicGitConfigFS(),
...cmd ...cmd
}) })
this.emit('rm')
}
}
async reset(cmd) {
if ((Registry.getInstance().get('platform').api.isDesktop())) {
await this.call('isogit', 'reset', cmd)
} else {
await git.resetIndex({
...await this.addIsomorphicGitConfigFS(),
...cmd
})
this.emit('rm')
} }
this.emit('rm')
} }
async checkout(cmd: checkoutInput): Promise<void> { async checkout(cmd: checkoutInputType): Promise<void> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
await this.call('isogit', 'checkout', cmd) await this.call('isogit', 'checkout', cmd)
@ -291,12 +183,11 @@ class DGitProvider extends Plugin {
this.emit('checkout') this.emit('checkout')
} }
async log(cmd: { ref: string }): Promise<ReadCommitResult[]> { async log(cmd: logInputType): Promise<ReadCommitResult[]> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
const status = await this.call('isogit', 'log', { const status = await this.call('isogit', 'log', {
...cmd, ...cmd,
depth: 10
}) })
return status return status
@ -306,101 +197,32 @@ class DGitProvider extends Plugin {
...await this.addIsomorphicGitConfigFS(), ...await this.addIsomorphicGitConfigFS(),
...cmd, ...cmd,
}) })
return status return status
} }
async compareBranches({ branch, remote }: compareBranchesInput): Promise<branchDifference> { async compareBranches({ branch, remote }: compareBranchesInput): Promise<branchDifference> {
// Get current branch commits if ((Registry.getInstance().get('platform').api.isDesktop())) {
const headCommits = await git.log({ return await this.call('isogit', 'compareBranches', { branch, remote })
...await this.addIsomorphicGitConfigFS(), }
ref: branch.name, return await isoGit.compareBranches({ branch, remote }, await this.addIsomorphicGitConfigFS())
});
// Get remote branch commits
const remoteCommits = await git.log({
...await this.addIsomorphicGitConfigFS(),
ref: `${remote.name}/${branch.name}`,
});
// Convert arrays of commit objects to sets of commit SHAs
const headCommitSHAs = new Set(headCommits.map(commit => commit.oid));
const remoteCommitSHAs = new Set(remoteCommits.map(commit => commit.oid));
// Filter out commits that are only in the remote branch
const uniqueRemoteCommits = remoteCommits.filter(commit => !headCommitSHAs.has(commit.oid));
// filter out commits that are only in the local branch
const uniqueHeadCommits = headCommits.filter(commit => !remoteCommitSHAs.has(commit.oid));
return {
uniqueHeadCommits,
uniqueRemoteCommits,
};
} }
async getCommitChanges(commitHash1: string, commitHash2: string): Promise<commitChange[]> { async getCommitChanges(commitHash1: string, commitHash2: string): Promise<commitChange[]> {
const result: commitChange[] = await git.walk({ if ((Registry.getInstance().get('platform').api.isDesktop())) {
...await this.addIsomorphicGitConfigFS(), const result = this.call('isogit', 'getCommitChanges', commitHash1, commitHash2)
trees: [git.TREE({ ref: commitHash1 }), git.TREE({ ref: commitHash2 })], return result
map: async function (filepath, [A, B]) { }
if (filepath === '.') {
return
}
try {
if ((A && await A.type()) === 'tree' || B && (await B.type()) === 'tree') {
return
}
} catch (e) {
// ignore
}
// generate ids
const Aoid = A && await A.oid() || undefined
const Boid = B && await B.oid() || undefined
const commitChange: Partial<commitChange> = {
hashModified: commitHash1,
hashOriginal: commitHash2,
path: filepath,
}
// determine modification type
if (Aoid !== Boid) {
commitChange.type = "modified"
}
if (Aoid === undefined) {
commitChange.type = "deleted"
}
if (Boid === undefined || !commitHash2) {
commitChange.type = "added"
}
if (Aoid === undefined && Boid === undefined) {
commitChange.type = "unknown"
}
if (commitChange.type)
return commitChange
else
return undefined
},
})
return result return await isoGit.getCommitChanges(commitHash1, commitHash2, await this.addIsomorphicGitConfigFS())
} }
async remotes(config): Promise<remote[]> { async remotes(): Promise<remote[]> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
return await this.call('isogit', 'remotes', config) return await this.call('isogit', 'remotes')
} }
let remotes: remote[] = [] return await isoGit.remotes(await this.addIsomorphicGitConfigFS())
try {
remotes = (await git.listRemotes({ ...config ? config : await this.addIsomorphicGitConfigFS() })).map((remote) => { return { name: remote.remote, url: remote.url } }
)
} catch (e) {
// do nothing
}
return remotes
} }
async branch(cmd: branchInputType): Promise<void> { async branch(cmd: branchInputType): Promise<void> {
@ -423,68 +245,28 @@ class DGitProvider extends Plugin {
return status return status
} }
async currentbranch(config: currentBranchInput): Promise<branch> { async currentbranch(input: currentBranchInput): Promise<branch> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
return await this.call('isogit', 'currentbranch') return await this.call('isogit', 'currentbranch', input)
} }
try { const defaultConfig = await this.addIsomorphicGitConfigFS()
const defaultConfig = await this.addIsomorphicGitConfigFS() return await isoGit.currentbranch(input, defaultConfig)
const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig
const name = await git.currentBranch(cmd)
let remote: remote = undefined
try {
const remoteName = await git.getConfig({
...defaultConfig,
path: `branch.${name}.remote`
})
if (remoteName) {
const remoteUrl = await git.getConfig({
...defaultConfig,
path: `remote.${remoteName}.url`
})
remote = { name: remoteName, url: remoteUrl }
}
} catch (e) {
// do nothing
}
return {
remote: remote,
name: name || ''
}
} catch (e) {
return undefined
}
} }
async branches(config): Promise<branch[]> { async branches(config: isoGitFSConfig): Promise<branch[]> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
return await this.call('isogit', 'branches') const branches = await this.call('isogit', 'branches')
}
try {
const defaultConfig = await this.addIsomorphicGitConfigFS()
const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig
const remotes = await this.remotes(config)
let branches: branch[] = []
branches = (await git.listBranches(cmd)).map((branch) => { return { remote: undefined, name: branch } })
for (const remote of remotes) {
cmd.remote = remote.name
const remotebranches = (await git.listBranches(cmd)).map((branch) => { return { remote: remote, name: branch } })
branches = [...branches, ...remotebranches]
}
return branches return branches
} catch (e) {
console.log(e)
return []
} }
const defaultConfig = await this.addIsomorphicGitConfigFS()
const cmd = config ? defaultConfig ? { ...defaultConfig, ...config } : config : defaultConfig
return await isoGit.branches(cmd)
} }
async commit(cmd: commitInput): Promise<string> { async commit(cmd: commitInputType): Promise<string> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
try { try {
@ -511,19 +293,6 @@ class DGitProvider extends Plugin {
} }
} }
async lsfiles(cmd) {
if ((Registry.getInstance().get('platform').api.isDesktop())) {
return await this.call('isogit', 'lsfiles', cmd)
}
const filesInStaging = await git.listFiles({
...await this.addIsomorphicGitConfigFS(),
...cmd
})
return filesInStaging
}
async resolveref(cmd: resolveRefInput): Promise<string> { async resolveref(cmd: resolveRefInput): Promise<string> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
@ -550,26 +319,9 @@ class DGitProvider extends Plugin {
return readBlobResult return readBlobResult
} }
async setIpfsConfig(config) {
this.ipfsconfig = config
return new Promise((resolve) => {
resolve(this.checkIpfsConfig())
})
}
async checkIpfsConfig(config?) {
this.ipfs = IpfsHttpClient(config || this.ipfsconfig)
try {
await this.ipfs.config.getAll()
return true
} catch (e) {
return false
}
}
async addremote(input: remote): Promise<void> { async addremote(input: remote): Promise<void> {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
await this.call('isogit', 'addremote', { url: input.url, remote: input.name }) await this.call('isogit', 'addremote', input)
return return
} }
await git.addRemote({ ...await this.addIsomorphicGitConfigFS(), url: input.url, remote: input.name }) await git.addRemote({ ...await this.addIsomorphicGitConfigFS(), url: input.url, remote: input.name })
@ -577,51 +329,39 @@ class DGitProvider extends Plugin {
async delremote(input: remote) { async delremote(input: remote) {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
await this.call('isogit', 'delremote', { remote: input.name }) await this.call('isogit', 'delremote', input)
return return
} }
await git.deleteRemote({ ...await this.addIsomorphicGitConfigFS(), remote: input.name }) await git.deleteRemote({ ...await this.addIsomorphicGitConfigFS(), remote: input.name })
} }
async localStorageUsed() {
return this.calculateLocalStorage()
}
async clone(input: cloneInputType) { async clone(input: cloneInputType) {
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
const folder = await this.call('fs', 'selectFolder', null, 'Select or create a folder to clone the repository in', 'Select as Repository Destination')
if (!folder) return false
const cmd = {
url: input.url,
singleBranch: input.singleBranch,
ref: input.branch,
depth: input.depth || 10,
dir: folder,
input
}
this.call('terminal', 'logHtml', `Cloning ${input.url}... please wait...`)
try { try {
const result = await this.call('isogit', 'clone', cmd) const folder = await this.call('fs', 'selectFolder', null, 'Select or create a folder to clone the repository in', 'Select as Repository Destination')
this.call('fs', 'openWindow', folder) if (!folder) return false
return result input.dir = folder
input.depth = input.depth || 10
const result = await this.call('isogit', 'clone', input)
this.call('fs' as any, 'openWindow', folder)
} catch (e) { } catch (e) {
this.call('notification', 'alert', { this.call('notification', 'alert', {
id: 'dgitAlert', id: 'dgitAlert',
title: 'Error Cloning',
message: 'Unexpected error while cloning the repository: \n' + e.toString(), message: 'Unexpected error while cloning the repository: \n' + e.toString(),
}) })
} }
} else { } else {
const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.') const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.')
if (!permission) return false if (!permission) return false
if (parseFloat(this.calculateLocalStorage()) > 10000) throw new Error('The local storage of the browser is full.')
if (!input.workspaceExists) await this.call('filePanel', 'createWorkspace', input.workspaceName || `workspace_${Date.now()}`, true) if (!input.workspaceExists) await this.call('filePanel', 'createWorkspace', input.workspaceName || `workspace_${Date.now()}`, true)
const cmd = { const cmd = {
url: input.url, url: input.url,
singleBranch: input.singleBranch, singleBranch: input.singleBranch,
ref: input.branch, ref: input.branch,
depth: input.depth || 10, depth: input.depth || 10,
...await this.addIsomorphicGitConfig(input), ...await isoGit.addIsomorphicGitProxyConfig(input, this),
...await this.addIsomorphicGitConfigFS() ...await this.addIsomorphicGitConfigFS()
} }
this.call('terminal', 'logHtml', `Cloning ${input.url}... please wait...`) this.call('terminal', 'logHtml', `Cloning ${input.url}... please wait...`)
@ -674,6 +414,9 @@ class DGitProvider extends Plugin {
} }
async updateSubmodules(input) { async updateSubmodules(input) {
if ((Registry.getInstance().get('platform').api.isDesktop())) {
return await this.call('isogit', 'updateSubmodules', null)
}
try { try {
const currentDir = (input && input.dir) || '' const currentDir = (input && input.dir) || ''
const gitmodules = await this.parseGitmodules(currentDir) const gitmodules = await this.parseGitmodules(currentDir)
@ -705,10 +448,10 @@ class DGitProvider extends Plugin {
url: module.url, url: module.url,
singleBranch: true, singleBranch: true,
depth: 1, depth: 1,
...await this.addIsomorphicGitConfig({ ...await isoGit.addIsomorphicGitProxyConfig({
...input, ...input,
provider: 'github' provider: 'github',
}), }, this),
...await this.addIsomorphicGitConfigFS(dir) ...await this.addIsomorphicGitConfigFS(dir)
} }
this.call('terminal', 'logHtml', `Cloning submodule ${dir}...`) this.call('terminal', 'logHtml', `Cloning submodule ${dir}...`)
@ -732,10 +475,10 @@ class DGitProvider extends Plugin {
if (result && result.length) { if (result && result.length) {
this.call('terminal', 'logHtml', `Checking out submodule ${dir} to ${result[0]} in directory ${dir}`) this.call('terminal', 'logHtml', `Checking out submodule ${dir} to ${result[0]} in directory ${dir}`)
await git.fetch({ await git.fetch({
...await this.addIsomorphicGitConfig({ ...await isoGit.addIsomorphicGitProxyConfig({
...input, ...input,
provider: 'github' provider: 'github',
}), }, this),
...await this.addIsomorphicGitConfigFS(dir), ...await this.addIsomorphicGitConfigFS(dir),
singleBranch: true, singleBranch: true,
ref: result[0] ref: result[0]
@ -782,55 +525,22 @@ class DGitProvider extends Plugin {
async push(input: pushInputType) { async push(input: pushInputType) {
const cmd = {
force: input.force,
ref: input.ref.name,
remoteRef: input.remoteRef && input.remoteRef.name,
remote: input.remote.name,
author: await this.getCommandUser(input),
input,
}
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
return await this.call('isogit', 'push', cmd) return await this.call('isogit', 'push', input)
} else { } else {
const result = await isoGit.push(input, await this.addIsomorphicGitConfigFS(), this)
const cmd2 = {
...cmd,
...await this.addIsomorphicGitConfig(input),
}
const result = await git.push({
...await this.addIsomorphicGitConfigFS(),
...cmd2
})
return result return result
} }
} }
async pull(input: pullInputType) { async pull(input: pullInputType) {
const cmd = {
ref: input.ref.name,
remoteRef: input.remoteRef && input.remoteRef.name,
author: await this.getCommandUser(input),
remote: input.remote.name,
input,
}
let result let result
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
result = await this.call('isogit', 'pull', cmd) result = await this.call('isogit', 'pull', input)
} }
else { else {
const cmd2 = { result = await isoGit.pull(input, await this.addIsomorphicGitConfigFS(), this)
...cmd,
...await this.addIsomorphicGitConfig(input),
}
result = await git.pull({
...await this.addIsomorphicGitConfigFS(),
...cmd2
})
} }
setTimeout(async () => { setTimeout(async () => {
await this.call('fileManager', 'refresh') await this.call('fileManager', 'refresh')
@ -839,192 +549,19 @@ class DGitProvider extends Plugin {
} }
async fetch(input: fetchInputType) { async fetch(input: fetchInputType) {
const cmd = {
ref: input.ref && input.ref.name,
remoteRef: input.remoteRef && input.remoteRef.name,
author: await this.getCommandUser(input),
remote: input.remote && input.remote.name,
depth: input.depth || 5,
singleBranch: input.singleBranch,
relative: input.relative,
input
}
let result let result
if ((Registry.getInstance().get('platform').api.isDesktop())) { if ((Registry.getInstance().get('platform').api.isDesktop())) {
result = await this.call('isogit', 'fetch', cmd) result = await this.call('isogit', 'fetch', {
} else { ...input,
const cmd2 = {
...cmd,
...await this.addIsomorphicGitConfig(input),
}
result = await git.fetch({
...await this.addIsomorphicGitConfigFS(),
...cmd2
}) })
}
setTimeout(async () => {
await this.call('fileManager', 'refresh')
}, 1000)
return result
}
async export(config) {
if (!this.checkIpfsConfig(config)) return false
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const files = await this.getDirectory('/')
this.filesToSend = []
for (const file of files) {
const c = await window.remixFileSystem.readFile(`${workspace.absolutePath}/${file}`, null)
const ob = {
path: file,
content: c
}
this.filesToSend.push(ob)
}
const addOptions = {
wrapWithDirectory: true
}
const r = await this.ipfs.add(this.filesToSend, addOptions)
return r.cid.string
}
async importIPFSFiles(config, cid, workspace) {
const ipfs = IpfsHttpClient(config)
let result = false
try {
const data = ipfs.get(cid, { timeout: 60000 })
for await (const file of data) {
if (file.path) result = true
file.path = file.path.replace(cid, '')
if (!file.content) {
continue
}
const content = []
for await (const chunk of file.content) {
content.push(chunk)
}
const dir = path.dirname(file.path)
try {
await this.createDirectories(`${workspace.absolutePath}/${dir}`)
} catch (e) { throw new Error(e) }
try {
await window.remixFileSystem.writeFile(`${workspace.absolutePath}/${file.path}`, Buffer.concat(content) || new Uint8Array(), null)
} catch (e) { throw new Error(e) }
}
} catch (e) {
throw new Error(e)
}
return result
}
calculateLocalStorage() {
let _lsTotal = 0
let _xLen; let _x
for (_x in localStorage) {
// eslint-disable-next-line no-prototype-builtins
if (!localStorage.hasOwnProperty(_x)) {
continue
}
_xLen = ((localStorage[_x].length + _x.length) * 2)
_lsTotal += _xLen
}
return (_lsTotal / 1024).toFixed(2)
}
async import(cmd) {
const permission = await this.askUserPermission('import', 'Import multiple files into your workspaces.')
if (!permission) return false
if (parseFloat(this.calculateLocalStorage()) > 10000) throw new Error('The local storage of the browser is full.')
const cid = cmd.cid
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true)
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
let result
if (cmd.local) {
result = await this.importIPFSFiles(this.ipfsconfig, cid, workspace)
} else { } else {
result = await this.importIPFSFiles(this.remixIPFS, cid, workspace) || await this.importIPFSFiles(this.ipfsconfig, cid, workspace) || await this.importIPFSFiles(this.globalIPFSConfig, cid, workspace) result = await isoGit.fetch(input, await this.addIsomorphicGitConfigFS(), this)
} }
setTimeout(async () => { setTimeout(async () => {
await this.call('fileManager', 'refresh') await this.call('fileManager', 'refresh')
}, 1000) }, 1000)
if (!result) throw new Error(`Cannot pull files from IPFS at ${cid}`)
}
async getItem(name) {
if (typeof window !== 'undefined') {
return window.localStorage.getItem(name)
}
}
async setItem(name, content) {
try {
if (typeof window !== 'undefined') {
window.localStorage.setItem(name, content)
}
} catch (e) {
console.log(e)
return false
}
return true
}
async zip() {
const zip = new JSZip()
const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const files = await this.getDirectory('/')
this.filesToSend = []
for (const file of files) {
const c = await window.remixFileSystem.readFile(`${workspace.absolutePath}/${file}`, null)
zip.file(file, c)
}
await zip.generateAsync({
type: 'blob'
})
.then(function (content) {
saveAs(content, `${workspace.name}.zip`)
})
}
async createDirectories(strdirectories) {
const ignore = ['.', '/.', '']
if (ignore.indexOf(strdirectories) > -1) return false
const directories = strdirectories.split('/')
for (let i = 0; i < directories.length; i++) {
let previouspath = ''
if (i > 0) previouspath = '/' + directories.slice(0, i).join('/')
const finalPath = previouspath + '/' + directories[i]
try {
if (!await window.remixFileSystem.exists(finalPath)) {
await window.remixFileSystem.mkdir(finalPath)
}
} catch (e) {
console.log(e)
}
}
}
async getDirectory(dir) {
let result = []
const files = await this.call('fileManager', 'readdir', dir)
const fileArray = normalize(files)
for (const fi of fileArray) {
if (fi) {
const type = fi.data.isDirectory
if (type === true) {
result = [
...result,
...(await this.getDirectory(
`${fi.filename}`
))
]
} else {
result = [...result, fi.filename]
}
}
}
return result return result
} }
@ -1079,6 +616,7 @@ class DGitProvider extends Plugin {
} }
async remotecommits(input: remoteCommitsInputType): Promise<pagedCommits[]> { async remotecommits(input: remoteCommitsInputType): Promise<pagedCommits[]> {
const octokit = new Octokit({ const octokit = new Octokit({
auth: input.token auth: input.token
}) })
@ -1169,23 +707,4 @@ const addSlash = (file) => {
return file return file
} }
const normalize = (filesList) => {
const folders = []
const files = []
Object.keys(filesList || {}).forEach(key => {
if (filesList[key].isDirectory) {
folders.push({
filename: key,
data: filesList[key]
})
} else {
files.push({
filename: key,
data: filesList[key]
})
}
})
return [...folders, ...files]
}
module.exports = DGitProvider module.exports = DGitProvider

@ -6,10 +6,7 @@ import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
import {Registry} from '@remix-project/remix-lib' import {Registry} from '@remix-project/remix-lib'
import { RemixdHandle } from '../plugins/remixd-handle' import { RemixdHandle } from '../plugins/remixd-handle'
import {PluginViewWrapper} from '@remix-ui/helper' import {PluginViewWrapper} from '@remix-ui/helper'
const { HardhatHandle } = require('../files/hardhat-handle.js')
const { FoundryHandle } = require('../files/foundry-handle.js')
const { TruffleHandle } = require('../files/truffle-handle.js') const { TruffleHandle } = require('../files/truffle-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js')
/* /*
Overview of APIs: Overview of APIs:
@ -69,10 +66,7 @@ module.exports = class Filepanel extends ViewPlugin {
this.el.setAttribute('id', 'fileExplorerView') this.el.setAttribute('id', 'fileExplorerView')
this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager) this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager)
this.hardhatHandle = new HardhatHandle()
this.foundryHandle = new FoundryHandle()
this.truffleHandle = new TruffleHandle() this.truffleHandle = new TruffleHandle()
this.slitherHandle = new SlitherHandle()
this.contentImport = contentImport this.contentImport = contentImport
this.workspaces = [] this.workspaces = []
this.appManager = appManager this.appManager = appManager

@ -0,0 +1,54 @@
import { ElectronPlugin } from '@remixproject/engine-electron'
const profile = {
displayName: 'appUpdater',
name: 'appUpdater',
description: 'appUpdater',
}
export class appUpdaterPlugin extends ElectronPlugin {
constructor() {
super(profile)
}
onActivation(): void {
this.on('appUpdater', 'askForUpdate', () => {
console.log('askForUpdate')
const upgradeModal = {
id: 'confirmUpdate',
title: 'An update is available',
message: `A new version of Remix Desktop is available. Do you want to update?`,
modalType: 'modal',
okLabel: 'Yes',
cancelLabel: 'No',
okFn: () => {
this.call('appUpdater', 'download')
},
cancelFn: () => {
},
hideFn: () => null
}
this.call('notification', 'modal', upgradeModal)
})
this.on('appUpdater', 'downloadReady', () => {
console.log('downloadReady')
const upgradeModal = {
id: 'confirmInstall',
title: 'An update is ready to install',
message: `A new version of Remix Desktop is ready to install. Do you want to install it now? This will close Remix Desktop.`,
modalType: 'modal',
okLabel: 'Yes',
cancelLabel: 'No',
okFn: () => {
this.call('appUpdater', 'install')
},
cancelFn: () => {
},
hideFn: () => null
}
this.call('notification', 'modal', upgradeModal)
})
}
}

@ -0,0 +1,13 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class FoundryHandleDesktop extends ElectronPlugin {
constructor() {
super({
displayName: 'foundry',
name: 'foundry',
description: 'electron foundry',
methods: ['sync', 'compile']
})
this.methods = ['sync', 'compile']
}
}

@ -0,0 +1,13 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class HardhatHandleDesktop extends ElectronPlugin {
constructor() {
super({
displayName: 'hardhat',
name: 'hardhat',
description: 'electron hardhat',
methods: ['sync', 'compile']
})
this.methods = ['sync', 'compile']
}
}

@ -0,0 +1,13 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class SlitherHandleDesktop extends ElectronPlugin {
constructor() {
super({
displayName: 'slither',
name: 'slither',
description: 'electron slither',
methods: ['analyse']
})
this.methods = ['analyse']
}
}

@ -98,7 +98,7 @@ export class PermissionHandlerPlugin extends Plugin {
<div className='d-flex flex-row'> <div className='d-flex flex-row'>
<span onClick={()=>{}}>To change the permission go to </span> <span onClick={()=>{}}>To change the permission go to </span>
<span className='px-2' style={{ fontWeight: 'bolder' }}>Plugin Manager</span> <span className='px-2' style={{ fontWeight: 'bolder' }}>Plugin Manager</span>
<img alt="" id="permissionModalImagesFrom" src="/assets/img/pluginManager.webp" style={{ height: '1rem', width: '1rem' }} /> <img alt="" id="permissionModalImagesFrom" src="assets/img/pluginManager.webp" style={{ height: '1rem', width: '1rem' }} />
<span className='pl-1' style={{ fontWeight: 'bolder' }}> / Permissions</span> <span className='pl-1' style={{ fontWeight: 'bolder' }}> / Permissions</span>
</div> </div>
</div> </div>

@ -1,11 +1,12 @@
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import * as templateWithContent from '@remix-project/remix-ws-templates' import * as templateWithContent from '@remix-project/remix-ws-templates'
import { TEMPLATE_METADATA } from '@remix-ui/workspace'
const profile = { const profile = {
name: 'remix-templates', name: 'remix-templates',
displayName: 'remix-templates', displayName: 'remix-templates',
description: 'Remix Templates plugin', description: 'Remix Templates plugin',
methods: ['getTemplate', 'loadTemplateInNewWindow'], methods: ['getTemplate', 'loadTemplateInNewWindow', 'loadFilesInNewWindow'],
} }
export class TemplatesPlugin extends Plugin { export class TemplatesPlugin extends Plugin {
@ -23,8 +24,28 @@ export class TemplatesPlugin extends Plugin {
} }
// electron only method // electron only method
async loadTemplateInNewWindow (template: string, opts?: any) { async loadTemplateInNewWindow (template: string, opts?: any) {
const metadata = TEMPLATE_METADATA[template]
if (metadata) {
if (metadata.type === 'git') {
this.call('notification', 'alert', {
id: 'dgitAlert',
message: 'This template is not available in the desktop version',
})
return
} else if (metadata.type === 'plugin'){
this.call('notification', 'alert', {
id: 'dgitAlert',
message: 'This template is not available in the desktop version',
})
return
}
}
const files = await this.getTemplate(template, opts) const files = await this.getTemplate(template, opts)
this.call('electronTemplates', 'loadTemplateInNewWindow', files) this.call('electronTemplates', 'loadTemplateInNewWindow', files)
} }
async loadFilesInNewWindow (files: any) {
this.call('electronTemplates', 'loadTemplateInNewWindow', files)
}
} }

@ -93,7 +93,7 @@ export class RemixGuidePlugin extends ViewPlugin {
<RemixUIGridView <RemixUIGridView
plugin={this} plugin={this}
styleList={""} styleList={""}
logo='/assets/img/YouTubeLogo.webp' logo='assets/img/YouTubeLogo.webp'
enableFilter={true} enableFilter={true}
showUntagged={true} showUntagged={true}
showPin={false} showPin={false}

@ -1,5 +1,5 @@
{ {
"logo": "/assets/img/YouTubeLogo.webp", "logo": "assets/img/YouTubeLogo.webp",
"title": "Remix Guide", "title": "Remix Guide",
"description": "Streamlined access to categorized video tutorials for mastering Remix IDE. From fundamentals to advanced techniques, level up your development skills with ease.", "description": "Streamlined access to categorized video tutorials for mastering Remix IDE. From fundamentals to advanced techniques, level up your development skills with ease.",
"sections": [ "sections": [

@ -9,7 +9,7 @@ import { QueryParams } from '@remix-project/remix-lib'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import { compilerConfigChangedToastMsg, compileToastMsg } from '@remix-ui/helper' import { compilerConfigChangedToastMsg, compileToastMsg } from '@remix-ui/helper'
import { isNative } from '../../remixAppManager' import { isNative } from '../../remixAppManager'
import { Registry } from '@remix-project/remix-lib'
const profile = { const profile = {
name: 'solidity', name: 'solidity',
displayName: 'Solidity compiler', displayName: 'Solidity compiler',
@ -90,6 +90,10 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
return this.fileManager.mode return this.fileManager.mode
} }
isDesktop () {
return Registry.getInstance().get('platform').api.isDesktop()
}
/** /**
* set the compiler configuration * set the compiler configuration
* This function is used by remix-plugin compiler API. * This function is used by remix-plugin compiler API.

@ -1,4 +1,6 @@
{ {
"electron.openFolder": "Open Folder", "electron.openFolder": "Open Folder",
"electron.recentFolders": "Recent Folders" "electron.recentFolders": "Recent Folders",
"electron.gitClone": "Clone a Git Repository",
"electron.openFolderMessage": "In order to use Git features, you can open a folder or clone a repository."
} }

@ -0,0 +1,3 @@
{
"gitui.openFolderMessage": "In order to use Git features, you can open a folder or clone a repository."
}

@ -2,20 +2,17 @@ const domains = {
'remix-alpha.ethereum.org': 27, 'remix-alpha.ethereum.org': 27,
'remix-beta.ethereum.org': 25, 'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23, 'remix.ethereum.org': 23,
'6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod': 35 // remix desktop 'localhost': 35 // remix desktop
} }
const domainsSecondaryTracker = { let domainToTrack = domains[window.location.hostname]
'remix-alpha.ethereum.org': 27,
'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23,
'6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod': 35 // remix desktop
}
if (domains[window.location.hostname]) {
function trackDomain(domainToTrack) {
var _paq = window._paq = window._paq || [] var _paq = window._paq = window._paq || []
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setExcludedQueryParams", ["code","gist"]]); _paq.push(["setExcludedQueryParams", ["code", "gist"]]);
_paq.push(["setExcludedReferrers", ["etherscan.io"]]); _paq.push(["setExcludedReferrers", ["etherscan.io"]]);
_paq.push(['enableJSErrorTracking']); _paq.push(['enableJSErrorTracking']);
_paq.push(['trackPageView']); _paq.push(['trackPageView']);
@ -31,13 +28,37 @@ if (domains[window.location.hostname]) {
} }
(function () { (function () {
var u = "https://ethereumfoundation.matomo.cloud/"; var u = "https://ethereumfoundation.matomo.cloud/";
_paq.push(['setTrackerUrl', u + 'matomo.php']); _paq.push(['setTrackerUrl', u + 'matomo.php?debug=1']);
_paq.push(['setSiteId', domains[window.location.hostname]]); _paq.push(['setSiteId', domainToTrack]);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true; g.src = '//cdn.matomo.cloud/ethereumfoundation.matomo.cloud/matomo.js'; s.parentNode.insertBefore(g,s); g.async = true; g.src = 'https://cdn.matomo.cloud/ethereumfoundation.matomo.cloud/matomo.js'; s.parentNode.insertBefore(g, s);
})() })();
} }
if (window.electronAPI) {
window.electronAPI.canTrackMatomo().then((canTrack) => {
if (!canTrack) {
console.log('Matomo tracking is disabled on Dev mode')
return
}
window._paq = {
push: function (...data) {
if (!window.localStorage.getItem('config-v0.8:.remix.config') ||
(window.localStorage.getItem('config-v0.8:.remix.config') && !window.localStorage.getItem('config-v0.8:.remix.config').includes('settings/matomo-analytics'))) {
// require user tracking consent before processing data
} else {
if (JSON.parse(window.localStorage.getItem('config-v0.8:.remix.config'))['settings/matomo-analytics']) {
window.electronAPI.trackEvent(...data)
}
}
}
}
})
} else {
if (domainToTrack) {
trackDomain(domainToTrack)
}
}
function isElectron() { function isElectron() {
// Renderer process // Renderer process
if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') { if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') {

@ -165,7 +165,7 @@ export class RemixAppManager extends PluginManager {
this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json' this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json'
this.pluginLoader = new PluginLoader() this.pluginLoader = new PluginLoader()
if (Registry.getInstance().get('platform').api.isDesktop()) { if (Registry.getInstance().get('platform').api.isDesktop()) {
requiredModules = [...requiredModules, 'fs', 'electronTemplates', 'isogit', 'remix-templates', 'electronconfig', 'xterm', 'compilerloader', 'ripgrep'] requiredModules = [...requiredModules, 'fs', 'electronTemplates', 'isogit', 'remix-templates', 'electronconfig', 'xterm', 'compilerloader', 'ripgrep', 'slither']
} }
} }
@ -299,7 +299,7 @@ export class RemixAppManager extends PluginManager {
} }
return plugins.map(plugin => { return plugins.map(plugin => {
if (plugin.name === 'dgit' && Registry.getInstance().get('platform').api.isDesktop()) { plugin.url = 'https://dgit4-76cc9.web.app/' } // temporary fix if (plugin.name === 'dgit' && Registry.getInstance().get('platform').api.isDesktop()) { plugin.url = 'https://dgit4-76cc9.web.app/' }
if (plugin.name === testPluginName) plugin.url = testPluginUrl if (plugin.name === testPluginName) plugin.url = testPluginUrl
return new IframePlugin(plugin) return new IframePlugin(plugin)
}) })

@ -2,21 +2,159 @@
## Development ## Development
### Running the app locally
In the main repo yarn, then run yarn serve In the main repo yarn, then run yarn serve
In this directory apps/remixdesktop, yarn, then run: yarn start:dev to boot the electron app In this directory apps/remixdesktop, yarn, then run: yarn start:dev to boot the electron app
In chrome chrome://inspect/#devices you can add localhost:5858 to the network targets and then you will see an inspect button electron/js2c/browser_init
file:/// Then app will be started in live reload mode, anything you do in Remix IDE will be reloaded.
You can use that to inspect the output of the electron app It will not however reload electron code. You need to rerun yarn start:dev every time.
If you run into issues with yarn when native node modules are being rebuilt you need If you run into issues with yarn when native node modules are being rebuilt you need
- Windows: install Visual Studio Tools with Desktop Development C++ enabled in the Workloads - Windows: install Visual Studio Tools with Desktop Development C++ enabled in the Workloads
- MacOS: install Xcode or Xcode Command Line Tools - MacOS: install Xcode or Xcode Command Line Tools. Also make sure the compilers (clang++ | g++) target the right sdk includes, ```export SDKROOT="xcrun --show-sdk-path"```
- Linux: unknown, probably a C++ compiler - Linux: unknown, probably a C++ compiler
## Builds
Builds can be found in the artefacts of CI. ### Electron Plugin
Electron has its own Plugin Engine, which holds plugins, these plugins have plugin clients attached to them. Each of those clients is created when an instance of Remix Desktop connects
and activates a plugin. Follow all these steps to make that work.
1. create a plugin file in apps/remixdesktop/src/plugins
2. add imports:
```
import { Profile } from '@remixproject/plugin-utils'
import { ElectronBasePlugin, ElectronBasePluginClient } from '@remixproject/plugin-electron'
```
3. add a base profile and a client profile:
```
const profile: Profile = {
displayName: 'compilerLoader',
name: 'compilerloader',
description: 'Compiler Loader',
}
const clientProfile: Profile = {
name: 'compilerloader',
displayName: 'compilerloader',
description: 'Compiler Loader',
methods: ['downloadCompiler', 'listCompilers', 'getBaseUrls', 'getJsonBinData'],
}
```
As you can see in the clientProfile you define the methods which are exposed to the Remix plugin system.
5. add a base plugin and a plugin client
```
export class CompilerLoaderPlugin extends ElectronBasePlugin {
clients: CompilerLoaderPluginClient[] = []
constructor() {
super(profile, clientProfile, CompilerLoaderPluginClient)
this.methods = [...super.methods]
}
}
class CompilerLoaderPluginClient extends ElectronBasePluginClient {
solJsonBinData: iSolJsonBinData
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
}
async onActivation(): Promise<void> {
this.onload(() => {
this.emit('loaded')
})
}
}
```
The ElectronBasePluginClient is the specific instance which will be connected to the IDE. The BasePlugin is just holding all the clients for all the instances.
Any instance specific code is set as functions on the ElectronBasePluginClient class.
6. If you need fs access you need to track the workingdir like we do here:
This ensures you know where the user is working
```
class IsoGitPluginClient extends ElectronBasePluginClient {
workingDir: string = ''
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
this.onload(async () => {
this.on('fs' as any, 'workingDirChanged', async (path: string) => {
this.workingDir = path
})
this.workingDir = await this.call('fs' as any, 'getWorkingDir')
})
}
```
7. If you need to call methods on the BASE which holds all the clients you can add methods there, for example this iterates over clients
and finds the one with the webContentsId. This ID passed on ie by menu items. Look at menu.ts to see how that works.
```
openTemplate(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId)
if (client) {
client.openTemplate()
}
}
```
8. Add your plugin to engine.ts
```
const compilerLoaderPlugin = new CompilerLoaderPlugin()
```
9. Register the plugin in engine.ts
```
engine.register(compilerLoaderPlugin)
```
10. activation of plugins is done when the clients connect to the engine. No need to activate it.
11. Add the plugin to the preload.ts. Add it to this list:
```
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader', 'appUpdater']
```
If you don't this, it won't work.
12. In Remix IDE create a plugin in src/app/plugins/electron. If everything works correctly the methods will be loaded from the electron side, no need to specify them here.
This plugin is only a passthrough.
```
const profile = {
displayName: 'compilerLoader',
name: 'compilerloader',
description: 'Loads the compiler for offline use',
}
export class compilerLoaderPluginDesktop extends ElectronPlugin {
constructor() {
super(profile)
this.methods = []
}
async onActivation(): Promise<void> {
// something to do
}
}
```
13. if you need to activate that on load you need to add it to the app.js where other plugins are activated.
## CI ## CI
CI will only run the builds is the branch is master or contains the word: desktop CI will only run the builds is the branch is master or contains the word: desktop

@ -0,0 +1,14 @@
const fs = require('fs-extra');
const path = require('path');
exports.default = async function (context) {
console.log('Running after-pack hook', context);
const resourcesPath = context.appOutDir;
console.log('resourcesPath', resourcesPath);
console.log('context outdir', context.appOutDir);
// Copy the node-pty module to the app folder
await fs.copy(
path.join('./node_modules', 'node-pty'),
path.join(resourcesPath, 'node_modules', 'node-pty')
);
};

@ -0,0 +1,30 @@
const fs = require('fs');
exports.default = async function afterbuild(context) {
// do not run when not on macOS or when not on CIRCLECI
if (process.platform !== 'darwin' || !process.env.CIRCLE_BRANCH) {
return;
}
console.log('AFTER BUILD', context);
const artifactPaths = context.artifactPaths;
const newDmgs = artifactPaths.filter((dmg) => dmg.endsWith('.dmg')).map((dmg) => dmg); // Removed unnecessary quotes for consistency
let existingDmgs = [];
try {
// Attempt to read the existing dmgs.json file
const data = fs.readFileSync('dmgs.json', 'utf8');
const parsedData = JSON.parse(data);
existingDmgs = parsedData.dmgs || []; // Ensure existingDmgs is an array
} catch (error) {
// If there's an error reading the file (e.g., file does not exist), proceed with an empty array
console.log('No existing dmgs.json or error reading file, creating new one.');
}
// Combine existing and new dmgs, avoiding duplicates
const combinedDmgs = [...new Set([...existingDmgs, ...newDmgs])];
// Write/overwrite the dmgs.json with the combined list of dmgs
fs.writeFileSync('dmgs.json', JSON.stringify({ dmgs: combinedDmgs }, null, 2));
};

@ -0,0 +1,104 @@
const { notarize } = require('@electron/notarize')
const fs = require('fs')
const { exec } = require('child_process') // Import the exec function
// read the environment variables from process
console.log(process.env.DO_NOT_NOTARIZE)
if (process.env.DO_NOT_NOTARIZE) {
console.log('NOTARIZING DISABLED')
exports.default = async function notarizing(context) {
return []
}
} else {
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context // Provided by electron-builder
console.log('NOTARIZING')
if (electronPlatformName !== 'darwin' || !process.env.CIRCLE_BRANCH) {
return
}
const appName = context.packager.appInfo.productFilename
const appPath = `${appOutDir}/${appName}.app`
// Function to promisify the exec command
function execShellCommand(cmd) {
return new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(new Error(`Error: ${error.message}`));
return;
}
if (stderr) {
reject(new Error(`Stderr: ${stderr}`));
return;
}
console.log(`stdout: ${stdout}`);
resolve(stdout);
});
});
}
// Function to check if the app is stapled
// Async function to check the stapling status
async function checkStapleStatus() {
try {
console.log(`xcrun stapler validate "${appPath}"`)
await execShellCommand(`xcrun stapler validate "${appPath}"`);
console.log('App is already stapled. No action needed.');
return true
} catch (error) {
console.log(`App is not stapled: ${error.message}`);
return false
}
}
async function runNotarize() {
console.log('NOTARIZING + ', `xcrun stapler staple "${appPath}"`)
console.log({
appBundleId: 'org.ethereum.remix-ide', // Your app's bundle ID
appPath: `${appOutDir}/${appName}.app`, // Path to your .app
appleId: process.env.APPLE_ID, // Your Apple ID
appleIdPassword: process.env.APPLE_ID_PASSWORD, // App-specific password
teamId: process.env.APPLE_TEAM_ID, // Your Apple Developer team ID (optional)
})
try {
const r = await notarize({
appBundleId: 'org.ethereum.remix-ide', // Your app's bundle ID
appPath: `${appOutDir}/${appName}.app`, // Path to your .app
appleId: process.env.APPLE_ID, // Your Apple ID
appleIdPassword: process.env.APPLE_ID_PASSWORD, // App-specific password
teamId: process.env.APPLE_TEAM_ID, // Your Apple Developer team ID (optional)
})
console.log(r)
// Stapling the app
console.log('STAPLING', `xcrun stapler staple "${appPath}"`)
await execShellCommand(`xcrun stapler staple "${appPath}"`)
} catch (error) {
console.error('Error during notarization:', error)
throw new Error('Error during notarization', error)
}
}
if (!await checkStapleStatus()) {
await runNotarize()
await checkStapleStatus()
} else {
return []
}
}
}

@ -0,0 +1,61 @@
{
"productName": "Remix-Desktop-alpha",
"appId": "org.ethereum.remix-ide",
"asar": true,
"generateUpdatesFilesForAllChannels": false,
"icon": "assets",
"files": [
"build/**/*"
],
"afterSign": "aftersign.js",
"afterAllArtifactBuild": "afterbuild.js",
"publish": [
{
"provider": "github",
"owner": "remix-project-org",
"repo": "remix-desktop-alpha",
"releaseType": "draft",
"publishAutoUpdate": true
}
],
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/icon.png",
"darkModeSupport": true,
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
},
"dmg": {
"writeUpdateInfo": true,
"sign": true
},
"nsis": {
"createDesktopShortcut": "always",
"allowToChangeInstallationDirectory": true,
"oneClick": false,
"shortcutName": "Remix Desktop alpha",
"differentialPackage": false
},
"win": {
"target": [
"nsis"
],
"artifactName": "${productName}-Setup-${version}.${ext}",
"icon": "assets/icon.png"
},
"deb": {},
"linux": {
"target": [
"deb",
"snap",
"AppImage"
],
"category": "WebBrowser",
"icon": "assets"
},
"directories": {
"output": "release"
}
}

@ -0,0 +1,61 @@
{
"productName": "Remix-Desktop-Beta",
"appId": "org.ethereum.remix-ide",
"asar": true,
"generateUpdatesFilesForAllChannels": false,
"icon": "assets",
"files": [
"build/**/*"
],
"afterSign": "aftersign.js",
"afterAllArtifactBuild": "afterbuild.js",
"publish": [
{
"provider": "github",
"owner": "remix-project-org",
"repo": "remix-desktop-beta",
"releaseType": "draft",
"publishAutoUpdate": true
}
],
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/icon.png",
"darkModeSupport": true,
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
},
"dmg": {
"writeUpdateInfo": true,
"sign": true
},
"nsis": {
"createDesktopShortcut": "always",
"allowToChangeInstallationDirectory": true,
"oneClick": false,
"shortcutName": "Remix Desktop Beta",
"differentialPackage": false
},
"win": {
"target": [
"nsis"
],
"artifactName": "Remix-Desktop-Setup-${version}.${ext}",
"icon": "assets/icon.png"
},
"deb": {},
"linux": {
"target": [
"deb",
"snap",
"AppImage"
],
"category": "WebBrowser",
"icon": "assets"
},
"directories": {
"output": "release"
}
}

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

@ -0,0 +1,12 @@
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['src/main.ts', 'src/preload.ts'], // Your TypeScript entry point
outdir: 'build', // Output bundled file
bundle: true, // Bundle all dependencies
platform: 'node', // Target Node.js platform
external: ['electron', 'fsevents', 'node-pty'], // Exclude native modules
target: ['node20'], // Match the Node.js version for Electron
tsconfig: 'tsconfig.json', // Your TypeScript config
minify: false, // Optional: Minify for production
}).catch(() => process.exit(1));

@ -0,0 +1,62 @@
{
"productName": "Remix-Desktop-Insiders",
"appId": "org.ethereum.remix-ide",
"asar": true,
"generateUpdatesFilesForAllChannels": false,
"icon": "assets",
"files": [
"build/**/*",
"node_modules/node-pty/**/*"
],
"afterSign": "aftersign.js",
"afterAllArtifactBuild": "afterbuild.js",
"publish": [
{
"provider": "github",
"owner": "remix-project-org",
"repo": "remix-desktop-insiders",
"releaseType": "draft",
"publishAutoUpdate": true
}
],
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/icon.png",
"darkModeSupport": true,
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
},
"dmg": {
"writeUpdateInfo": true,
"sign": true
},
"nsis": {
"createDesktopShortcut": "always",
"allowToChangeInstallationDirectory": true,
"oneClick": false,
"shortcutName": "Remix Desktop Insiders",
"differentialPackage": false
},
"win": {
"target": [
"nsis"
],
"artifactName": "Remix-Desktop-Setup-${version}.${ext}",
"icon": "assets/icon.png"
},
"deb": {},
"linux": {
"target": [
"deb",
"snap",
"AppImage"
],
"category": "WebBrowser",
"icon": "assets"
},
"directories": {
"output": "release"
}
}

@ -0,0 +1,61 @@
{
"productName": "Remix-Desktop",
"appId": "org.ethereum.remix-ide",
"asar": true,
"generateUpdatesFilesForAllChannels": false,
"icon": "assets",
"files": [
"build/**/*"
],
"afterSign": "aftersign.js",
"afterAllArtifactBuild": "afterbuild.js",
"publish": [
{
"provider": "github",
"owner": "remix-project-org",
"repo": "remix-desktop",
"releaseType": "draft",
"publishAutoUpdate": true
}
],
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/icon.png",
"darkModeSupport": true,
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
},
"dmg": {
"writeUpdateInfo": true,
"sign": true
},
"nsis": {
"createDesktopShortcut": "always",
"allowToChangeInstallationDirectory": true,
"oneClick": false,
"shortcutName": "Remix Desktop",
"differentialPackage": false
},
"win": {
"target": [
"nsis"
],
"artifactName": "${productName}-Setup-${version}.${ext}",
"icon": "assets/icon.png"
},
"deb": {},
"linux": {
"target": [
"deb",
"snap",
"AppImage"
],
"category": "WebBrowser",
"icon": "assets"
},
"directories": {
"output": "release"
}
}

@ -0,0 +1,344 @@
//
// Refer to the online docs for more details:
// https://nightwatchjs.org/guide/configuration/nightwatch-configuration-file.html
//
// _ _ _ _ _ _ _
// | \ | |(_) | | | | | | | |
// | \| | _ __ _ | |__ | |_ __ __ __ _ | |_ ___ | |__
// | . ` || | / _` || '_ \ | __|\ \ /\ / / / _` || __| / __|| '_ \
// | |\ || || (_| || | | || |_ \ V V / | (_| || |_ | (__ | | | |
// \_| \_/|_| \__, ||_| |_| \__| \_/\_/ \__,_| \__| \___||_| |_|
// __/ |
// |___/
//
module.exports = {
// An array of folders (excluding subfolders) where your tests are located;
// if this is not specified, the test source must be passed as the second argument to the test runner.
src_folders: [],
// See https://nightwatchjs.org/guide/concepts/page-object-model.html
page_objects_path: ['node_modules/nightwatch/examples/pages/'],
// See https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-commands.html
custom_commands_path: ['node_modules/nightwatch/examples/custom-commands/'],
// See https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-assertions.html
custom_assertions_path: '',
// See https://nightwatchjs.org/guide/extending-nightwatch/adding-plugins.html
plugins: [],
// See https://nightwatchjs.org/guide/concepts/test-globals.html#external-test-globals
globals_path : '',
webdriver: {},
test_settings: {
default: {
disable_error_log: false,
launch_url: 'https://nightwatchjs.org',
screenshots: {
enabled: false,
path: 'screens',
on_failure: true
},
desiredCapabilities: {
browserName : 'firefox'
},
webdriver: {
start_process: true,
server_path: ''
}
},
firefox: {
desiredCapabilities : {
browserName : 'firefox',
alwaysMatch: {
acceptInsecureCerts: true,
'moz:firefoxOptions': {
args: [
// '-headless',
// '-verbose'
]
}
}
},
webdriver: {
start_process: true,
server_path: '',
cli_args: [
// very verbose geckodriver logs
// '-vv'
]
}
},
chrome: {
desiredCapabilities : {
browserName : 'chrome',
'goog:chromeOptions' : {
// More info on Chromedriver: https://sites.google.com/a/chromium.org/chromedriver/
//
// w3c:false tells Chromedriver to run using the legacy JSONWire protocol (not required in Chrome 78)
w3c: true,
args: [
//'--no-sandbox',
//'--ignore-certificate-errors',
//'--allow-insecure-localhost',
//'--headless'
]
}
},
webdriver: {
start_process: true,
server_path: '',
cli_args: [
// --verbose
]
}
},
edge: {
desiredCapabilities : {
browserName : 'MicrosoftEdge',
'ms:edgeOptions' : {
w3c: true,
// More info on EdgeDriver: https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options
args: [
//'--headless'
]
}
},
webdriver: {
start_process: true,
// Download msedgedriver from https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/
// and set the location below:
server_path: '',
cli_args: [
// --verbose
]
}
},
//////////////////////////////////////////////////////////////////////////////////
// Configuration for when using cucumber-js (https://cucumber.io) |
// |
// It uses the bundled examples inside the nightwatch examples folder; feel free |
// to adapt this to your own project needs |
//////////////////////////////////////////////////////////////////////////////////
'cucumber-js': {
src_folders: ['examples/cucumber-js/features/step_definitions'],
test_runner: {
// set cucumber as the runner
type: 'cucumber',
// define cucumber specific options
options: {
//set the feature path
feature_path: 'node_modules/nightwatch/examples/cucumber-js/*/*.feature',
// start the webdriver session automatically (enabled by default)
// auto_start_session: true
// use parallel execution in Cucumber
// parallel: 2 // set number of workers to use (can also be defined in the cli as --parallel 2
}
}
},
//////////////////////////////////////////////////////////////////////////////////
// Configuration for when using the browserstack.com cloud service |
// |
// Please set the username and access key by setting the environment variables: |
// - BROWSERSTACK_USERNAME |
// - BROWSERSTACK_ACCESS_KEY |
// .env files are supported |
//////////////////////////////////////////////////////////////////////////////////
browserstack: {
selenium: {
host: 'hub.browserstack.com',
port: 443
},
// More info on configuring capabilities can be found on:
// https://www.browserstack.com/automate/capabilities?tag=selenium-4
desiredCapabilities: {
'bstack:options' : {
userName: '${BROWSERSTACK_USERNAME}',
accessKey: '${BROWSERSTACK_ACCESS_KEY}',
}
},
disable_error_log: true,
webdriver: {
timeout_options: {
timeout: 15000,
retry_attempts: 3
},
keep_alive: true,
start_process: false
}
},
'browserstack.local': {
extends: 'browserstack',
desiredCapabilities: {
'browserstack.local': true
}
},
'browserstack.chrome': {
extends: 'browserstack',
desiredCapabilities: {
browserName: 'chrome',
chromeOptions : {
w3c: true
}
}
},
'browserstack.firefox': {
extends: 'browserstack',
desiredCapabilities: {
browserName: 'firefox'
}
},
'browserstack.ie': {
extends: 'browserstack',
desiredCapabilities: {
browserName: 'internet explorer',
browserVersion: '11.0'
}
},
'browserstack.safari': {
extends: 'browserstack',
desiredCapabilities: {
browserName: 'safari'
}
},
'browserstack.local_chrome': {
extends: 'browserstack.local',
desiredCapabilities: {
browserName: 'chrome'
}
},
'browserstack.local_firefox': {
extends: 'browserstack.local',
desiredCapabilities: {
browserName: 'firefox'
}
},
//////////////////////////////////////////////////////////////////////////////////
// Configuration for when using the SauceLabs cloud service |
// |
// Please set the username and access key by setting the environment variables: |
// - SAUCE_USERNAME |
// - SAUCE_ACCESS_KEY |
//////////////////////////////////////////////////////////////////////////////////
saucelabs: {
selenium: {
host: 'ondemand.saucelabs.com',
port: 443
},
// More info on configuring capabilities can be found on:
// https://docs.saucelabs.com/dev/test-configuration-options/
desiredCapabilities: {
'sauce:options' : {
username: '${SAUCE_USERNAME}',
accessKey: '${SAUCE_ACCESS_KEY}',
screenResolution: '1280x1024'
// https://docs.saucelabs.com/dev/cli/sauce-connect-proxy/#--region
// region: 'us-west-1'
// https://docs.saucelabs.com/dev/test-configuration-options/#tunnelidentifier
// parentTunnel: '',
// tunnelIdentifier: '',
}
},
disable_error_log: false,
webdriver: {
start_process: false
}
},
'saucelabs.chrome': {
extends: 'saucelabs',
desiredCapabilities: {
browserName: 'chrome',
browserVersion: 'latest',
javascriptEnabled: true,
acceptSslCerts: true,
timeZone: 'London',
chromeOptions : {
w3c: true
}
}
},
'saucelabs.firefox': {
extends: 'saucelabs',
desiredCapabilities: {
browserName: 'firefox',
browserVersion: 'latest',
javascriptEnabled: true,
acceptSslCerts: true,
timeZone: 'London'
}
},
//////////////////////////////////////////////////////////////////////////////////
// Configuration for when using the Selenium service, either locally or remote, |
// like Selenium Grid |
//////////////////////////////////////////////////////////////////////////////////
selenium_server: {
// Selenium Server is running locally and is managed by Nightwatch
// Install the NPM package @nightwatch/selenium-server or download the selenium server jar file from https://github.com/SeleniumHQ/selenium/releases/, e.g.: selenium-server-4.1.1.jar
selenium: {
start_process: true,
port: 4444,
server_path: '', // Leave empty if @nightwatch/selenium-server is installed
command: 'standalone', // Selenium 4 only
cli_args: {
//'webdriver.gecko.driver': '',
//'webdriver.chrome.driver': ''
}
},
webdriver: {
start_process: false,
default_path_prefix: '/wd/hub'
}
},
'selenium.chrome': {
extends: 'selenium_server',
desiredCapabilities: {
browserName: 'chrome',
chromeOptions : {
w3c: true
}
}
},
'selenium.firefox': {
extends: 'selenium_server',
desiredCapabilities: {
browserName: 'firefox',
'moz:firefoxOptions': {
args: [
// '-headless',
// '-verbose'
]
}
}
}
}
};

@ -0,0 +1,39 @@
#!/bin/bash
# Path to the JSON file containing the DMG paths
JSON_FILE="dmgs.json"
# Read the DMGs array from the JSON file
DMG_PATHS=$(jq -r '.dmgs[]' "$JSON_FILE")
echo $DMG_PATHS
xcrun notarytool store-credentials "notarytool-password" \
--apple-id ${APPLE_ID} \
--team-id ${APPLE_TEAM_ID} \
--password ${APPLE_ID_PASSWORD} || exit 1
# Use jq to parse the DMGs array and read each line
while IFS= read -r DMG_PATH; do
# Remove single quotes from the path if present
DMG_PATH_CLEANED=$(echo $DMG_PATH | tr -d "'")
echo "Submitting $DMG_PATH_CLEANED for notarization..."
# Replace `your-app-specific-args` with the actual arguments for your app
# Ensure your notarytool command and arguments are correct for your application
xcrun notarytool submit "$DMG_PATH_CLEANED" --keychain-profile "notarytool-password" --wait
# Check the command's success
if [ $? -eq 0 ]; then
echo "Successfully submitted $DMG_PATH_CLEANED for notarization."
xcrun stapler staple "$DMG_PATH_CLEANED"
echo "Successfully stapled $DMG_PATH_CLEANED."
spctl -a -t open -vvv --context context:primary-signature "$DMG_PATH_CLEANED"
echo "Successfully checked $DMG_PATH_CLEANED."
else
echo "Failed to submit $DMG_PATH_CLEANED for notarization."
fi
done < <(jq -r '.dmgs[]' "$JSON_FILE")
echo "All DMG submissions completed."

@ -1,18 +1,21 @@
{ {
"name": "remixdesktop", "name": "remixdesktop",
"version": "0.0.11-Alpha", "version": "1.0.8-insiders",
"main": "build/main.js", "main": "build/main.js",
"license": "MIT", "license": "MIT",
"type": "commonjs", "type": "commonjs",
"description": "Remix IDE Desktop", "description": "Remix IDE Desktop",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/bunsenstraat/remix-desktop.git" "url": "git+https://github.com/remix-project-org/remix-desktop.git"
}, },
"author": { "author": {
"name": "Remix Team", "name": "Remix Team",
"email": "remix@ethereum.org" "email": "remix@ethereum.org"
}, },
"engines": {
"node": "20.2.0"
},
"bugs": { "bugs": {
"url": "https://github.com/ethereum/remix-project/issues" "url": "https://github.com/ethereum/remix-project/issues"
}, },
@ -22,100 +25,65 @@
"category": "public.app-category.productivity" "category": "public.app-category.productivity"
}, },
"scripts": { "scripts": {
"start:dev": "tsc && cp -R node_modules/yarn build/tools/ && cross-env NODE_ENV=development electron --inspect=5858 .", "start:dev": "yarn webpack --config webpack.config.js && electron --inspect=5858 .",
"start:production": "tsc && && cp -R node_modules/yarn build/tools/ && cross-env NODE_ENV=production electron .", "start:production": "cross-env NODE_ENV=production yarn webpack --config webpack.config.js && electron .",
"dist": "tsc && cp -R node_modules/yarn build/tools/ && electron-builder", "dist": "cross-env NODE_ENV=production yarn webpack --config webpack.config.js && electron-builder -p never",
"tscbuild": "tsc && cp -R node_modules/yarn build/tools/ && electron-builder -p never",
"esbuild": "cross-env NODE_ENV=production node esbuild.js && electron-builder -p never",
"installRipGrepMacOXx64": "rm -rf node_modules/@vscode/ripgrep/bin && npm_config_arch=x64 node node_modules/@vscode/ripgrep/lib/postinstall.js", "installRipGrepMacOXx64": "rm -rf node_modules/@vscode/ripgrep/bin && npm_config_arch=x64 node node_modules/@vscode/ripgrep/lib/postinstall.js",
"installRipGrepMacOXarm64": "rm -rf node_modules/@vscode/ripgrep/bin && npm_config_arch=arm64 node node_modules/@vscode/ripgrep/lib/postinstall.js", "installRipGrepMacOXarm64": "rm -rf node_modules/@vscode/ripgrep/bin && npm_config_arch=arm64 node node_modules/@vscode/ripgrep/lib/postinstall.js",
"postinstall": "electron-builder install-app-deps" "postinstall": "electron-builder install-app-deps",
"test": "yarn run build:e2e && nightwatch --config build-e2e/remixdesktop/test/nighwatch.app.js",
"test:isogit": "yarn run test --use-isogit",
"test:offline": "yarn run test --use-offline --test build-e2e/remixdesktop/test/tests/app/offline.test.js",
"build:e2e": "tsc -p tsconfig.e2e.json"
}, },
"devDependencies": { "devDependencies": {
"@electron/rebuild": "^3.2.13", "@electron/notarize": "^2.3.0",
"@types/byline": "^4.2.35", "@types/byline": "^4.2.35",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/nightwatch": "^2.3.23",
"chromedriver": "116",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"electron": "^25.0.1", "deep-equal": "^2.2.3",
"electron-builder": "^23.6.0", "electron": "^26.0.0",
"electron-builder": "24.9.1",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"esbuild": "^0.23.1",
"nightwatch": "2.3",
"node-loader": "^2.0.0",
"selenium-standalone": "^9.3.1",
"tree-kill": "^1.2.2",
"ts-loader": "^9.5.1",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"typescript": "^5.1.3", "typescript": "^5.1.3",
"webpack": "^5.92.1",
"webpack-cli": "^5.1.4",
"webpack-merge": "^6.0.1",
"webpack-node-externals": "^3.0.0",
"yarn": "^1.22.21" "yarn": "^1.22.21"
}, },
"dependencies": { "dependencies": {
"@remix-project/remix-url-resolver": "^0.0.65", "@remix-project/remix-url-resolver": "^0.0.65",
"@remixproject/engine": "0.3.41", "@remixproject/engine": "0.3.43",
"@remixproject/engine-electron": "0.3.41", "@remixproject/engine-electron": "0.3.43",
"@remixproject/plugin": "0.3.41", "@remixproject/plugin": "0.3.43",
"@remixproject/plugin-api": "^0.3.38", "@remixproject/plugin-api": "^0.3.43",
"@remixproject/plugin-electron": "0.3.41", "@remixproject/plugin-electron": "0.3.43",
"@vscode/ripgrep": "^1.15.6", "@vscode/ripgrep": "^1.15.6",
"add": "^2.0.6", "add": "^2.0.6",
"axios": "^1.7.4", "axios": "^1.7.4",
"byline": "^5.0.0", "byline": "^5.0.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"electron-updater": "^6.1.8",
"express": "^4.20.0", "express": "^4.20.0",
"isomorphic-git": "^1.24.2", "isomorphic-git": "^1.24.2",
"node-pty": "^0.10.1", "matomo-tracker": "^2.2.4",
"node-pty": "^1.0.0",
"octokit": "^3.1.2",
"semver": "^7.5.4" "semver": "^7.5.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"@remix-project/remix-ws-templates": "^1.0.27" "@remix-project/remix-ws-templates": "^1.0.27"
},
"build": {
"productName": "Remix IDE",
"appId": "org.ethereum.remix-ide",
"asar": true,
"generateUpdatesFilesForAllChannels": true,
"icon": "assets",
"files": [
"build/**/*"
],
"publish": [{
"provider": "github",
"owner": "bunsenstraat",
"repo": "remix-desktop",
"releaseType": "draft",
"publishAutoUpdate": true
}],
"mac": {
"category": "public.app-category.productivity",
"target": [
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "assets/icon.png",
"darkModeSupport": true
},
"dmg": {
"writeUpdateInfo": false
},
"nsis": {
"createDesktopShortcut": "always",
"allowToChangeInstallationDirectory": true,
"oneClick": false,
"shortcutName": "Remix IDE",
"differentialPackage": false
},
"win": {
"target": [
"nsis"
],
"icon": "assets/icon.png",
"artifactName": "${productName}.${ext}"
},
"linux": {
"target": [
"deb"
],
"category": "WebBrowser",
"icon": "assets"
},
"directories": {
"output": "release"
}
} }
} }

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
TEST_EXITCODE=0
yarn run build:e2e && node ./splice_tests.js
TESTFILES=$(node ./splice_tests.js | circleci tests split --split-by=timings)
for TESTFILE in $TESTFILES; do
yarn run test --test ./build-e2e/remixdesktop/test/tests/app/${TESTFILE} || yarn run test --test ./build-e2e/remixdesktop/test/tests/app/${TESTFILE} || TEST_EXITCODE=1
done
if [ "$CIRCLE_NODE_INDEX" -eq 1 ]; then
yarn test:offline || TEST_EXITCODE=1
fi
echo "$TEST_EXITCODE"
if [ "$TEST_EXITCODE" -eq 1 ]
then
exit 1
fi

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e
TEST_EXITCODE=0
yarn run build:e2e && node ./splice_tests.js
TESTFILES=$(node ./splice_tests.js | grep -i 'git' | circleci tests split --split-by=timings)
for TESTFILE in $TESTFILES; do
yarn run test --use-isogit --test ./build-e2e/remixdesktop/test/tests/app/${TESTFILE} || yarn run test --use-isogit --test ./build-e2e/remixdesktop/test/tests/app/${TESTFILE} || TEST_EXITCODE=1
done
echo "$TEST_EXITCODE"
if [ "$TEST_EXITCODE" -eq 1 ]
then
exit 1
fi

@ -0,0 +1,24 @@
#!/bin/bash
# Read the version from package.json
version=$(awk -F'"' '/"version":/ {print $4}' package.json)
# Determine the command to run based on the version
if [[ $version == *"beta"* ]]; then
command="yarn dist -c beta.json"
elif [[ $version == *"alpha"* ]]; then
command="yarn dist -c alpha.json"
elif [[ $version == *"insiders"* ]]; then
command="yarn dist -c insiders.json"
else
command="yarn dist -c latest.json"
fi
# Append any arguments passed in CLI
for arg in "$@"; do
command+=" $arg"
done
# Print and run the command
echo "Running command: $command"
$command

@ -0,0 +1,24 @@
#!/bin/bash
# Read the version from package.json
version=$(awk -F'"' '/"version":/ {print $4}' package.json)
# Determine the command to run based on the version
if [[ $version == *"beta"* ]]; then
command="yarn esbuild -c beta.json"
elif [[ $version == *"alpha"* ]]; then
command="yarn esbuild -c alpha.json"
elif [[ $version == *"insiders"* ]]; then
command="yarn esbuild -c insiders.json"
else
command="yarn esbuild -c latest.json"
fi
# Append any arguments passed in CLI
for arg in "$@"; do
command+=" $arg"
done
# Print and run the command
echo "Running command: $command"
$command

@ -0,0 +1,24 @@
#!/bin/bash
# Read the version from package.json
version=$(awk -F'"' '/"version":/ {print $4}' package.json)
# Determine the command to run based on the version
if [[ $version == *"beta"* ]]; then
command="yarn tscbuild -c beta.json"
elif [[ $version == *"alpha"* ]]; then
command="yarn tscbuild -c alpha.json"
elif [[ $version == *"insiders"* ]]; then
command="yarn tscbuild -c insiders.json"
else
command="yarn tscbuild -c latest.json"
fi
# Append any arguments passed in CLI
for arg in "$@"; do
command+=" $arg"
done
# Print and run the command
echo "Running command: $command"
$command

@ -0,0 +1,24 @@
#!/bin/bash
# Read the version from package.json
version=$(awk -F'"' '/"version":/ {print $4}' package.json)
# Determine the command to run based on the version
if [[ $version == *"beta"* ]]; then
command="yarn dist -c beta.json"
elif [[ $version == *"alpha"* ]]; then
command="yarn dist -c alpha.json"
elif [[ $version == *"insiders"* ]]; then
command="yarn dist -c insiders.json"
else
command="yarn dist -c latest.json"
fi
# Append any arguments passed in CLI
for arg in "$@"; do
command+=" $arg"
done
# Print and run the command
echo "Running command: $command"
$command

@ -0,0 +1,35 @@
const fs = require('fs');
const path = require('path');
// Directory to read files from
const testDirectory = './build-e2e/remixdesktop/test/tests/app/';
// Function to read files in a directory and return their paths
function getTestFiles(directory) {
return fs.readdirSync(directory)
.filter(file => file.endsWith('.test.js')) // Get only .test.js files
.map(file => path.join(directory, file)); // Return full path of each file
}
// Function to check if a file contains a specific word
function fileContainsWord(filePath, word) {
const content = fs.readFileSync(filePath, 'utf-8'); // Read file content
return content.includes(word); // Check if word is in content
}
// Function to filter out files that do not contain the specified word
function filterFilesByWord(files, word) {
return files.filter(file => fileContainsWord(file, word)); // Return files that do not contain the word
}
// Get all test files in the specified directory
const testFiles = getTestFiles(testDirectory);
// Filter out files that do not contain "@offline"
const filteredFiles = filterFilesByWord(testFiles, '');
// Output the list of filtered files
//console.log('Files without "@offline":', filteredFiles);
for (let i = 0; i < filteredFiles.length; i++) {
console.log(path.basename(filteredFiles[i]));
}

@ -9,6 +9,11 @@ import { ConfigPlugin } from './plugins/configPlugin';
import { TemplatesPlugin } from './plugins/templates'; import { TemplatesPlugin } from './plugins/templates';
import { RipgrepPlugin } from './plugins/ripgrepPlugin'; import { RipgrepPlugin } from './plugins/ripgrepPlugin';
import { CompilerLoaderPlugin } from './plugins/compilerLoader'; import { CompilerLoaderPlugin } from './plugins/compilerLoader';
import { SlitherPlugin } from './plugins/slitherPlugin';
import { AppUpdaterPlugin } from './plugins/appUpdater';
import { FoundryPlugin } from './plugins/foundryPlugin';
import { HardhatPlugin } from './plugins/hardhatPlugin';
import { isE2E } from './main';
const engine = new Engine() const engine = new Engine()
const appManager = new PluginManager() const appManager = new PluginManager()
@ -19,6 +24,10 @@ const configPlugin = new ConfigPlugin()
const templatesPlugin = new TemplatesPlugin() const templatesPlugin = new TemplatesPlugin()
const ripgrepPlugin = new RipgrepPlugin() const ripgrepPlugin = new RipgrepPlugin()
const compilerLoaderPlugin = new CompilerLoaderPlugin() const compilerLoaderPlugin = new CompilerLoaderPlugin()
const slitherPlugin = new SlitherPlugin()
const appUpdaterPlugin = new AppUpdaterPlugin()
const foundryPlugin = new FoundryPlugin()
const hardhatPlugin = new HardhatPlugin()
engine.register(appManager) engine.register(appManager)
engine.register(fsPlugin) engine.register(fsPlugin)
@ -28,6 +37,10 @@ engine.register(configPlugin)
engine.register(templatesPlugin) engine.register(templatesPlugin)
engine.register(ripgrepPlugin) engine.register(ripgrepPlugin)
engine.register(compilerLoaderPlugin) engine.register(compilerLoaderPlugin)
engine.register(slitherPlugin)
engine.register(foundryPlugin)
engine.register(appUpdaterPlugin)
engine.register(hardhatPlugin)
appManager.activatePlugin('electronconfig') appManager.activatePlugin('electronconfig')
appManager.activatePlugin('fs') appManager.activatePlugin('fs')
@ -40,6 +53,18 @@ ipcMain.on('fs:openFolder', async (event, path?) => {
fsPlugin.openFolder(event, path) fsPlugin.openFolder(event, path)
}) })
ipcMain.handle('fs:openFolder', async (event, webContentsId, path?) => {
if(!isE2E) return
console.log('openFolder', webContentsId, path)
fsPlugin.openFolder(webContentsId, path)
})
ipcMain.handle('fs:openFolderInSameWindow', async (event, webContentsId, path?) => {
if(!isE2E) return
console.log('openFolderInSameWindow', webContentsId, path)
fsPlugin.openFolderInSameWindow(webContentsId, path)
})
ipcMain.on('terminal:new', async (event) => { ipcMain.on('terminal:new', async (event) => {
xtermPlugin.new(event) xtermPlugin.new(event)
@ -53,12 +78,6 @@ ipcMain.on('git:startclone', async (event) => {
isoGitPlugin.startClone(event) isoGitPlugin.startClone(event)
}) })
ipcMain.on('terminal:new', async (event) => {
console.log('new terminal')
xtermPlugin.new(event)
})
ipcMain.handle('getWebContentsID', (event, message) => { ipcMain.handle('getWebContentsID', (event, message) => {
return event.sender.id return event.sender.id
}) })

@ -0,0 +1,37 @@
import { ElectronBasePluginClient } from "@remixproject/plugin-electron";
import { Profile } from "@remixproject/plugin-utils";
export class ElectronBasePluginRemixdClient extends ElectronBasePluginClient {
log: (...message: any) => void
error: (...message: any) => void
currentSharedFolder: string = ''
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile);
this.log = (...message: any) => {
for(const m of message) {
this.call('terminal', 'log', {
type: 'log',
value: m
})
}
}
this.error = (...message: any) => {
for(const m of message) {
this.call('terminal', 'log', {
type: 'error',
value: m
})
}
}
this.onload(async () => {
this.on('fs' as any, 'workingDirChanged', async (path: string) => {
console.log('workingDirChanged base remixd', path)
this.currentSharedFolder = path
})
this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir')
})
}
}

@ -0,0 +1,24 @@
import * as pathModule from 'path'
/**
* returns the absolute path of the given @arg path
*
* @param {String} path - relative path (Unix style which is the one used by Remix IDE)
* @param {String} sharedFolder - absolute shared path. platform dependent representation.
* @return {String} platform dependent absolute path (/home/user1/.../... for unix, c:\user\...\... for windows)
*/
function absolutePath (path: string, sharedFolder:string): string {
path = normalizePath(path)
path = pathModule.resolve(sharedFolder, path)
return path
}
function normalizePath (path) {
if (path === '/') path = './'
if (process.platform === 'win32') {
return path.replace(/\//g, '\\')
}
return path
}
export { absolutePath, normalizePath }

@ -1,10 +1,24 @@
import { app, BrowserWindow, dialog, Menu, MenuItem, shell, utilityProcess } from 'electron'; import { app, BrowserWindow, dialog, Menu, MenuItem, shell, utilityProcess, screen, ipcMain } from 'electron';
import path from 'path'; import path from 'path';
export let isPackaged = false; export let isPackaged = false;
export const version = app.getVersion(); export const version = app.getVersion();
const args = process.argv.slice(1)
console.log("args", args)
export const isE2ELocal = args.find(arg => arg.startsWith('--e2e-local'))
export const isE2E = args.find(arg => arg.startsWith('--e2e'))
if (isE2ELocal) {
console.log('e2e mode')
}
const cache_dir_arg = args.find(arg => arg.startsWith('--cache_dir='))
export let cache_dir = ''
if (cache_dir_arg) {
cache_dir = cache_dir_arg.split('=')[1]
}
if ( if (
process.mainModule && process.mainModule &&
process.mainModule.filename.indexOf('app.asar') !== -1 process.mainModule.filename.indexOf('app.asar') !== -1
@ -17,14 +31,17 @@ if (
// get system home dir // get system home dir
const homeDir = app.getPath('userData') const homeDir = app.getPath('userData')
const windowSet = new Set<BrowserWindow>([]); const windowSet = new Set<BrowserWindow>([]);
export const createWindow = async (dir?: string): Promise<void> => { export const createWindow = async (dir?: string): Promise<void> => {
// Create the browser window. // Create the browser window.
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
height: 800, width: (isE2E ? 2560 : screen.getPrimaryDisplay().size.width * 0.8),
width: 1024, height: (isE2E ? 1140 : screen.getPrimaryDisplay().size.height * 0.8),
frame: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, 'preload.js') preload: path.join(__dirname, 'preload.js')
}, },
}); });
mainWindow.webContents.setWindowOpenHandler((details) => { mainWindow.webContents.setWindowOpenHandler((details) => {
@ -35,10 +52,10 @@ export const createWindow = async (dir?: string): Promise<void> => {
const params = dir ? `?opendir=${encodeURIComponent(dir)}` : ''; const params = dir ? `?opendir=${encodeURIComponent(dir)}` : '';
// and load the index.html of the app. // and load the index.html of the app.
mainWindow.loadURL( mainWindow.loadURL(
process.env.NODE_ENV === 'production' || isPackaged ? `file://${__dirname}/remix-ide/index.html` + params : (process.env.NODE_ENV === 'production' || isPackaged) && !isE2ELocal ? `file://${__dirname}/remix-ide/index.html` + params :
'http://localhost:8080' + params) 'http://localhost:8080' + params)
mainWindow.maximize(); trackEvent('Instance', 'create_window', '', 1);
if (dir) { if (dir) {
mainWindow.setTitle(dir) mainWindow.setTitle(dir)
@ -49,6 +66,9 @@ export const createWindow = async (dir?: string): Promise<void> => {
windowSet.delete(mainWindow) windowSet.delete(mainWindow)
}) })
if (isE2E)
mainWindow.maximize()
windowSet.add(mainWindow) windowSet.add(mainWindow)
//mainWindow.webContents.openDevTools(); //mainWindow.webContents.openDevTools();
}; };
@ -57,6 +77,8 @@ export const createWindow = async (dir?: string): Promise<void> => {
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
app.on('ready', async () => { app.on('ready', async () => {
trackEvent('App', 'Launch', app.getVersion(), 1, 1);
trackEvent('App', 'OS', process.platform, 1);
require('./engine') require('./engine')
}); });
@ -102,6 +124,8 @@ import ViewMenu from './menus/view';
import TerminalMenu from './menus/terminal'; import TerminalMenu from './menus/terminal';
import HelpMenu from './menus/help'; import HelpMenu from './menus/help';
import { execCommand } from './menus/commands'; import { execCommand } from './menus/commands';
import main from './menus/main';
import { trackEvent } from './utils/matamo';
const commandKeys: Record<string, string> = { const commandKeys: Record<string, string> = {
@ -110,16 +134,39 @@ const commandKeys: Record<string, string> = {
}; };
const menu = [...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []), const menu = [...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []),
FileMenu(commandKeys, execCommand), FileMenu(commandKeys, execCommand),
GitMenu(commandKeys, execCommand), GitMenu(commandKeys, execCommand),
EditMenu(commandKeys, execCommand), EditMenu(commandKeys, execCommand),
ViewMenu(commandKeys, execCommand), ViewMenu(commandKeys, execCommand),
TerminalMenu(commandKeys, execCommand), TerminalMenu(commandKeys, execCommand),
WindowMenu(commandKeys, execCommand, []), WindowMenu(commandKeys, execCommand, []),
HelpMenu(commandKeys, execCommand), HelpMenu(commandKeys, execCommand),
] ]
if (!isE2E || isE2ELocal)
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))
ipcMain.handle('logger', async (...args) => {
console.log('log:', ...args)
})
ipcMain.handle('config:isPackaged', async () => {
return isPackaged
})
Menu.setApplicationMenu(Menu.buildFromTemplate(menu)) ipcMain.handle('config:isE2E', async () => {
return isE2E
})
ipcMain.handle('config:canTrackMatomo', async (event, name: string) => {
console.log('config:canTrackMatomo', ((process.env.NODE_ENV === 'production' || isPackaged) && !isE2E))
return ((process.env.NODE_ENV === 'production' || isPackaged) && !isE2E)
})
ipcMain.handle('matomo:trackEvent', async (event, data) => {
if (data && data[0] && data[0] === 'trackEvent') {
trackEvent(data[1], data[2], data[3], data[4])
}
})

@ -1,22 +1,4 @@
import { BrowserWindow, MenuItemConstructorOptions, app, ipcMain } from 'electron'; import { BrowserWindow, MenuItemConstructorOptions, app, ipcMain } from 'electron';
import fs from 'fs'
import os from 'os'
import path from 'path'
import { cacheDir } from '../utils/config';
let recentFolders: string[] = []
if (fs.existsSync(cacheDir + '/remixdesktop.json')) {
try {
// read the cache file
const cache = fs.readFileSync(cacheDir + '/remixdesktop.json')
const data = JSON.parse(cache.toString())
recentFolders = data && data.recentFolders || []
console.log('recentFolders', recentFolders)
} catch (e) {
}
}
export default ( export default (
commandKeys: Record<string, string>, commandKeys: Record<string, string>,
@ -47,19 +29,6 @@ export default (
execCommand('template:open', focusedWindow); execCommand('template:open', focusedWindow);
} }
}, },
{
role: 'recentDocuments',
submenu: recentFolders.map((folder) => {
return {
label: folder,
click(item, focusedWindow) {
if(focusedWindow) {
ipcMain.emit('fs:openFolder', focusedWindow.webContents.id, folder);
}
}
}
})
},
{ {
role: 'close', role: 'close',
accelerator: commandKeys['window:close'] accelerator: commandKeys['window:close']

@ -0,0 +1,122 @@
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import { Profile } from "@remixproject/plugin-utils"
import { autoUpdater } from "electron-updater"
import { app } from 'electron';
import { isE2E } from "../main";
import { trackEvent } from "../utils/matamo";
const profile = {
displayName: 'appUpdater',
name: 'appUpdater',
description: 'appUpdater',
}
export class AppUpdaterPlugin extends ElectronBasePlugin {
clients: AppUpdaterPluginClient[] = []
constructor() {
super(profile, clientProfile, AppUpdaterPluginClient)
this.methods = [...super.methods]
autoUpdater.autoDownload = false
autoUpdater.disableDifferentialDownload = true
autoUpdater.on('checking-for-update', () => {
console.log('Checking for update...');
this.sendToLog('Checking for update...')
})
autoUpdater.on('update-available', (info: any) => {
console.log('Update available.', info);
this.sendToLog('Update available.')
for (const client of this.clients) {
client.askForUpdate()
}
})
autoUpdater.on('update-not-available', () => {
console.log('Update not available.');
this.sendToLog('App is already up to date.')
})
autoUpdater.on('error', (err) => {
console.log('Error in auto-updater. ' + err);
this.sendToLog('Cannot find updates...')
})
autoUpdater.on('download-progress', (progressObj) => {
let log_message = "Download speed: " + progressObj.bytesPerSecond;
log_message = log_message + ' - Downloaded ' + progressObj.percent + '%';
log_message = log_message + ' (' + progressObj.transferred + "/" + progressObj.total + ')';
console.log(log_message);
this.sendToLog(log_message)
})
autoUpdater.on('update-downloaded', (info) => {
console.log('Update downloaded');
this.sendToLog('Update downloaded')
this.sendToLog('processing download... please wait...')
for(const client of this.clients) {
client.downloadReady()
}
})
}
async sendToLog(message: string): Promise<void> {
for (const client of this.clients) {
client.call('terminal', 'log', {
type: 'log',
value: message,
})
}
}
}
const clientProfile: Profile = {
name: 'appUpdater',
displayName: 'appUpdater',
description: 'appUpdater',
methods: ['checkForUpdates', 'download', 'install'],
}
class AppUpdaterPluginClient extends ElectronBasePluginClient {
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
}
async onActivation(): Promise<void> {
this.onload(async () => {
this.emit('loaded')
if(isE2E) return
await this.checkForUpdates()
})
}
async askForUpdate(): Promise<void> {
this.emit('askForUpdate')
}
async downloadReady(): Promise<void> {
// we do a wait here to make sure that the download is done, it's a bug in electron-updater
setTimeout(() => {
this.emit('downloadReady')
}
, 10000)
}
async download(): Promise<void> {
autoUpdater.downloadUpdate()
}
async install(): Promise<void> {
autoUpdater.quitAndInstall()
}
async checkForUpdates(): Promise<void> {
console.log('checkForUpdates')
this.call('terminal', 'log', {
type: 'log',
value: 'Remix Desktop version: ' + autoUpdater.currentVersion,
})
trackEvent('App', 'CheckForUpdate', 'Remix Desktop version: ' + autoUpdater.currentVersion, 1);
autoUpdater.checkForUpdates()
}
}

@ -1,16 +1,19 @@
import {Profile} from '@remixproject/plugin-utils' import { Profile } from '@remixproject/plugin-utils'
import {ElectronBasePlugin, ElectronBasePluginClient} from '@remixproject/plugin-electron' import { ElectronBasePlugin, ElectronBasePluginClient } from '@remixproject/plugin-electron'
import fs from 'fs/promises' import fs from 'fs/promises'
import axios from 'axios' import axios from 'axios'
import express from 'express' import express from 'express'
import {cacheDir} from '../utils/config' import { cacheDir } from '../utils/config'
export const baseURLBin = 'https://binaries.soliditylang.org/bin' export const baseURLBin = 'https://binaries.soliditylang.org/bin'
export const baseURLWasm = 'https://binaries.soliditylang.org/wasm' export const baseURLWasm = 'https://binaries.soliditylang.org/wasm'
const appExpress = express() const appExpress = express()
// used in e2e tests
const useOffline = process.argv.includes('--use-offline');
console.log('cacheDir', cacheDir) console.log('cacheDir', cacheDir)
appExpress.use(express.static(cacheDir)) appExpress.use(express.static(cacheDir))
const server = appExpress.listen(0, () => { const server = appExpress.listen(0, () => {
@ -28,9 +31,9 @@ export class CompilerLoaderPlugin extends ElectronBasePlugin {
constructor() { constructor() {
super(profile, clientProfile, CompilerLoaderPluginClient) super(profile, clientProfile, CompilerLoaderPluginClient)
this.methods = [...super.methods] this.methods = [...super.methods]
;(async () => { ; (async () => {
await getLists() await getLists()
})() })()
} }
@ -66,16 +69,14 @@ class CompilerLoaderPluginClient extends ElectronBasePluginClient {
} }
async onActivation(): Promise<void> { async onActivation(): Promise<void> {
console.log('onActivation', 'CompilerLoaderPluginClient')
this.onload(() => { this.onload(() => {
console.log('onload', 'CompilerLoaderPluginClient')
this.emit('loaded') this.emit('loaded')
}) })
} }
async downloadCompiler(url: string): Promise<void> { async downloadCompiler(url: string): Promise<void> {
console.log('downloadCompiler', url) console.log('downloadCompiler', url)
if(url.includes('localhost')) return if (url.includes('localhost')) return
const plugin = this const plugin = this
try { try {
const fileName = url.split('/').pop() const fileName = url.split('/').pop()
@ -129,7 +130,7 @@ class CompilerLoaderPluginClient extends ElectronBasePluginClient {
async getJsonBinData() { async getJsonBinData() {
const lists = await this.getLists() const lists = await this.getLists()
this.solJsonBinData = { this.solJsonBinData = {
baseURLWasm: 'http://localhost:' + (server.address() as any).port + '/compilers', baseURLWasm: 'http://localhost:' + (server.address() as any).port + '/compilers',
baseURLBin: 'http://localhost:' + (server.address() as any).port + '/compilers', baseURLBin: 'http://localhost:' + (server.address() as any).port + '/compilers',
@ -139,11 +140,11 @@ class CompilerLoaderPluginClient extends ElectronBasePluginClient {
const localCompilers = await this.listCompilers() const localCompilers = await this.listCompilers()
this.solJsonBinData.wasmList && (this.solJsonBinData.wasmList = this.solJsonBinData.wasmList.map((item) => { this.solJsonBinData.wasmList && (this.solJsonBinData.wasmList = this.solJsonBinData.wasmList.map((item) => {
localCompilers.includes(item.path) ? (item.wasmURL = 'http://localhost:' + (server.address() as any).port + '/compilers/') && (item.isDownloaded=true) : (item.wasmURL = baseURLWasm) && (item.isDownloaded = false) localCompilers.includes(item.path) ? (item.wasmURL = 'http://localhost:' + (server.address() as any).port + '/compilers/') && (item.isDownloaded = true) : (item.wasmURL = baseURLWasm) && (item.isDownloaded = false)
return item return item
})) }))
this.solJsonBinData.binList && (this.solJsonBinData.binList = this.solJsonBinData.binList.map((item) => { this.solJsonBinData.binList && (this.solJsonBinData.binList = this.solJsonBinData.binList.map((item) => {
localCompilers.includes(item.path) ? (item.binURL = 'http://localhost:' + (server.address() as any).port + '/compilers/') && (item.isDownloaded=true) : (item.binURL = baseURLBin) && (item.isDownloaded = false) localCompilers.includes(item.path) ? (item.binURL = 'http://localhost:' + (server.address() as any).port + '/compilers/') && (item.isDownloaded = true) : (item.binURL = baseURLBin) && (item.isDownloaded = false)
return item return item
})) }))
this.emit('jsonBinDataLoaded', this.solJsonBinData) this.emit('jsonBinDataLoaded', this.solJsonBinData)
@ -158,17 +159,19 @@ const getLists = async () => {
let binData let binData
let wasmData let wasmData
try { if (!useOffline) {
const binRes = await axios.get(baseURLBin + '/list.json') try {
await fs.writeFile(cacheDir + '/binlist.json', JSON.stringify(binRes.data, null, 2)) const binRes = await axios.get(baseURLBin + '/list.json')
binData = binRes.data await fs.writeFile(cacheDir + '/binlist.json', JSON.stringify(binRes.data, null, 2))
} catch (e) {} binData = binRes.data
} catch (e) { }
try {
const wasmRes = await axios.get(baseURLWasm + '/list.json') try {
await fs.writeFile(cacheDir + '/wasmlist.json', JSON.stringify(wasmRes.data, null, 2)) const wasmRes = await axios.get(baseURLWasm + '/list.json')
wasmData = wasmRes.data await fs.writeFile(cacheDir + '/wasmlist.json', JSON.stringify(wasmRes.data, null, 2))
} catch (e) {} wasmData = wasmRes.data
} catch (e) { }
}
if (!wasmData) { if (!wasmData) {
try { try {

@ -0,0 +1,248 @@
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import chokidar from 'chokidar'
import { ElectronBasePluginRemixdClient } from "../lib/remixd"
import fs from 'fs'
import * as utils from '../lib/utils'
import { basename, join } from "path";
import { spawn } from "child_process";
const profile: Profile = {
name: 'foundry',
displayName: 'electron foundry',
description: 'electron foundry',
}
export class FoundryPlugin extends ElectronBasePlugin {
clients: any[]
constructor() {
super(profile, clientProfile, FoundryPluginClient)
this.methods = [...super.methods]
}
}
const clientProfile: Profile = {
name: 'foundry',
displayName: 'electron foundry',
description: 'electron foundry',
methods: ['sync', 'compile']
}
class FoundryPluginClient extends ElectronBasePluginRemixdClient {
watcher: chokidar.FSWatcher
warnlog: boolean
buildPath: string
cachePath: string
logTimeout: NodeJS.Timeout
processingTimeout: NodeJS.Timeout
async onActivation(): Promise<void> {
console.log('Foundry plugin activated')
this.call('terminal', 'log', { type: 'log', value: 'Foundry plugin activated' })
this.on('fs' as any, 'workingDirChanged', async (path: string) => {
console.log('workingDirChanged foundry', path)
this.currentSharedFolder = path
this.startListening()
})
this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir')
if(this.currentSharedFolder) this.startListening()
}
startListening() {
this.buildPath = utils.absolutePath('out', this.currentSharedFolder)
this.cachePath = utils.absolutePath('cache', this.currentSharedFolder)
console.log('Foundry plugin checking for', this.buildPath, this.cachePath)
if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) {
this.listenOnFoundryCompilation()
} else {
this.listenOnFoundryFolder()
}
}
listenOnFoundryFolder() {
console.log('Foundry out folder doesn\'t exist... waiting for the compilation.')
try {
if (this.watcher) this.watcher.close()
this.watcher = chokidar.watch(this.currentSharedFolder, { depth: 1, ignorePermissionErrors: true, ignoreInitial: true })
// watch for new folders
this.watcher.on('addDir', (path: string) => {
console.log('add dir foundry', path)
if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) {
this.listenOnFoundryCompilation()
}
})
} catch (e) {
console.log(e)
}
}
compile() {
return new Promise((resolve, reject) => {
const cmd = `forge build`
const options = { cwd: this.currentSharedFolder, shell: true }
const child = spawn(cmd, options)
let result = ''
let error = ''
child.stdout.on('data', (data) => {
const msg = `[Foundry Compilation]: ${data.toString()}`
console.log('\x1b[32m%s\x1b[0m', msg)
result += msg + '\n'
})
child.stderr.on('data', (err) => {
error += `[Foundry Compilation]: ${err.toString()} \n`
})
child.on('close', () => {
if (error && result) resolve(error + result)
else if (error) reject(error)
else resolve(result)
})
})
}
checkPath() {
if (!fs.existsSync(this.buildPath) || !fs.existsSync(this.cachePath)) {
this.listenOnFoundryFolder()
return false
}
if (!fs.existsSync(join(this.cachePath, 'solidity-files-cache.json'))) return false
return true
}
private async processArtifact() {
if (!this.checkPath()) return
const folderFiles = await fs.promises.readdir(this.buildPath) // "out" folder
try {
const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' }))
// name of folders are file names
for (const file of folderFiles) {
const path = join(this.buildPath, file) // out/Counter.sol/
const compilationResult = {
input: {},
output: {
contracts: {},
sources: {}
},
inputSources: { sources: {}, target: '' },
solcVersion: null,
compilationTarget: null
}
compilationResult.inputSources.target = file
await this.readContract(path, compilationResult, cache)
this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion)
}
clearTimeout(this.logTimeout)
this.logTimeout = setTimeout(() => {
// @ts-ignore
this.call('terminal', 'log', { type: 'log', value: `receiving compilation result from Foundry. Select a file to populate the contract interaction interface.` })
console.log('Syncing compilation result from Foundry')
}, 1000)
} catch (e) {
console.log(e)
}
}
async triggerProcessArtifact() {
// prevent multiple calls
clearTimeout(this.processingTimeout)
this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000)
}
listenOnFoundryCompilation() {
try {
console.log('Foundry out folder exists... processing the artifact.')
if (this.watcher) this.watcher.close()
this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true })
this.watcher.on('change', async () => await this.triggerProcessArtifact())
this.watcher.on('add', async () => await this.triggerProcessArtifact())
this.watcher.on('unlink', async () => await this.triggerProcessArtifact())
// process the artifact on activation
this.triggerProcessArtifact()
} catch (e) {
console.log(e)
}
}
async readContract(contractFolder, compilationResultPart, cache) {
const files = await fs.promises.readdir(contractFolder)
for (const file of files) {
const path = join(contractFolder, file)
const content = await fs.promises.readFile(path, { encoding: 'utf-8' })
compilationResultPart.inputSources.sources[file] = { content }
await this.feedContractArtifactFile(file, content, compilationResultPart, cache)
}
}
async feedContractArtifactFile(path, content, compilationResultPart, cache) {
const contentJSON = JSON.parse(content)
const contractName = basename(path).replace('.json', '')
let sourcePath = ''
if (contentJSON?.metadata?.settings?.compilationTarget) {
for (const key in contentJSON.metadata.settings.compilationTarget) {
if (contentJSON.metadata.settings.compilationTarget[key] === contractName) {
sourcePath = key
break
}
}
}
if (!sourcePath) return
const currentCache = cache.files[sourcePath]
if (!currentCache.artifacts[contractName]) return
// extract source and version
const metadata = contentJSON.metadata
if (metadata.compiler && metadata.compiler.version) {
compilationResultPart.solcVersion = metadata.compiler.version
} else {
compilationResultPart.solcVersion = ''
console.log('\x1b[32m%s\x1b[0m', 'compiler version not found, please update Foundry to the latest version.')
}
if (metadata.sources) {
for (const path in metadata.sources) {
const absPath = utils.absolutePath(path, this.currentSharedFolder)
try {
const content = await fs.promises.readFile(absPath, { encoding: 'utf-8' })
compilationResultPart.input[path] = { content }
} catch (e) {
compilationResultPart.input[path] = { content: '' }
}
}
} else {
console.log('\x1b[32m%s\x1b[0m', 'sources input not found, please update Foundry to the latest version.')
}
compilationResultPart.compilationTarget = sourcePath
// extract data
if (!compilationResultPart.output['sources'][sourcePath]) compilationResultPart.output['sources'][sourcePath] = {}
compilationResultPart.output['sources'][sourcePath] = {
ast: contentJSON['ast'],
id: contentJSON['id']
}
if (!compilationResultPart.output['contracts'][sourcePath]) compilationResultPart.output['contracts'][sourcePath] = {}
contentJSON.bytecode.object = contentJSON.bytecode.object.replace('0x', '')
contentJSON.deployedBytecode.object = contentJSON.deployedBytecode.object.replace('0x', '')
compilationResultPart.output['contracts'][sourcePath][contractName] = {
abi: contentJSON.abi,
evm: {
bytecode: contentJSON.bytecode,
deployedBytecode: contentJSON.deployedBytecode,
methodIdentifiers: contentJSON.methodIdentifiers
}
}
}
async sync() {
console.log('syncing Foundry with Remix...')
this.processArtifact()
}
}

@ -3,13 +3,16 @@ import fs from 'fs/promises'
import {Profile} from '@remixproject/plugin-utils' import {Profile} from '@remixproject/plugin-utils'
import chokidar from 'chokidar' import chokidar from 'chokidar'
import {dialog, shell} from 'electron' import {dialog, shell} from 'electron'
import {createWindow, isPackaged} from '../main' import {createWindow, isE2E, isPackaged} from '../main'
import {writeConfig} from '../utils/config' import {writeConfig} from '../utils/config'
import path from 'path' import path from 'path'
import {customAction} from '@remixproject/plugin-api' import {customAction} from '@remixproject/plugin-api'
import { PluginEventDataBatcher } from '../utils/pluginEventDataBatcher' import { PluginEventDataBatcher } from '../utils/pluginEventDataBatcher'
type recentFolder = {
timestamp: number,
path: string
}
const profile: Profile = { const profile: Profile = {
displayName: 'fs', displayName: 'fs',
@ -29,36 +32,44 @@ const getBaseName = (pathName: string): string => {
return path.basename(pathName) return path.basename(pathName)
} }
function onlyUnique(value: recentFolder, index: number, self: recentFolder[]) {
return self.findIndex((rc, index) => rc.path === value.path) === index
}
const deplucateFolderList = (list: recentFolder[]): recentFolder[] => {
return list.filter(onlyUnique)
}
export class FSPlugin extends ElectronBasePlugin { export class FSPlugin extends ElectronBasePlugin {
clients: FSPluginClient[] = [] clients: FSPluginClient[] = []
constructor() { constructor() {
super(profile, clientProfile, FSPluginClient) super(profile, clientProfile, isE2E? FSPluginClientE2E: FSPluginClient)
this.methods = [...super.methods, 'closeWatch', 'removeCloseListener'] this.methods = [...super.methods, 'closeWatch', 'removeCloseListener']
} }
async onActivation(): Promise<void> { async onActivation(): Promise<void> {
const config = await this.call('electronconfig' as any, 'readConfig') const config = await this.call('electronconfig', 'readConfig')
const openedFolders = (config && config.openedFolders) || [] const openedFolders = (config && config.openedFolders) || []
const recentFolders = (config && config.recentFolders) || [] const recentFolders: recentFolder[] = (config && config.recentFolders) || []
this.call('electronconfig', 'writeConfig', {...config, this.call('electronconfig', 'writeConfig', {...config,
recentFolders: recentFolders, recentFolders: deplucateFolderList(recentFolders),
openedFolders: openedFolders}) openedFolders: openedFolders})
const foldersToDelete: string[] = [] const foldersToDelete: string[] = []
if (openedFolders && openedFolders.length) { if (recentFolders && recentFolders.length) {
for (const folder of openedFolders) { for (const folder of recentFolders) {
try { try {
const stat = await fs.stat(folder) const stat = await fs.stat(folder.path);
if (stat.isDirectory()) { if (stat.isDirectory()) {
// do nothing // do nothing
} }
} catch (e) { } catch (e) {
console.log('error opening folder', folder, e) console.log('error opening folder', folder, e)
foldersToDelete.push(folder) foldersToDelete.push(folder.path)
} }
} }
if (foldersToDelete.length) { if (foldersToDelete.length) {
const newFolders = openedFolders.filter((f: string) => !foldersToDelete.includes(f)) const newFolders = recentFolders.filter((f: recentFolder) => !foldersToDelete.includes(f.path))
this.call('electronconfig', 'writeConfig', {recentFolders: newFolders}) this.call('electronconfig', 'writeConfig', {recentFolders: deplucateFolderList(newFolders)})
} }
} }
createWindow() createWindow()
@ -82,6 +93,13 @@ export class FSPlugin extends ElectronBasePlugin {
client.openFolder(path) client.openFolder(path)
} }
} }
openFolderInSameWindow(webContentsId: any, path?: string): void {
const client = this.clients.find((c) => c.webContentsId === webContentsId)
if (client) {
client.openFolderInSameWindow(path)
}
}
} }
const clientProfile: Profile = { const clientProfile: Profile = {
@ -200,6 +218,7 @@ class FSPluginClient extends ElectronBasePluginClient {
} }
async exists(path: string): Promise<boolean> { async exists(path: string): Promise<boolean> {
if (this.workingDir === '') return false
return fs return fs
.access(this.fixPath(path)) .access(this.fixPath(path))
.then(() => true) .then(() => true)
@ -257,7 +276,7 @@ class FSPluginClient extends ElectronBasePluginClient {
depth: 0, depth: 0,
}) })
.on('all', async (eventName, path, stats) => { .on('all', async (eventName, path, stats) => {
this.watcherExec(eventName, path) this.watcherExec(eventName, convertPathToPosix(path))
}) })
.on('error', (error) => { .on('error', (error) => {
watcher.close() watcher.close()
@ -294,7 +313,6 @@ class FSPluginClient extends ElectronBasePluginClient {
} else { } else {
try { try {
const dirname = path.dirname(pathWithoutPrefix) const dirname = path.dirname(pathWithoutPrefix)
//console.log('check emitting', eventName, pathWithoutPrefix, this.expandedPaths, dirname)
if (this.expandedPaths.includes(dirname) || this.expandedPaths.includes(pathWithoutPrefix)) { if (this.expandedPaths.includes(dirname) || this.expandedPaths.includes(pathWithoutPrefix)) {
//console.log('emitting', eventName, pathWithoutPrefix, this.expandedPaths) //console.log('emitting', eventName, pathWithoutPrefix, this.expandedPaths)
//this.emit('change', eventName, pathWithoutPrefix) //this.emit('change', eventName, pathWithoutPrefix)
@ -312,11 +330,38 @@ class FSPluginClient extends ElectronBasePluginClient {
} }
} }
async convertRecentFolders(): Promise<void> {
const config = await this.call('electronconfig' as any, 'readConfig')
if(config.recentFolders) {
const remaps = config.recentFolders.map((f: any) => {
// if type is string
if(typeof f ==='string') {
return {
path: f,
timestamp: new Date().getTime(),
}
}else{
return f
}
})
config.recentFolders = remaps
await writeConfig(config)
}
}
async updateRecentFolders(path: string): Promise<void> { async updateRecentFolders(path: string): Promise<void> {
await this.convertRecentFolders()
const config = await this.call('electronconfig' as any, 'readConfig') const config = await this.call('electronconfig' as any, 'readConfig')
config.recentFolders = config.recentFolders || [] config.recentFolders = config.recentFolders || []
config.recentFolders = config.recentFolders.filter((p: string) => p !== path) config.recentFolders = config.recentFolders.filter((p: string) => p !== path)
config.recentFolders.push(path) const timestamp = new Date().getTime()
config.recentFolders.push({
path,
timestamp,
})
config.recentFolders = deplucateFolderList(config.recentFolders)
writeConfig(config) writeConfig(config)
} }
@ -336,16 +381,18 @@ class FSPluginClient extends ElectronBasePluginClient {
} }
async getRecentFolders(): Promise<string[]> { async getRecentFolders(): Promise<string[]> {
await this.convertRecentFolders()
const config = await this.call('electronconfig' as any, 'readConfig') const config = await this.call('electronconfig' as any, 'readConfig')
let folders: string[] = config.recentFolders || [] let folders: string[] = []
folders = folders.map((f: string) => convertPathToPosix(f)) folders = (config.recentFolders || []).map((f: recentFolder) => convertPathToPosix(f.path)).sort((a: recentFolder, b: recentFolder) => a.timestamp - b.timestamp).slice(-15).reverse()
return folders return folders
} }
async removeRecentFolder(path: string): Promise<void> { async removeRecentFolder(path: string): Promise<void> {
await this.convertRecentFolders()
const config = await this.call('electronconfig' as any, 'readConfig') const config = await this.call('electronconfig' as any, 'readConfig')
config.recentFolders = config.recentFolders || [] config.recentFolders = config.recentFolders || []
config.recentFolders = config.recentFolders.filter((p: string) => p !== path) config.recentFolders = config.recentFolders.filter((p: recentFolder) => p.path !== path)
writeConfig(config) writeConfig(config)
} }
@ -389,7 +436,7 @@ class FSPluginClient extends ElectronBasePluginClient {
} }
path = dirs && dirs.length && dirs[0] ? dirs[0] : path path = dirs && dirs.length && dirs[0] ? dirs[0] : path
if (!path) return if (!path) return
this.workingDir = path this.workingDir = convertPathToPosix(path)
await this.updateRecentFolders(path) await this.updateRecentFolders(path)
await this.updateOpenedFolders(path) await this.updateOpenedFolders(path)
this.window.setTitle(this.workingDir) this.window.setTitle(this.workingDir)
@ -398,13 +445,14 @@ class FSPluginClient extends ElectronBasePluginClient {
} }
async setWorkingDir(path: string): Promise<void> { async setWorkingDir(path: string): Promise<void> {
this.workingDir = path console.log('setWorkingDir', path)
this.workingDir = convertPathToPosix(path)
await this.updateRecentFolders(path) await this.updateRecentFolders(path)
await this.updateOpenedFolders(path) await this.updateOpenedFolders(path)
this.window.setTitle(getBaseName(this.workingDir)) this.window.setTitle(getBaseName(this.workingDir))
this.watch() this.watch()
this.emit('workingDirChanged', path) this.emit('workingDirChanged', path)
await this.call('fileManager', 'closeAllFiles') return
} }
async revealInExplorer(action: customAction, isAbsolutePath: boolean = false): Promise<void> { async revealInExplorer(action: customAction, isAbsolutePath: boolean = false): Promise<void> {
@ -433,3 +481,43 @@ class FSPluginClient extends ElectronBasePluginClient {
createWindow(path) createWindow(path)
} }
} }
import os from 'os'
export class FSPluginClientE2E extends FSPluginClient {
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile)
}
async selectFolder(dir?: string, title?: string, button?: string): Promise<string> {
if (!dir) {
// create random directory on os homedir
const randomdir = path.join(os.homedir(), 'remix-tests' + Date.now().toString())
await fs.mkdir(randomdir)
return randomdir
}
if (!dir) return ''
return dir
}
async openFolder(dir?: string): Promise<void> {
dir = await this.selectFolder(dir)
await this.updateRecentFolders(dir)
await this.updateOpenedFolders(dir)
if (!dir) return
this.openWindow(dir)
}
async openFolderInSameWindow(dir?: string): Promise<void> {
dir = await this.selectFolder(dir)
if (!dir) return
this.workingDir = convertPathToPosix(dir)
await this.updateRecentFolders(dir)
await this.updateOpenedFolders(dir)
this.window.setTitle(this.workingDir)
this.watch()
this.emit('workingDirChanged', dir)
}
}

@ -0,0 +1,220 @@
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import chokidar from 'chokidar'
import { ElectronBasePluginRemixdClient } from "../lib/remixd"
import fs from 'fs'
import * as utils from '../lib/utils'
import { basename, join } from "path";
import { spawn } from "child_process";
const profile: Profile = {
name: 'hardhat',
displayName: 'electron slither',
description: 'electron slither',
}
export class HardhatPlugin extends ElectronBasePlugin {
clients: any[]
constructor() {
super(profile, clientProfile, HardhatPluginClient)
this.methods = [...super.methods]
}
}
const clientProfile: Profile = {
name: 'hardhat',
displayName: 'electron hardhat',
description: 'electron hardhat',
methods: ['sync', 'compile']
}
class HardhatPluginClient extends ElectronBasePluginRemixdClient {
watcher: chokidar.FSWatcher
warnlog: boolean
buildPath: string
cachePath: string
logTimeout: NodeJS.Timeout
processingTimeout: NodeJS.Timeout
async onActivation(): Promise<void> {
console.log('Hardhat plugin activated')
this.call('terminal', 'log', { type: 'log', value: 'Hardhat plugin activated' })
this.on('fs' as any, 'workingDirChanged', async (path: string) => {
console.log('workingDirChanged hardhat', path)
this.currentSharedFolder = path
this.startListening()
})
this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir')
if(this.currentSharedFolder) this.startListening()
}
startListening() {
this.buildPath = utils.absolutePath('artifacts/contracts', this.currentSharedFolder)
if (fs.existsSync(this.buildPath)) {
this.listenOnHardhatCompilation()
} else {
console.log('If you are using Hardhat, run `npx hardhat compile` or run the compilation with `Enable Hardhat Compilation` checked from the Remix IDE.')
this.listenOnHardHatFolder()
}
}
compile(configPath: string) {
return new Promise((resolve, reject) => {
const cmd = `npx hardhat compile --config ${utils.normalizePath(configPath)}`
const options = { cwd: this.currentSharedFolder, shell: true }
const child = spawn(cmd, options)
let result = ''
let error = ''
child.stdout.on('data', (data) => {
const msg = `[Hardhat Compilation]: ${data.toString()}`
console.log('\x1b[32m%s\x1b[0m', msg)
result += msg + '\n'
})
child.stderr.on('data', (err) => {
error += `[Hardhat Compilation]: ${err.toString()} \n`
})
child.on('close', () => {
if (error && result) resolve(error + result)
else if (error) reject(error)
else resolve(result)
})
})
}
checkPath() {
if (!fs.existsSync(this.buildPath)) {
this.listenOnHardHatFolder()
return false
}
return true
}
private async processArtifact() {
console.log('processing artifact')
if (!this.checkPath()) return
// resolving the files
const folderFiles = await fs.promises.readdir(this.buildPath)
const targetsSynced = []
// name of folders are file names
for (const file of folderFiles) { // ["artifacts/contracts/Greeter.sol/"]
const contractFilePath = join(this.buildPath, file)
const stat = await fs.promises.stat(contractFilePath)
if (!stat.isDirectory()) continue
const files = await fs.promises.readdir(contractFilePath)
const compilationResult = {
input: {},
output: {
contracts: {},
sources: {}
},
solcVersion: null,
target: null
}
for (const file of files) {
if (file.endsWith('.dbg.json')) { // "artifacts/contracts/Greeter.sol/Greeter.dbg.json"
const stdFile = file.replace('.dbg.json', '.json')
const contentStd = await fs.promises.readFile(join(contractFilePath, stdFile), { encoding: 'utf-8' })
const contentDbg = await fs.promises.readFile(join(contractFilePath, file), { encoding: 'utf-8' })
const jsonDbg = JSON.parse(contentDbg)
const jsonStd = JSON.parse(contentStd)
compilationResult.target = jsonStd.sourceName
targetsSynced.push(compilationResult.target)
const path = join(contractFilePath, jsonDbg.buildInfo)
const content = await fs.promises.readFile(path, { encoding: 'utf-8' })
await this.feedContractArtifactFile(content, compilationResult)
}
if (compilationResult.target) {
// we are only interested in the contracts that are in the target of the compilation
compilationResult.output = {
...compilationResult.output,
contracts: { [compilationResult.target]: compilationResult.output.contracts[compilationResult.target] }
}
this.emit('compilationFinished', compilationResult.target, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion)
}
}
}
clearTimeout(this.logTimeout)
this.logTimeout = setTimeout(() => {
this.call('terminal', 'log', { value: 'receiving compilation result from Hardhat. Select a file to populate the contract interaction interface.', type: 'log' })
if (targetsSynced.length) {
console.log(`Processing artifacts for files: ${[...new Set(targetsSynced)].join(', ')}`)
// @ts-ignore
this.call('terminal', 'log', { type: 'log', value: `synced with Hardhat: ${[...new Set(targetsSynced)].join(', ')}` })
} else {
console.log('No artifacts to process')
// @ts-ignore
this.call('terminal', 'log', { type: 'log', value: 'No artifacts from Hardhat to process' })
}
}, 1000)
}
listenOnHardHatFolder() {
console.log('Hardhat artifacts folder doesn\'t exist... waiting for the compilation.')
try {
if (this.watcher) this.watcher.close()
this.watcher = chokidar.watch(this.currentSharedFolder, { depth: 2, ignorePermissionErrors: true, ignoreInitial: true })
// watch for new folders
this.watcher.on('addDir', (path: string) => {
console.log('add dir hardhat', path)
if (fs.existsSync(this.buildPath)) {
this.listenOnHardhatCompilation()
}
})
} catch (e) {
console.log('listenOnHardHatFolder', e)
}
}
async triggerProcessArtifact() {
console.log('triggerProcessArtifact')
// prevent multiple calls
clearTimeout(this.processingTimeout)
this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000)
}
listenOnHardhatCompilation() {
try {
console.log('listening on Hardhat compilation...', this.buildPath)
if (this.watcher) this.watcher.close()
this.watcher = chokidar.watch(this.buildPath, { depth: 1, ignorePermissionErrors: true, ignoreInitial: true })
this.watcher.on('change', async () => await this.triggerProcessArtifact())
this.watcher.on('add', async () => await this.triggerProcessArtifact())
this.watcher.on('unlink', async () => await this.triggerProcessArtifact())
// process the artifact on activation
this.processArtifact()
} catch (e) {
console.log('listenOnHardhatCompilation', e)
}
}
async sync() {
console.log('syncing from Hardhat')
this.processArtifact()
}
async feedContractArtifactFile(artifactContent, compilationResultPart) {
const contentJSON = JSON.parse(artifactContent)
compilationResultPart.solcVersion = contentJSON.solcVersion
for (const file in contentJSON.input.sources) {
const source = contentJSON.input.sources[file]
const absPath = join(this.currentSharedFolder, file)
if (fs.existsSync(absPath)) { // if not that is a lib
const contentOnDisk = await fs.promises.readFile(absPath, { encoding: 'utf-8' })
if (contentOnDisk === source.content) {
compilationResultPart.input[file] = source
compilationResultPart.output['sources'][file] = contentJSON.output.sources[file]
compilationResultPart.output['contracts'][file] = contentJSON.output.contracts[file]
if (contentJSON.output.errors && contentJSON.output.errors.length) {
compilationResultPart.output['errors'] = contentJSON.output.errors.filter(error => error.sourceLocation.file === file)
}
}
}
}
}
}

@ -1,19 +1,19 @@
import { PluginClient } from "@remixproject/plugin"; import {Profile} from '@remixproject/plugin-utils'
import { Profile } from "@remixproject/plugin-utils"; import {ElectronBasePlugin, ElectronBasePluginClient} from '@remixproject/plugin-electron'
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import fs from 'fs/promises' import fs from 'fs/promises'
import git from 'isomorphic-git' import git from 'isomorphic-git'
import { dialog } from "electron";
import http from 'isomorphic-git/http/web' import http from 'isomorphic-git/http/web'
import { gitProxy } from "../tools/git"; import {gitProxy} from '../tools/git'
import { remote } from "../types"; import {isoGit} from '@remix-git'
import {branchDifference, branchInputType, checkoutInputType, cloneInputType, commitChange, commitInputType, compareBranchesInput, currentBranchInput, fetchInputType, initInputType, logInputType, pullInputType, pushInputType, remote, resolveRefInput, statusInput} from '@remix-api'
const profile: Profile = { const profile: Profile = {
name: 'isogit', name: 'isogit',
displayName: 'isogit', displayName: 'isogit',
description: 'isogit plugin', description: 'isogit plugin',
} }
// used in e2e tests
const useIsoGit = process.argv.includes('--use-isogit')
export class IsoGitPlugin extends ElectronBasePlugin { export class IsoGitPlugin extends ElectronBasePlugin {
clients: IsoGitPluginClient[] = [] clients: IsoGitPluginClient[] = []
constructor() { constructor() {
@ -21,33 +21,18 @@ export class IsoGitPlugin extends ElectronBasePlugin {
} }
startClone(webContentsId: any): void { startClone(webContentsId: any): void {
const client = this.clients.find(c => c.webContentsId === webContentsId) const client = this.clients.find((c) => c.webContentsId === webContentsId)
if (client) { if (client) {
client.startClone() client.startClone()
} }
} }
} }
const parseInput = (input: any) => {
return {
corsProxy: 'https://corsproxy.remixproject.org/',
http,
onAuth: (url: any) => {
url
const auth = {
username: input.token,
password: ''
}
return auth
}
}
}
const clientProfile: Profile = { const clientProfile: Profile = {
name: 'isogit', name: 'isogit',
displayName: 'isogit', displayName: 'isogit',
description: 'isogit plugin', description: 'isogit plugin',
methods: ['init', 'localStorageUsed', 'version', 'addremote', 'delremote', 'remotes', 'fetch', 'clone', 'export', 'import', 'status', 'log', 'commit', 'add', 'remove', 'reset', 'rm', 'lsfiles', 'readblob', 'resolveref', 'branches', 'branch', 'checkout', 'currentbranch', 'push', 'pin', 'pull', 'pinList', 'unPin', 'setIpfsConfig', 'zip', 'setItem', 'getItem', 'openFolder'] methods: ['init', 'localStorageUsed', 'version', 'addremote', 'delremote', 'remotes', 'fetch', 'clone', 'export', 'import', 'status', 'log', 'commit', 'add', 'remove', 'rm', 'readblob', 'resolveref', 'branches', 'branch', 'checkout', 'currentbranch', 'push', 'pin', 'pull', 'pinList', 'unPin', 'setIpfsConfig', 'zip', 'setItem', 'getItem', 'openFolder', 'getCommitChanges', 'compareBranches', 'startClone', 'updateSubmodules'],
} }
class IsoGitPluginClient extends ElectronBasePluginClient { class IsoGitPluginClient extends ElectronBasePluginClient {
@ -58,15 +43,15 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
this.onload(async () => { this.onload(async () => {
this.on('fs' as any, 'workingDirChanged', async (path: string) => { this.on('fs' as any, 'workingDirChanged', async (path: string) => {
this.workingDir = path this.workingDir = path
this.gitIsInstalled = await gitProxy.version() ? true : false this.gitIsInstalled = (await gitProxy.version()) && !useIsoGit ? true : false
}) })
this.workingDir = await this.call('fs' as any, 'getWorkingDir') this.workingDir = await this.call('fs' as any, 'getWorkingDir')
this.gitIsInstalled = await gitProxy.version() ? true : false this.gitIsInstalled = (await gitProxy.version()) && !useIsoGit ? true : false
}) })
} }
async version() { async version() {
return gitProxy.version() return this.gitIsInstalled ? gitProxy.version() : 'built-in'
} }
async getGitConfig() { async getGitConfig() {
@ -76,13 +61,12 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
} }
} }
async status(cmd: any) { async status(cmd: statusInput) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
if (this.workingDir === '') { if (this.workingDir === '') {
return [] return []
} }
@ -93,30 +77,25 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
} }
const status = await git.statusMatrix({ const status = await git.statusMatrix({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd ...cmd,
}) })
//console.log('STATUS', status, await this.getGitConfig())
return status return status
} }
async log(cmd: any) { async log(cmd: logInputType) {
/* we will use isomorphic git for now const token = await this.call('config' as any, 'getAppParameter', 'settings/gist-access-token')
if(this.gitIsInstalled){
const log = await gitProxy.log(this.workingDir, cmd.ref)
console.log('LOG', log)
return log
}
*/
if (this.workingDir === '') { if (this.workingDir === '') {
return [] return []
} }
const log = await git.log({ const log = await git.log({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd ...cmd,
depth: cmd.depth || 10,
}) })
return log return log
@ -128,62 +107,45 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
} }
const add = await git.add({ const add = await git.add({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd ...cmd,
}) })
return add return add
} }
async rm(cmd: any) { async rm(cmd: any) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
const rm = await git.remove({ const rm = await git.remove({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd ...cmd,
}) })
return rm return rm
} }
async reset(cmd: any) { async commit(cmd: commitInputType) {
if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory')
}
const reset = await git.resetIndex({
...await this.getGitConfig(),
...cmd
})
return reset
}
async commit(cmd: any) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
if (this.gitIsInstalled) { if (this.gitIsInstalled) {
const status = await gitProxy.commit(this.workingDir, cmd.message) const status = await gitProxy.commit(this.workingDir, cmd)
return status return status
} }
const commit = await git.commit({ const commit = await git.commit({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd ...cmd,
}) })
return commit return commit
} }
async init(input: any) { async init(input: initInputType) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
@ -192,157 +154,119 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
return status return status
} }
await git.init({ await git.init({
...await this.getGitConfig(), ...(await this.getGitConfig()),
defaultBranch: (input && input.branch) || 'main' defaultBranch: (input && input.defaultBranch) || 'main',
}) })
} }
async branch(cmd: any) { async branch(cmd: branchInputType) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
return null return null
} }
const branch = await git.branch({ const branch = await git.branch({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd ...cmd,
}) })
return branch return branch
} }
async lsfiles(cmd: any) { async resolveref(cmd: resolveRefInput) {
if (!this.workingDir || this.workingDir === '') {
return []
}
const lsfiles = await git.listFiles({
...await this.getGitConfig(),
...cmd
})
return lsfiles
}
async resolveref(cmd: any) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
return null return null
} }
const resolveref = await git.resolveRef({ const resolveref = await git.resolveRef({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd ...cmd,
}) })
return resolveref return resolveref
} }
async readblob(cmd: any) { async readblob(cmd: any) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
const readblob = await git.readBlob({ const readblob = await git.readBlob({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd ...cmd,
}) })
return readblob return readblob
} }
async checkout(cmd: any) { async checkout(cmd: checkoutInputType) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
if (this.gitIsInstalled) {
const checkout = await git.checkout({ return await gitProxy.checkout(this.workingDir, cmd)
...await this.getGitConfig(), } else {
...cmd const checkout = await git.checkout({
}) ...(await this.getGitConfig()),
...cmd,
return checkout })
return checkout
}
} }
async push(cmd: any) { async push(input: pushInputType) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
if (this.gitIsInstalled) { if (this.gitIsInstalled) {
await gitProxy.push(this.workingDir, cmd.remote, cmd.ref, cmd.remoteRef, cmd.force) return await gitProxy.push(this.workingDir, input)
} else { } else {
const push = await isoGit.push(input, await this.getGitConfig(), this)
const push = await git.push({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input)
})
return push return push
} }
} }
async pull(cmd: any) { async pull(input: pullInputType) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
if (this.gitIsInstalled) { if (this.gitIsInstalled) {
await gitProxy.pull(this.workingDir, cmd.remote, cmd.ref, cmd.remoteRef) return await gitProxy.pull(this.workingDir, input)
} else { } else {
const pull = await isoGit.pull(input, await this.getGitConfig(), this)
const pull = await git.pull({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input)
})
return pull return pull
} }
} }
async fetch(cmd: any) { async fetch(input: fetchInputType) {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
throw new Error('No working directory') throw new Error('No working directory')
} }
if (this.gitIsInstalled) { if (this.gitIsInstalled) {
await gitProxy.fetch(this.workingDir, cmd.remote, cmd.remoteRef) await gitProxy.fetch(this.workingDir, input)
} else { } else {
const fetch = await isoGit.fetch(input, await this.getGitConfig(), this)
const fetch = await git.fetch({
...await this.getGitConfig(),
...cmd,
...parseInput(cmd.input)
})
return fetch return fetch
} }
} }
async clone(cmd: any) { async clone(cmd: cloneInputType) {
if (this.gitIsInstalled) { if (this.gitIsInstalled) {
try { try {
await gitProxy.clone(cmd.url, cmd.dir) this.call('terminal' as any, 'log', 'Cloning using git... please wait.')
await gitProxy.clone(cmd)
} catch (e) { } catch (e) {
throw e throw e
} }
} else { } else {
try { try {
const clone = await git.clone({ this.call('terminal' as any, 'log', 'Cloning using builtin git... please wait.')
...await this.getGitConfig(), const clone = await isoGit.clone(cmd, await this.getGitConfig(), this)
...cmd,
...parseInput(cmd.input),
dir: cmd.dir || this.workingDir
})
return clone return clone
} catch (e) { } catch (e) {
console.log('CLONE ERROR', e) console.log('CLONE ERROR', e)
@ -351,85 +275,74 @@ class IsoGitPluginClient extends ElectronBasePluginClient {
} }
} }
async addremote(cmd: any) { async addremote(input: remote) {
const addremote = await git.addRemote({ const addremote = await git.addRemote({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd url: input.url,
remote: input.name,
}) })
return addremote return addremote
} }
async delremote(cmd: any) { async delremote(input: remote) {
const delremote = await git.deleteRemote({ const delremote = await git.deleteRemote({
...await this.getGitConfig(), ...(await this.getGitConfig()),
...cmd remote: input.name,
}) })
return delremote return delremote
} }
async remotes() {
remotes = async () => {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
return [] return []
} }
let remotes: remote[] = [] const defaultConfig = await this.getGitConfig()
remotes = (await git.listRemotes({ ...await this.getGitConfig() })).map((remote) => { return { name: remote.remote, url: remote.url } } return await isoGit.remotes(defaultConfig)
)
return remotes
} }
async currentbranch() { async currentbranch(input: currentBranchInput) {
if (!this.workingDir || this.workingDir === '') {
return ''
}
try {
const defaultConfig = await this.getGitConfig()
const name = await git.currentBranch(defaultConfig)
return name if (!this.workingDir || this.workingDir === '') {
} catch (e) {
return '' return ''
} }
const defaultConfig = await this.getGitConfig()
return await isoGit.currentbranch(input, defaultConfig)
} }
async branches(config: any) {
async branches() {
if (!this.workingDir || this.workingDir === '') { if (!this.workingDir || this.workingDir === '') {
return [] return []
} }
try {
let cmd: any = { ...await this.getGitConfig() }
const remotes = await this.remotes()
let branches = []
branches = (await git.listBranches(cmd)).map((branch) => { return { remote: undefined, name: branch } })
for (const remote of remotes) {
cmd = {
...cmd,
remote: remote.name
}
const remotebranches = (await git.listBranches(cmd)).map((branch) => { return { remote: remote.name, name: branch } })
branches = [...branches, ...remotebranches]
} const defaultConfig = await this.getGitConfig()
return await isoGit.branches(defaultConfig)
return branches
} catch (e) {
return []
}
} }
async startClone() { async startClone() {
this.call('filePanel' as any, 'clone') this.call('filePanel' as any, 'clone')
} }
} async getCommitChanges(commitHash1: string, commitHash2: string): Promise<commitChange[]> {
return await isoGit.getCommitChanges(commitHash1, commitHash2, await this.getGitConfig())
}
async compareBranches({branch, remote}: compareBranchesInput): Promise<branchDifference> {
return await isoGit.compareBranches({branch, remote}, await this.getGitConfig())
}
async updateSubmodules(input) {
if (this.gitIsInstalled) {
try {
return await gitProxy.updateSubmodules(this.workingDir)
} catch (e) {
throw e
}
} else {
this.call('terminal', 'log', {type: 'error', value: 'Please install git into your OS to use this functionality...'})
}
}
}

@ -1,10 +1,9 @@
import {PluginClient} from '@remixproject/plugin' import { Profile } from '@remixproject/plugin-utils'
import {Profile} from '@remixproject/plugin-utils' import { ElectronBasePlugin, ElectronBasePluginClient } from '@remixproject/plugin-electron'
import {ElectronBasePlugin, ElectronBasePluginClient} from '@remixproject/plugin-electron'
import path from 'path' import path from 'path'
import {rgPath} from '@vscode/ripgrep' import { rgPath } from '@vscode/ripgrep'
import byline from 'byline' import byline from 'byline'
import {spawn} from 'child_process' import { spawn } from 'child_process'
import { SearchInWorkspaceOptions } from '../lib' import { SearchInWorkspaceOptions } from '../lib'
const profile: Profile = { const profile: Profile = {
@ -69,14 +68,14 @@ export class RipgrepPluginClient extends ElectronBasePluginClient {
if (opts && opts.include) { if (opts && opts.include) {
for (const include of opts.include) { for (const include of opts.include) {
if (include !== '') { if (include !== '') {
globs.push('--glob=' + include) globs.push('--glob=' + include + '')
} }
} }
} }
if (opts && opts.exclude) { if (opts && opts.exclude) {
for (const exclude of opts.exclude) { for (const exclude of opts.exclude) {
if (exclude !== '') { if (exclude !== '') {
globs.push('--glob=!=' + exclude) globs.push('--glob=!' + exclude + '')
} }
} }
} }
@ -85,29 +84,37 @@ export class RipgrepPluginClient extends ElectronBasePluginClient {
// replace packed app path with unpacked app path for release on windows // replace packed app path with unpacked app path for release on windows
const customRgPath = rgPath.includes('app.asar.unpacked') ? rgPath : rgPath.replace('app.asar', 'app.asar.unpacked') const customRgPath = rgPath.includes('app.asar.unpacked') ? rgPath : rgPath.replace('app.asar', 'app.asar.unpacked')
console.log('customRgPath', [...globs, ...args, opts.pattern, '.'], path)
const rg = spawn(customRgPath, [...globs, ...args, opts.pattern, '.'], {
cwd: path
});
const rg = spawn(customRgPath, [...globs, ...args, opts.pattern, path])
const resultrg: any[] = [] const resultrg: any[] = []
const stream = byline(rg.stdout.setEncoding('utf8')) const stream = byline(rg.stdout.setEncoding('utf8'))
stream.on('data', (rgresult: string) => { stream.on('data', (rgresult: string) => {
let pathWithoutWorkingDir = rgresult.replace(convertPathToPosix(this.workingDir), '') console.log('rgresult', rgresult, convertPathToPosix(this.workingDir), convertPathToPosix(rgresult))
let pathWithoutWorkingDir = convertPathToPosix(rgresult).replace(convertPathToPosix(this.workingDir), '')
console.log(pathWithoutWorkingDir)
if (pathWithoutWorkingDir.endsWith('/')) { if (pathWithoutWorkingDir.endsWith('/')) {
pathWithoutWorkingDir = pathWithoutWorkingDir.slice(0, -1) pathWithoutWorkingDir = pathWithoutWorkingDir.slice(0, -1)
} }
if (pathWithoutWorkingDir.startsWith('/')) { if (pathWithoutWorkingDir.startsWith('/')) {
pathWithoutWorkingDir = pathWithoutWorkingDir.slice(1) pathWithoutWorkingDir = pathWithoutWorkingDir.slice(1)
} }
if (pathWithoutWorkingDir.startsWith('./')) {
pathWithoutWorkingDir = pathWithoutWorkingDir.slice(2)
}
if (pathWithoutWorkingDir.startsWith('\\')) { if (pathWithoutWorkingDir.startsWith('\\')) {
pathWithoutWorkingDir = pathWithoutWorkingDir.slice(1) pathWithoutWorkingDir = pathWithoutWorkingDir.slice(1)
} }
resultrg.push({ resultrg.push({
path: convertPathToPosix(pathWithoutWorkingDir), path: pathWithoutWorkingDir,
isDirectory: false, isDirectory: false,
}) })
}) })
stream.on('end', () => { stream.on('end', () => {
c(resultrg) c(resultrg)
}) })
}) })

@ -0,0 +1,197 @@
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import { ElectronBasePluginRemixdClient } from "../lib/remixd"
import * as utils from '../lib/utils'
import { existsSync, readdirSync, readFileSync, unlinkSync } from "fs-extra";
export interface OutputStandard {
description: string
title: string
confidence: string
severity: string
sourceMap: any
category?: string
reference?: string
example?: any
[key: string]: any
}
const { spawn, execSync } = require('child_process') // eslint-disable-line
const profile: Profile = {
name: 'slither',
displayName: 'electron slither',
description: 'electron slither',
}
export class SlitherPlugin extends ElectronBasePlugin {
clients: any[]
constructor() {
super(profile, clientProfile, SlitherPluginClient)
this.methods = [...super.methods]
}
}
const clientProfile: Profile = {
name: 'slither',
displayName: 'electron slither',
description: 'electron slither',
methods: ['analyse']
}
class SlitherPluginClient extends ElectronBasePluginRemixdClient {
mapNpmDepsDir(list) {
const remixNpmDepsPath = utils.absolutePath('.deps/npm', this.currentSharedFolder)
const localNpmDepsPath = utils.absolutePath('node_modules', this.currentSharedFolder)
const npmDepsExists = existsSync(remixNpmDepsPath)
const nodeModulesExists = existsSync(localNpmDepsPath)
let isLocalDep = false
let isRemixDep = false
let allowPathString = ''
let remapString = ''
for (const e of list) {
const importPath = e.replace(/import ['"]/g, '').trim()
const packageName = importPath.split('/')[0]
if (nodeModulesExists && readdirSync(localNpmDepsPath).includes(packageName)) {
isLocalDep = true
remapString += `${packageName}=./node_modules/${packageName} `
} else if (npmDepsExists && readdirSync(remixNpmDepsPath).includes(packageName)) {
isRemixDep = true
remapString += `${packageName}=./.deps/npm/${packageName} `
}
}
if (isLocalDep) allowPathString += './node_modules,'
if (isRemixDep) allowPathString += './.deps/npm,'
return { remapString, allowPathString }
}
transform(detectors: Record<string, any>[]): OutputStandard[] {
const standardReport: OutputStandard[] = []
for (const e of detectors) {
const obj = {} as OutputStandard
obj.description = e.description
obj.title = e.check
obj.confidence = e.confidence
obj.severity = e.impact
obj.sourceMap = e.elements.map((element) => {
delete element.source_mapping.filename_used
delete element.source_mapping.filename_absolute
return element
})
standardReport.push(obj)
}
return standardReport
}
analyse(filePath: string, compilerConfig: Record<string, any>) {
return new Promise((resolve, reject) => {
const options = { cwd: this.currentSharedFolder, shell: true }
const { currentVersion, optimize, evmVersion } = compilerConfig
if (currentVersion && currentVersion.includes('+commit')) {
// Get compiler version with commit id e.g: 0.8.2+commit.661d110
const versionString: string = currentVersion.substring(0, currentVersion.indexOf('+commit') + 16)
this.log(`[Slither Analysis]: Compiler version is ${versionString}`)
let solcOutput: Buffer
// Check solc current installed version
try {
solcOutput = execSync('solc --version', options)
} catch (err) {
this.error(err)
reject(new Error('Error in running solc command'))
}
if (!solcOutput.toString().includes(versionString)) {
this.log('[Slither Analysis]: Compiler version is different from installed solc version')
// Get compiler version without commit id e.g: 0.8.2
const version: string = versionString.substring(0, versionString.indexOf('+commit'))
// List solc versions installed using solc-select
try {
const solcSelectInstalledVersions: Buffer = execSync('solc-select versions', options)
// Check if required version is already installed
if (!solcSelectInstalledVersions.toString().includes(version)) {
this.log(`[Slither Analysis]: Installing ${version} using solc-select`)
// Install required version
execSync(`solc-select install ${version}`, options)
}
this.log(`[Slither Analysis]: Setting ${version} as current solc version using solc-select`)
// Set solc current version as required version
execSync(`solc-select use ${version}`, options)
} catch (err) {
this.error(err)
reject(new Error('Error in running solc-select command'))
}
} else this.log('[Slither Analysis]: Compiler version is same as installed solc version')
}
// Allow paths and set solc remapping for import URLs
const fileContent = readFileSync(utils.absolutePath(filePath, this.currentSharedFolder), 'utf8')
const importsArr = fileContent.match(/import ['"][^.|..](.+?)['"];/g)
let remaps = ''
if (importsArr?.length) {
const { remapString } = this.mapNpmDepsDir(importsArr)
remaps = remapString.trim()
}
const optimizeOption: string = optimize ? '--optimize' : ''
const evmOption: string = evmVersion ? `--evm-version ${evmVersion}` : ''
let solcArgs = ''
if (optimizeOption) {
solcArgs += optimizeOption + ' '
}
if (evmOption) {
if (!solcArgs.endsWith(' ')) solcArgs += ' '
solcArgs += evmOption
}
if (solcArgs) {
solcArgs = `--solc-args "${solcArgs.trimStart()}"`
}
const solcRemaps = remaps ? `--solc-remaps "${remaps}"` : ''
const outputFile = 'remix-slither-report.json'
try {
// We don't keep the previous analysis
const outputFilePath = utils.absolutePath(outputFile, this.currentSharedFolder)
if (existsSync(outputFilePath)) unlinkSync(outputFilePath)
} catch (e) {
this.error('unable to remove the output file')
this.error(e.message)
}
const cmd = `slither ${filePath} ${solcArgs} ${solcRemaps} --json ${outputFile}`
this.log('[Slither Analysis]: Running Slither...')
// Added `stdio: 'ignore'` as for contract with NPM imports analysis which is exported in 'stderr'
// get too big and hangs the process. We process analysis from the report file only
const child = spawn(cmd, { cwd: this.currentSharedFolder, shell: true, stdio: 'ignore' })
const response = {}
child.on('close', () => {
const outputFileAbsPath: string = utils.absolutePath(outputFile, this.currentSharedFolder)
// Check if slither report file exists
if (existsSync(outputFileAbsPath)) {
let report = readFileSync(outputFileAbsPath, 'utf8')
report = JSON.parse(report)
if (report['success']) {
response['status'] = true
if (!report['results'] || !report['results'].detectors || !report['results'].detectors.length) {
response['count'] = 0
} else {
const { detectors } = report['results']
response['count'] = detectors.length
response['data'] = this.transform(detectors)
}
resolve(response)
} else {
this.log(report['error'])
reject(new Error('Error in running Slither Analysis.'))
}
} else {
this.error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.')
reject(new Error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.'))
}
})
})
}
}

@ -42,20 +42,26 @@ class TemplatesPluginClient extends ElectronBasePluginClient {
super(webContentsId, profile) super(webContentsId, profile)
} }
async loadTemplateInNewWindow (files: any) { async loadTemplateInNewWindow(files: any) {
let folder = await this.call('fs' as any, 'selectFolder', null ,'Select or create a folder to load the template in', 'Set as destination folder for the template') let folder = await this.call('fs' as any, 'selectFolder', null, 'Select or create a folder to load the files in', 'Set as destination folder for the files')
if (!folder || folder === '') return if (!folder || folder === '') return
// @ts-ignore // @ts-ignore
for (const file in files) { for (const file in files) {
try { try {
if(!folder.endsWith('/')) folder += '/' if (!folder.endsWith('/')) folder += '/'
await fs.mkdir(path.dirname(folder + file), { recursive: true}) await fs.mkdir(path.dirname(folder + file), { recursive: true })
await fs.writeFile(folder + file, files[file], { if (typeof files[file] !== 'string' && files[file].content) {
encoding: 'utf8' await fs.writeFile(folder + file, files[file].content, {
}) encoding: 'utf8',
})
} else {
await fs.writeFile(folder + file, files[file], {
encoding: 'utf8'
})
}
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
@ -63,7 +69,7 @@ class TemplatesPluginClient extends ElectronBasePluginClient {
createWindow(folder) createWindow(folder)
} }
async openTemplate(){ async openTemplate() {
this.call('filePanel' as any, 'loadTemplate') this.call('filePanel' as any, 'loadTemplate')
} }

@ -1,18 +1,15 @@
import {PluginClient} from '@remixproject/plugin' import {PluginClient} from '@remixproject/plugin'
import {Profile} from '@remixproject/plugin-utils' import {Profile} from '@remixproject/plugin-utils'
import { import {ElectronBasePlugin, ElectronBasePluginClient} from '@remixproject/plugin-electron'
ElectronBasePlugin,
ElectronBasePluginClient,
} from '@remixproject/plugin-electron'
import os from 'os' import os from 'os'
import * as pty from 'node-pty' import * as pty from 'node-pty'
import process from 'node:process' import process from 'node:process'
import {userInfo} from 'node:os' import {userInfo} from 'node:os'
import {findExecutable} from '../utils/findExecutable' import {findExecutable} from '../utils/findExecutable'
import {spawnSync} from 'child_process' import {exec, spawnSync} from 'child_process'
import { stripAnsi } from '../lib' import {stripAnsi} from '../lib'
import { DataBatcher } from '../lib/databatcher' import {DataBatcher} from '../lib/databatcher'
export const detectDefaultShell = () => { export const detectDefaultShell = () => {
const {env} = process const {env} = process
@ -38,10 +35,7 @@ export const detectDefaultShell = () => {
// Stores default shell when imported. // Stores default shell when imported.
const defaultShell = detectDefaultShell() const defaultShell = detectDefaultShell()
const getShellEnvArgs = [ const getShellEnvArgs = ['-ilc', 'echo -n "_SHELL_ENV_DELIMITER_"; env; echo -n "_SHELL_ENV_DELIMITER_"; exit']
'-ilc',
'echo -n "_SHELL_ENV_DELIMITER_"; env; echo -n "_SHELL_ENV_DELIMITER_"; exit',
]
const getShellEnvEnv = { const getShellEnvEnv = {
// Disables Oh My Zsh auto-update thing that can block the process. // Disables Oh My Zsh auto-update thing that can block the process.
@ -81,7 +75,9 @@ export class XtermPlugin extends ElectronBasePlugin {
new(webContentsId: any): void { new(webContentsId: any): void {
const client = this.clients.find((c) => c.webContentsId === webContentsId) const client = this.clients.find((c) => c.webContentsId === webContentsId)
console.log('new terminal', webContentsId)
if (client) { if (client) {
console.log('client exists')
client.new() client.new()
} }
} }
@ -104,6 +100,7 @@ class XtermPluginClient extends ElectronBasePluginClient {
terminals: pty.IPty[] = [] terminals: pty.IPty[] = []
dataBatchers: DataBatcher[] = [] dataBatchers: DataBatcher[] = []
workingDir: string = '' workingDir: string = ''
parsedEnv: any = null
constructor(webContentsId: number, profile: Profile) { constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile) super(webContentsId, profile)
this.onload(async () => { this.onload(async () => {
@ -114,6 +111,13 @@ class XtermPluginClient extends ElectronBasePluginClient {
this.workingDir = await this.call('fs' as any, 'getWorkingDir') this.workingDir = await this.call('fs' as any, 'getWorkingDir')
console.log('workingDir', this.workingDir) console.log('workingDir', this.workingDir)
}) })
if (!(process.platform === 'win32')) {
const {stdout} = spawnSync(defaultShell, getShellEnvArgs, {
encoding: 'utf8',
})
this.parsedEnv = parseEnv(stdout)
}
} }
async keystroke(key: string, pid: number): Promise<void> { async keystroke(key: string, pid: number): Promise<void> {
@ -123,7 +127,7 @@ class XtermPluginClient extends ElectronBasePluginClient {
async getShells(): Promise<string[]> { async getShells(): Promise<string[]> {
if (os.platform() === 'win32') { if (os.platform() === 'win32') {
let bash = await findExecutable('bash.exe') let bash = await findExecutable('bash.exe')
if(bash.length === 0) { if (bash.length === 0) {
bash = await findExecutable('bash.exe', undefined, [process.env['ProgramFiles'] + '\\Git\\bin']) bash = await findExecutable('bash.exe', undefined, [process.env['ProgramFiles'] + '\\Git\\bin'])
} }
if (bash) { if (bash) {
@ -137,40 +141,72 @@ class XtermPluginClient extends ElectronBasePluginClient {
} }
async createTerminal(path?: string, shell?: string): Promise<number> { async createTerminal(path?: string, shell?: string): Promise<number> {
let parsedEnv: any = null const start_time = Date.now()
if (!(process.platform === 'win32')) { console.log('createTerminal', path, shell || defaultShell)
const {stdout} = spawnSync(defaultShell, getShellEnvArgs, {
encoding: 'utf8',
})
parsedEnv = parseEnv(stdout)
}
const env = parsedEnv || process.env const env = this.parsedEnv || process.env
const ptyProcess = pty.spawn(shell || defaultShell, [], { const ptyProcess = pty.spawn(shell || defaultShell, [], {
name: 'xterm-color', name: 'xterm-color',
cols: 80, cols: 80,
rows: 20, rows: 20,
cwd: path || process.cwd(), cwd: path || this.workingDir || process.cwd(),
env: env, env: env,
encoding: 'utf8',
}) })
const dataBatcher = new DataBatcher(ptyProcess.pid) const dataBatcher = new DataBatcher(ptyProcess.pid)
this.dataBatchers[ptyProcess.pid] = dataBatcher
ptyProcess.onData((data: string) => { ptyProcess.onData((data: string) => {
dataBatcher.write(Buffer.from(data)) dataBatcher.write(Buffer.from(data))
//this.sendData(data, ptyProcess.pid) })
ptyProcess.onExit(() => {
const pid = ptyProcess.pid
this.closeTerminal(pid)
}) })
dataBatcher.on('flush', (data: string, uid: number) => { dataBatcher.on('flush', (data: string, uid: number) => {
this.sendData(data, uid) this.sendData(data, uid)
}) })
this.terminals[ptyProcess.pid] = ptyProcess this.terminals[ptyProcess.pid] = ptyProcess
const end_time = Date.now()
console.log('createTerminal', end_time - start_time)
return ptyProcess.pid return ptyProcess.pid
} }
async closeTerminal(pid: number): Promise<void> { async closeTerminal(pid: number): Promise<void> {
this.terminals[pid].kill() console.log('closeTerminal', pid)
delete this.terminals[pid]
this.emit('close', pid) try {
if (this.terminals) {
if (this.dataBatchers[pid]) delete this.dataBatchers[pid]
if (this.terminals[pid]) {
try {
if (os.platform() === 'win32') {
// For Windows, use taskkill to terminate the process
exec(`taskkill /PID ${pid} /T /F`, (error, stdout, stderr) => {
if (error) {
console.error(`Error killing process: ${error}`);
}else{
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
}
});
} else {
this.terminals[pid].kill();
}
} catch (err) {
console.error(err)
// ignore
}
delete this.terminals[pid]
}
}
this.emit('close', pid)
} catch (err) {
console.error(err)
}
} }
async resize({cols, rows}: {cols: number; rows: number}, pid: number) { async resize({cols, rows}: {cols: number; rows: number}, pid: number) {
@ -199,8 +235,6 @@ class XtermPluginClient extends ElectronBasePluginClient {
} }
async new(): Promise<void> { async new(): Promise<void> {
console.log('new terminal') this.emit('new')
const pid = await this.createTerminal(this.workingDir)
this.emit('new', pid)
} }
} }

@ -6,7 +6,7 @@ console.log('preload.ts', new Date().toLocaleTimeString())
/* preload script needs statically defined API for each plugin */ /* preload script needs statically defined API for each plugin */
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader'] const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader', 'appUpdater', 'slither', 'foundry', 'hardhat']
let webContentsId: number | undefined let webContentsId: number | undefined
@ -15,19 +15,29 @@ ipcRenderer.invoke('getWebContentsID').then((id: number) => {
}) })
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
isPackaged: () => ipcRenderer.invoke('config:isPackaged'),
isE2E: () => ipcRenderer.invoke('config:isE2E'),
canTrackMatomo: () => ipcRenderer.invoke('config:canTrackMatomo'),
trackEvent: (args: any[]) => ipcRenderer.invoke('matomo:trackEvent', args),
openFolder: (path: string) => ipcRenderer.invoke('fs:openFolder', webContentsId, path),
openFolderInSameWindow: (path: string) => ipcRenderer.invoke('fs:openFolderInSameWindow', webContentsId, path),
activatePlugin: (name: string) => { activatePlugin: (name: string) => {
return ipcRenderer.invoke('manager:activatePlugin', name) return ipcRenderer.invoke('manager:activatePlugin', name)
}, },
getWindowId: () => ipcRenderer.invoke('getWindowID'),
plugins: exposedPLugins.map(name => { plugins: exposedPLugins.map(name => {
return { return {
name, name,
on: (cb:any) => ipcRenderer.on(`${name}:send`, cb), on: (cb:any) => {
ipcRenderer.on(`${name}:send`, cb)
},
send: (message: Partial<Message>) => { send: (message: Partial<Message>) => {
//if(name === 'isogit') console.log(name, message)
//if(name === 'isogit') ipcRenderer.invoke(`logger`, name, message)
ipcRenderer.send(`${name}:on:${webContentsId}`, message) ipcRenderer.send(`${name}:on:${webContentsId}`, message)
} }
} }
}) })
}) })

@ -1,6 +1,7 @@
import { exec } from 'child_process'; import { exec } from 'child_process';
import { CommitObject, ReadCommitResult } from 'isomorphic-git'; import { CommitObject, ReadCommitResult } from 'isomorphic-git';
import { promisify } from 'util'; import { promisify } from 'util';
import { cloneInputType, commitInputType, fetchInputType, pullInputType, pushInputType, checkoutInputType } from "@remix-api";
const execAsync = promisify(exec); const execAsync = promisify(exec);
const statusTransFormMatrix = (status: string) => { const statusTransFormMatrix = (status: string) => {
@ -9,12 +10,16 @@ const statusTransFormMatrix = (status: string) => {
return [0, 2, 0] return [0, 2, 0]
case 'A ': case 'A ':
return [0, 2, 2] return [0, 2, 2]
case 'R ':
return [0, 2, 2]
case 'M ': case 'M ':
return [1, 2, 2] return [1, 2, 2]
case 'MM': case 'MM':
return [1, 2, 3] return [1, 2, 3]
case ' M': case ' M':
return [1, 2, 0] return [1, 2, 1]
case 'AD':
return [0, 0, 3]
case ' D': case ' D':
return [1, 0, 1] return [1, 0, 1]
case 'D ': case 'D ':
@ -37,34 +42,87 @@ export const gitProxy = {
} }
}, },
async defaultRemoteName(path: string) {
try {
const { stdout } = await execAsync('git remote', { cwd: path });
const remotes = stdout.trim().split('\n');
return remotes[0];
} catch (error) {
throw new Error(`Failed to get the default remote name: ${error.message}`);
}
},
clone: async (url: string, path: string) => { clone: async (input: cloneInputType) => {
const { stdout, stderr } = await execAsync(`git clone ${url} ${path}`); const { stdout, stderr } = await execAsync(`git clone ${input.url} "${input.dir}"`);
}, },
async push(path: string, remote: string, src: string, branch: string, force: boolean = false) { async push(path: string, input: pushInputType) {
const { stdout, stderr } = await execAsync(`git push ${force ? ' -f' : ''} ${remote} ${src}:${branch}`, { cwd: path }); if(!input.remote || !input.remote.name) {
input.remote = { name: await gitProxy.defaultRemoteName(path), url: '' }
}
let remoteRefString = ''
if(input.remoteRef && !input.remoteRef.name) {
remoteRefString = `:${input.remoteRef.name}`
}
const { stdout, stderr } = await execAsync(`git push ${input.force ? ' -f' : ''} ${input.remote.name}${remoteRefString} ${input.ref.name}`, { cwd: path });
}, },
async pull(path: string, remote: string, src: string, branch: string) { async pull(path: string, input: pullInputType) {
const { stdout, stderr } = await execAsync(`git pull ${remote} ${src}:${branch}`, { cwd: path }); if(!input.remote || !input.remote.name) {
input.remote = { name: await gitProxy.defaultRemoteName(path), url: '' }
}
let remoteRefString = ''
if(input.remoteRef && !input.remoteRef.name) {
remoteRefString = `:${input.remoteRef.name}`
}
const { stdout, stderr } = await execAsync(`git pull ${input.remote.name} ${input.ref.name}${remoteRefString}`, { cwd: path });
}, },
async fetch(path: string, remote: string, branch: string) { async fetch(path: string, input: fetchInputType) {
const { stdout, stderr } = await execAsync(`git fetch ${remote} ${branch}`, { cwd: path }); if(!input.remote || !input.remote.name) {
input.remote = { name: await gitProxy.defaultRemoteName(path), url: '' }
}
try {
const { stdout, stderr } = await execAsync(`git fetch ${input.remote.name} ${(input.ref && input.ref.name) ? input.ref.name : ''}`, { cwd: path });
if (stdout) {
console.log('stdout:', stdout);
}
if (stderr) {
console.error('stderr:', stderr);
}
} catch (error) {
console.error('Error during fetch:', error);
}
},
async checkout(path: string, input: checkoutInputType) {
let force = input.force ? ' -f' : '';
const { stdout, stderr } = await execAsync(`git checkout ${force} ${input.ref}`, { cwd: path });
}, },
async commit(path: string, message: string) { async commit(path: string, input: commitInputType) {
await execAsync(`git commit -m '${message}'`, { cwd: path }); await execAsync(`git commit -m '${input.message}'`, { cwd: path });
const { stdout, stderr } = await execAsync(`git rev-parse HEAD`, { cwd: path }); const { stdout, stderr } = await execAsync(`git rev-parse HEAD`, { cwd: path });
console.log('stdout commit:', stdout);
return stdout; return stdout;
}, },
async init(path: string) { async init(path: string) {
await execAsync(`git init`, { cwd: path }); await execAsync(`git init --initial-branch=main`, { cwd: path });
},
async updateSubmodules(path: string) {
const { stdout, stderr } = await execAsync(`git submodule update --init --recursive`, { cwd: path });
if (stdout) {
console.log('stdout:', stdout);
}
if (stderr) {
console.error('stderr:', stderr);
}
}, },
@ -103,7 +161,7 @@ export const gitProxy = {
return 0 return 0
}) })
console.log('files', files)
return files return files
}, },

@ -1,9 +0,0 @@
export type branch = {
name: string
remote: remote
}
export type remote = {
name: string
url: string
}

@ -4,7 +4,7 @@ import path from 'path'
export const cacheDir = path.join(os.homedir(), '.cache_remix_ide') export const cacheDir = path.join(os.homedir(), '.cache_remix_ide')
console.log('cacheDir', cacheDir) console.log('cache dir is:', cacheDir)
export const createDefaultConfigLocations = async() => { export const createDefaultConfigLocations = async() => {
try { try {

@ -0,0 +1,51 @@
import { screen } from 'electron';
import { isPackaged, isE2E } from "../main";
var MatomoTracker = require('matomo-tracker');
// Function to send events to Matomo
export function trackEvent(category: string, action: string, name: string, value: string | number, new_visit: number = 0): void {
var matomo = new MatomoTracker(35, 'http://ethereumfoundation.matomo.cloud/matomo.php');
matomo.on('error', function (err: any) {
console.log('error tracking request: ', err);
});
// Customize the user agent
const electronVersion = process.versions.electron;
const chromiumVersion = process.versions.chrome;
const os = process.platform; // 'darwin', 'win32', 'linux', etc.
const osVersion = process.getSystemVersion();
const ua = `Electron/${electronVersion} (Chromium/${chromiumVersion}) ${os} ${osVersion}`;
const res = `${screen.getPrimaryDisplay().size.width}x${screen.getPrimaryDisplay().size.height}`;
if ((process.env.NODE_ENV === 'production' || isPackaged) && !isE2E) {
console.log('trackEvent', category, action, name, value, ua, new_visit);
matomo.track({
e_c: category,
e_a: action,
e_n: name,
e_v: value,
ua,
new_visit,
res,
url: 'https://github.com/remix-project-org/remix-desktop'
// You can add other parameters if needed
}, (error: any) => {
if (error) {
console.error('Error tracking event:', error);
} else {
console.log('Event tracked successfully');
}
});
} else {
console.log('Matomo tracking is disabled');
}
}

@ -0,0 +1,2 @@
{"openedFolders":["/home/bunsen/Documents/remix-reward"],
"recentFolders":["/home/bunsen/Documents/remix-reward"]}

@ -0,0 +1,195 @@
import { spawn, ChildProcess } from "child_process"
export async function getBranches(path: string): Promise<string> {
return new Promise((resolve, reject) => {
const git = spawn('git', ['branch'], { cwd: path })
let branches = ''
git.stdout.on('data', function (data) {
console.log('stdout git branches', data.toString())
branches += data.toString()
})
git.stderr.on('data', function (data) {
console.log('stderr git branches', data.toString())
reject(data.toString())
})
git.on('close', function () {
resolve(branches)
})
})
}
export async function getGitLog(path: string): Promise<string> {
return new Promise((resolve, reject) => {
const git = spawn('git', ['log'], { cwd: path })
let logs = ''
git.stdout.on('data', function (data) {
logs += data.toString()
})
git.stderr.on('err', function (data) {
reject(data.toString())
})
git.on('close', function () {
resolve(logs)
})
})
}
export async function cloneOnServer(repo: string, path: string, name: string = 'bare') {
console.log('cloning', repo, path)
return new Promise((resolve, reject) => {
const git = spawn(`rm -rf ${name} && git`, ['clone', repo], { cwd: path, shell: true, detached: true });
git.stdout.on('data', function (data) {
console.log('stdout data cloning', data.toString());
if (data.toString().includes('done')) {
resolve(git);
}
});
git.stderr.on('data', function (data) {
console.log('stderr data cloning', data.toString());
if (data.toString().includes('into')) {
setTimeout(() => {
resolve(git);
}, 5000)
}
});
git.on('error', (error) => {
reject(`Process error: ${error.message}`);
});
git.on('exit', (code, signal) => {
if (code !== 0) {
reject(`Process exited with code: ${code} and signal: ${signal}`);
}
});
});
}
export async function onLocalGitRepoAddFile(path: string, file: string) {
console.log('adding file', file)
return new Promise((resolve, reject) => {
const git = spawn('touch', [file], { cwd: path });
git.stdout.on('data', function (data) {
console.log('stdout data adding file', data.toString());
if (data.toString().includes('done')) {
resolve(git);
}
});
git.stderr.on('data', function (data) {
console.error('stderr adding file', data.toString());
reject(data.toString());
});
git.on('error', (error) => {
reject(`Process error: ${error.message}`);
});
git.on('exit', (code, signal) => {
if (code !== 0) {
reject(`Process exited with code: ${code} and signal: ${signal}`);
} else {
resolve(git);
}
});
});
}
export async function onLocalGitRepoPush(path: string, branch: string = 'master') {
console.log('pushing', path)
return new Promise((resolve, reject) => {
const git = spawn('git', ['push', 'origin', branch], { cwd: path, shell: true, detached: true });
git.stdout.on('data', function (data) {
console.log('stdout data pushing', data.toString());
if (data.toString().includes('done')) {
resolve(git);
}
});
git.stderr.on('data', function (data) {
console.error('stderr data pushing', data.toString());
if (data.toString().includes(branch)) {
resolve(git);
}
});
git.on('error', (error) => {
reject(`Process error: ${error.message}`);
});
git.on('exit', (code, signal) => {
if (code !== 0) {
reject(`Process exited with code: ${code} and signal: ${signal}`);
} else {
resolve(git);
}
});
});
}
export async function createCommitOnLocalServer(path: string, message: string) {
console.log('committing', message, path)
return new Promise((resolve, reject) => {
const git = spawn('git add . && git', ['commit', '-m', message], { cwd: path, shell: true, detached: true });
git.stdout.on('data', function (data) {
console.log('data stdout committing', data.toString());
if (data.toString().includes(message)) {
setTimeout(() => {
resolve(git);
}, 1000)
}
});
git.stderr.on('data', function (data) {
console.error('data commiting', data.toString());
reject(data.toString());
});
git.on('error', (error) => {
console.error('error', error);
reject(`Process error: ${error.message}`);
});
git.on('exit', (code, signal) => {
if (code !== 0) {
console.error('exit', code, signal);
reject(`Process exited with code: ${code} and signal: ${signal}`);
} else {
resolve(git);
}
});
});
}
export async function spawnGitServer(path: string): Promise<ChildProcess> {
console.log(process.cwd())
try {
const server = spawn('yarn && sh setup.sh && yarn start:server', [`${path}`], { cwd: process.cwd() + '/../remix-ide-e2e/src/githttpbackend/', shell: true, detached: true })
console.log('spawned', server.stdout.closed, server.stderr.closed)
return new Promise((resolve, reject) => {
server.stdout.on('data', function (data) {
console.log(data.toString())
if (
data.toString().includes('is listening')
|| data.toString().includes('address already in use')
) {
console.log('resolving')
resolve(server)
}
})
server.stderr.on('err', function (data) {
console.log(data.toString())
reject(data.toString())
})
})
} catch (e) {
console.log(e)
}
}

@ -0,0 +1,104 @@
import os from 'os';
import fs from 'fs';
const useIsoGit = process.argv.includes('--use-isogit');
const useOffline = process.argv.includes('--use-offline');
// Function to read JSON file synchronously
function readJSONFileSync(filename: string): any {
try {
const data = fs.readFileSync(filename, 'utf8');
return JSON.parse(data);
} catch (err) {
throw err;
}
}
const packageData: any = readJSONFileSync('package.json');
const version = packageData.version;
let channel: string = ''
if (version.includes('beta')) {
channel = 'Beta';
} else if (version.includes('alpha')) {
channel = 'Alpha';
} else if (version.includes('insiders')) {
channel = 'Insiders';
}
// Determine if running on CircleCI or locally with --e2e-local
const isLocalE2E = process.argv.includes('--e2e-local') && !process.env.CIRCLECI;
module.exports = {
src_folders: ['build-e2e/remixdesktop/test/tests/app'],
output_folder: './reports/tests',
custom_commands_path: ['build-e2e/remix-ide-e2e/src/commands'],
page_objects_path: '',
globals_path: '',
test_settings: {
default: {
enable_fail_fast: true,
selenium_port: 4444,
selenium_host: 'localhost',
globals: {
waitForConditionTimeout: 10000,
asyncHookTimeout: 100000
},
screenshots: {
enabled: true,
path: './reports/screenshots',
on_failure: true,
on_error: true
},
webdriver: {
start_process: true,
timeout_options: {
timeout: 60000,
retry_attempts: 3
}
},
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true,
'goog:chromeOptions': (() => {
const type = os.type();
const arch = os.arch();
let binaryPath = "";
// Check if running on CircleCI or locally
let args = process.env.CIRCLECI ? ["--e2e"] : ["--e2e-local"];
if(useIsoGit) args = [...args, '--use-isogit'];
if(useOffline) args = [...args, '--use-offline'];
// Set display size
const windowSize = "--window-size=1000,1000";
args = [...args];
switch (type) {
case 'Windows_NT':
binaryPath = `./release/win-unpacked/Remix-Desktop-${channel}.exe`;
break;
case 'Darwin':
binaryPath = arch === 'x64' ?
`release/mac/Remix-Desktop-${channel}.app/Contents/MacOS/Remix-Desktop-${channel}` :
`release/mac-arm64/Remix-Desktop-${channel}.app/Contents/MacOS/Remix-Desktop-${channel}`;
break;
case 'Linux':
binaryPath = "release/linux-unpacked/remixdesktop";
break;
}
console.log('binaryPath', binaryPath);
return {
binary: binaryPath,
args: args
};
})()
}
}
}
};

@ -0,0 +1,37 @@
import { NightwatchBrowser } from 'nightwatch'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
'download compiler': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('solidity')
.pause(1000)
.setSolidityCompilerVersion('soljson-v0.8.23+commit.f704f362.js')
.waitForElementVisible({
selector: "//*[@data-id='selectedVersion' and contains(.,'0.8.23+commit.f704f362')]",
locateStrategy: 'xpath'
})
.waitForElementContainsText('*[data-id="terminalJournal"]', 'Compiler downloaded from https://binaries.soliditylang.org/wasm/soljson-v0.8.23+commit.f704f362.js to soljson-v0.8.23+commit.f704f362.js', 10000)
.waitForElementPresent({
selector:
"//a[@data-id='dropdown-item-soljson-v0.8.23+commit.f704f362.js']//*[contains(@class, 'fa-arrow-circle-down')]",
locateStrategy: 'xpath'
})
},
'refresh': function (browser: NightwatchBrowser) {
browser.refresh()
.clickLaunchIcon('solidity')
.waitForElementVisible('*[data-id="versionSelector"]')
.click('*[data-id="versionSelector"]')
.waitForElementPresent({
selector:
"//a[@data-id='dropdown-item-soljson-v0.8.23+commit.f704f362.js']//*[contains(@class, 'fa-arrow-circle-down')]",
locateStrategy: 'xpath'
})
}
}

@ -0,0 +1,122 @@
import {NightwatchBrowser} from 'nightwatch'
const testsBash = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
open: function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="openFolderButton"]', 10000).click('*[data-id="openFolderButton"]')
},
'open xterm linux and create a file': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="tabXTerm"]', 10000)
.click('*[data-id="tabXTerm"]')
.waitForElementVisible('*[data-type="remixUIXT"]', 10000)
.click('*[data-type="remixUIXT"]')
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('mkdir dir && cd dir && echo "test" >> example.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemdir"]', 10000)
.openFile('dir')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemdir/example.txt"]', 10000)
.openFile('dir/example.txt')
.getEditorValue((result) => {
browser.assert.equal(result, 'test\n')
})
.waitForElementVisible('*[data-type="remixUIXT"]', 10000)
.click('*[data-type="remixUIXT"]')
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('echo "123" >> example.txt').sendKeys(this.Keys.ENTER)
})
.pause(1000)
.getEditorValue((result) => {
browser.assert.equal(result, 'test\n123\n')
})
.setEditorValue('somethinginthere')
.pause(1000)
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('cat example.txt').sendKeys(this.Keys.ENTER)
})
.pause(1000)
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok((result.value as string).includes('somethinginthere'))
}
)
},
}
const testsWindows = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
open: function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="openFolderButton"]', 10000).click('*[data-id="openFolderButton"]')
},
'open xterm window and create a file': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="tabXTerm"]', 10000)
.click('*[data-id="tabXTerm"]')
.waitForElementVisible('*[data-id="select_shell"]')
.click('*[data-id="select_shell"]')
.waitForElementVisible('*[data-id="select_powershell.exe"]')
.click('*[data-id="select_powershell.exe"]')
.pause(3000)
.waitForElementVisible("[data-active='1'][data-type='remixUIXT']", 10000)
.click("[data-active='1'][data-type='remixUIXT']")
.pause(1000)
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('New-Item -ItemType Directory -Name "dir" ; Set-Location -Path "./dir" ; Add-Content -Path "example.txt" -Value "test" -Encoding UTF8').sendKeys(this.Keys.ENTER)
})
.pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemdir"]', 10000)
.openFile('dir')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemdir/example.txt"]', 10000)
.openFile('dir/example.txt').pause(1000)
.getEditorValue((result) => {
browser.assert.equal(result, 'test\r\n')
})
.pause(1000)
.waitForElementVisible("[data-active='1'][data-type='remixUIXT']", 10000)
.click("[data-active='1'][data-type='remixUIXT']")
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('Add-Content -Path "example.txt" -Value "123" -Encoding UTF8').sendKeys(this.Keys.ENTER)
})
.pause(1000)
.getEditorValue((result) => {
browser.assert.equal(result, 'test\r\n123\r\n')
})
.setEditorValue('somethinginthere')
.pause(1000)
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('Get-Content example.txt').sendKeys(this.Keys.ENTER)
}).pause(1000)
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok((result.value as string).includes('somethinginthere'))
}
)
}
}
module.exports = {
...process.platform.startsWith('win')?testsWindows:testsBash
}

@ -0,0 +1,157 @@
import { NightwatchBrowser } from 'nightwatch'
import { ChildProcess, spawn, execSync } from 'child_process'
import { homedir } from 'os'
import path from 'path'
import os from 'os'
const projectDir = path.join('remix-desktop-test-' + Date.now().toString())
const dir = '/tmp/' + projectDir
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
installFoundry: function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
await downloadFoundry()
await installFoundry()
await initFoundryProject()
done()
})
},
addScript: function (browser: NightwatchBrowser) {
// run script in console
browser.executeAsync(function (dir, done) {
(window as any).electronAPI.openFolderInSameWindow(dir + '/hello_foundry/').then(done)
}, [dir], () => {
console.log('done window opened')
})
.waitForElementVisible('*[data-id="treeViewDivDraggableItemfoundry.toml"]', 10000)
},
compile: function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
console.log('generating compilation result')
await buildFoundryProject()
done()
})
.expect.element('*[data-id="terminalJournal"]').text.to.contain('receiving compilation result from Foundry').before(60000)
let contractAaddress
browser.clickLaunchIcon('filePanel')
.openFile('src')
.openFile('src/Counter.sol')
.clickLaunchIcon('udapp')
.selectContract('Counter')
.createContract('')
.getAddressAtPosition(0, (address) => {
console.log(contractAaddress)
contractAaddress = address
})
.clickInstance(0)
.clickFunction('increment - transact (not payable)')
.perform((done) => {
browser.testConstantFunction(contractAaddress, 'number - call', null, '0:\nuint256: 1').perform(() => {
done()
})
})
}
}
async function downloadFoundry(): Promise<void> {
console.log('downloadFoundry', process.cwd())
try {
const server = spawn('curl -L https://foundry.paradigm.xyz | bash', [], { cwd: process.cwd(), shell: true, detached: true })
return new Promise((resolve, reject) => {
server.stdout.on('data', function (data) {
console.log(data.toString())
if (
data.toString().includes("simply run 'foundryup' to install Foundry")
|| data.toString().includes("foundryup: could not detect shell, manually add")
) {
console.log('resolving')
resolve()
}
})
server.stderr.on('err', function (data) {
console.log(data.toString())
reject(data.toString())
})
})
} catch (e) {
console.log(e)
}
}
async function installFoundry(): Promise<void> {
console.log('installFoundry', process.cwd())
try {
const server = spawn('export PATH="' + homedir() + '/.foundry/bin:$PATH" && foundryup', [], { cwd: process.cwd(), shell: true, detached: true })
return new Promise((resolve, reject) => {
server.stdout.on('data', function (data) {
console.log(data.toString())
if (
data.toString().includes("foundryup: done!")
) {
console.log('resolving')
resolve()
}
})
server.stderr.on('err', function (data) {
console.log(data.toString())
reject(data.toString())
})
})
} catch (e) {
console.log(e)
}
}
async function initFoundryProject(): Promise<void> {
console.log('initFoundryProject', homedir())
try {
if (process.env.CIRCLECI) {
spawn('git config --global user.email \"you@example.com\"', [], { cwd: homedir(), shell: true, detached: true })
spawn('git config --global user.name \"Your Name\"', [], { cwd: homedir(), shell: true, detached: true })
}
spawn('mkdir ' + projectDir, [], { cwd: '/tmp/', shell: true, detached: true })
const server = spawn('export PATH="' + homedir() + '/.foundry/bin:$PATH" && forge init hello_foundry', [], { cwd: dir, shell: true, detached: true })
server.stdout.pipe(process.stdout)
return new Promise((resolve, reject) => {
server.on('exit', function (exitCode) {
console.log("Child exited with code: " + exitCode);
console.log('end')
resolve()
})
server.stderr.on('err', function (data) {
console.log('err', data.toString())
})
server.stdout.on('data', function (data) {
console.log('data', data.toString())
})
})
} catch (e) {
console.log(e)
}
}
async function buildFoundryProject(): Promise<void> {
console.log('buildFoundryProject', homedir())
try {
const server = spawn('export PATH="' + homedir() + '/.foundry/bin:$PATH" && forge build', [], { cwd: dir + '/hello_foundry', shell: true, detached: true })
server.stdout.pipe(process.stdout)
return new Promise((resolve, reject) => {
server.on('exit', function (exitCode) {
console.log("Child exited with code: " + exitCode);
console.log('end')
resolve()
})
})
} catch (e) {
console.log(e)
}
}
module.exports = {
...{}//...process.platform.startsWith('linux') ? tests : {}
}

@ -0,0 +1,36 @@
import { NightwatchBrowser } from 'nightwatch'
const gist_id = '02a847917a6a7ecaf4a7e0d4e68715bf'
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
'start gist': function (browser: NightwatchBrowser) {
browser.end()
/*
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('*[data-id="landingPageImportFromGist"]')
.click('*[data-id="landingPageImportFromGist"]')
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]')
.execute(function () {
(document.querySelector('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]') as any).focus()
})
.setValue('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]', gist_id)
.modalFooterOKClick('gisthandler')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewDivtreeViewItemREADME.txt"]')
})
.click('[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/3_Ballot.sol')
.end()
*/
}
}
module.exports = {
...tests
}

@ -0,0 +1,203 @@
import { ChildProcess, spawn } from "child_process"
import kill from 'tree-kill'
import { Nightwatch, NightwatchBrowser } from "nightwatch"
import { spawnGitServer, getGitLog, cloneOnServer, onLocalGitRepoAddFile, createCommitOnLocalServer, onLocalGitRepoPush, getBranches } from "../../lib/git"
let gitserver: ChildProcess
/*
/ uses the git-http-backend package to create a git server ( if needed kill the server: kill -9 $(sudo lsof -t -i:6868) )
/ GROUP 1: file operations PUSH PULL COMMIT SYNC FETCH CLONE ADD
/ GROUP 2: branch operations CREATE & PUBLISH
/ GROUP 3: file operations rename delete
*/
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
browser.hideToolTips()
done()
},
after: function (browser: NightwatchBrowser) {
browser.perform((done) => {
console.log('kill server', gitserver.pid)
kill(gitserver.pid)
done()
})
},
'run server #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
gitserver = await spawnGitServer('/tmp/')
console.log('working directory', process.cwd())
done()
})
},
'clone a repo #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.pause(5000)
.waitForElementVisible('*[data-id="cloneButton"]')
.click('*[data-id="cloneButton"]')
.pause(1000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalBody-react"]')
.click('[data-id="fileSystemModalDialogModalBody-react"]')
.waitForElementVisible('[data-id="modalDialogCustomPromptTextClone"]')
.setValue('*[data-id="modalDialogCustomPromptTextClone"]', 'http://localhost:6868/bare.git')
.click('[data-id="fileSystem-modal-footer-ok-react"]')
.pause(5000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.git"]')
.hideToolTips()
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemREADME.md"]')
},
'Update settings for git #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]')
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]')
.modalFooterOKClick('github-credentials-error')
},
// GROUP 1
'check file added #group1 #group3': function (browser: NightwatchBrowser) {
browser
.addFile('test.txt', { content: 'hello world' }, 'README.md')
.clickLaunchIcon('dgit')
.pause(1000)
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.pause(1000)
.click('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='added-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.setValue('*[data-id="commitMessage"]', 'testcommit')
.click('*[data-id="commitButton"]')
},
'look at the commit #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit-ahead"]',
locateStrategy: 'xpath'
})
},
'sync the commit #group1': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="sourcecontrol-panel"]')
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible('*[data-id="syncButton"]')
.click('*[data-id="syncButton"]')
.pause(2000)
.waitForElementVisible('*[data-id="commitButton"]')
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit-"]',
locateStrategy: 'xpath'
})
},
'check the log #group1': async function (browser: NightwatchBrowser) {
const logs = await getGitLog('/tmp/git/bare.git')
console.log(logs)
browser.assert.ok(logs.includes('testcommit'))
},
'change a file #group1': function (browser: NightwatchBrowser) {
browser.
openFile('test.txt').
pause(1000).
setEditorValue('changes', null)
},
'stage changed file #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible({
selector: "//*[@data-status='modified-unstaged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.click('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='modified-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.setValue('*[data-id="commitMessage"]', 'testcommit2')
.click('*[data-id="commitButton"]')
},
'push the commit #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commands-panel"]')
.waitForElementVisible('*[data-id="sourcecontrol-push"]')
.click('*[data-id="sourcecontrol-push"]')
.pause(2000)
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit2-"]',
locateStrategy: 'xpath'
}).pause(2000)
},
'check the log for testcommit2 #group1': async function (browser: NightwatchBrowser) {
const logs = await getGitLog('/tmp/git/bare.git')
console.log(logs)
browser.assert.ok(logs.includes('testcommit2'))
},
'clone locally and add a file and push #group1': async function (browser: NightwatchBrowser) {
await cloneOnServer('http://localhost:6868/bare.git', '/tmp/')
await onLocalGitRepoAddFile('/tmp/bare/', 'test2.txt')
await createCommitOnLocalServer('/tmp/bare/', 'testlocal')
await onLocalGitRepoPush('/tmp/bare/', 'master')
},
'run a git fetch #group1': function (browser: NightwatchBrowser) {
browser
.pause(2000)
.click('*[data-id="commands-panel"]')
.waitForElementVisible('*[data-id="sourcecontrol-fetch-branch"]')
.click('*[data-id="sourcecontrol-fetch-branch"]')
.pause(2000)
.click('*[data-id="commits-panel"]')
.waitForElementVisible('*[data-id="commits-panel-behind"]')
.click('*[data-id="commits-panel-behind"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testlocal-"]',
locateStrategy: 'xpath'
})
},
'run pull from the header #group1': function (browser: NightwatchBrowser) {
browser.
click('*[data-id="sourcecontrol-button-pull"]')
.waitForElementNotPresent('*[data-id="commits-panel-behind"]')
},
'check if the file is added #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest2.txt"]')
},
}
const useIsoGit = process.argv.includes('--use-isogit');
if (process.platform.startsWith('win')) {
module.exports = {}
}
else
module.exports = { ...tests }

@ -0,0 +1,181 @@
import { ChildProcess, spawn } from "child_process"
import kill from 'tree-kill'
import { Nightwatch, NightwatchBrowser } from "nightwatch"
import { spawnGitServer, getGitLog, cloneOnServer, onLocalGitRepoAddFile, createCommitOnLocalServer, onLocalGitRepoPush, getBranches } from "../../lib/git"
let gitserver: ChildProcess
/*
/ uses the git-http-backend package to create a git server ( if needed kill the server: kill -9 $(sudo lsof -t -i:6868) )
/ GROUP 1: file operations PUSH PULL COMMIT SYNC FETCH CLONE ADD
/ GROUP 2: branch operations CREATE & PUBLISH
/ GROUP 3: file operations rename delete
*/
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
browser.hideToolTips()
done()
},
after: function (browser: NightwatchBrowser) {
browser.perform((done) => {
console.log('kill server', gitserver.pid)
kill(gitserver.pid)
done()
})
},
'run server #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
gitserver = await spawnGitServer('/tmp/')
console.log('working directory', process.cwd())
done()
})
},
'clone a repo #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.pause(5000)
.waitForElementVisible('*[data-id="cloneButton"]')
.click('*[data-id="cloneButton"]')
.pause(1000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalBody-react"]')
.click('[data-id="fileSystemModalDialogModalBody-react"]')
.waitForElementVisible('[data-id="modalDialogCustomPromptTextClone"]')
.setValue('*[data-id="modalDialogCustomPromptTextClone"]', 'http://localhost:6868/bare.git')
.click('[data-id="fileSystem-modal-footer-ok-react"]')
.pause(5000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.git"]')
.hideToolTips()
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemREADME.md"]')
},
'Update settings for git #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]')
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]')
.modalFooterOKClick('github-credentials-error')
},
// GROUP 2
'create a branch #group2': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="branches-panel"]')
.waitForElementVisible('*[data-id="newbranchname"]')
.setValue('*[data-id="newbranchname"]', 'testbranch')
.click('*[data-id="sourcecontrol-create-branch"]')
.waitForElementVisible('*[data-id="branches-current-branch-testbranch"]')
.pause(1000)
},
'check if the branch is in the filePanel #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.pause(1000)
.waitForElementVisible('*[data-id="workspaceGitBranchesDropdown"]')
.click('[data-id="workspaceGitBranchesDropdown"]')
.waitForElementVisible('*[data-id="workspaceGit-testbranch"]')
.expect.element('[data-id="workspaceGit-testbranch"]').text.to.contain('✓ ')
},
'publish the branch #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="sourcecontrol-panel"]')
.click('*[data-id="sourcecontrol-panel"]')
.pause(1000)
.click('*[data-id="publishBranchButton"]')
.pause(2000)
.waitForElementNotVisible('*[data-id="publishBranchButton"]')
},
'check if the branch is published #group2': async function (browser: NightwatchBrowser) {
const branches = await getBranches('/tmp/git/bare.git')
browser.assert.ok(branches.includes('testbranch'))
},
'add file to new branch #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.addFile('test.txt', { content: 'hello world' }, 'README.md')
.clickLaunchIcon('dgit')
.pause(2000)
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.pause(1000)
.click('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='added-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.setValue('*[data-id="commitMessage"]', 'testcommit')
.click('*[data-id="commitButton"]')
.pause(1000)
},
'check if the commit is ahead in the branches list #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="branches-panel"]')
.click('*[data-id="branches-panel"]')
.waitForElementVisible('*[data-id="branches-current-branch-testbranch"]')
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-current-branch-testbranch']",
locateStrategy: 'xpath',
suppressNotFoundErrors: true
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='commits-panel-ahead']",
locateStrategy: 'xpath',
suppressNotFoundErrors: true
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branchdifference-commits-testbranch-ahead']//*[@data-id='commit-summary-testcommit-ahead']",
locateStrategy: 'xpath',
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branchdifference-commits-testbranch-ahead']//*[@data-id='commit-change-added-test.txt']",
locateStrategy: 'xpath',
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='local-branch-commits-testbranch']//*[@data-id='commit-summary-testcommit-ahead']",
locateStrategy: 'xpath',
})
.waitForElementVisible({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='local-branch-commits-testbranch']//*[@data-id='commit-change-added-test.txt']",
locateStrategy: 'xpath',
})
},
'switch back to master #group2': function (browser: NightwatchBrowser) {
browser
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-toggle-branch-master']",
locateStrategy: 'xpath',
})
.waitForElementVisible({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-toggle-current-branch-master']",
locateStrategy: 'xpath',
})
},
'check if test file is gone #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.txt"]')
}
}
const useIsoGit = process.argv.includes('--useIsoGit');
if (process.platform.startsWith('win')) {
module.exports = {}
}
else
module.exports = { ...tests }

@ -0,0 +1,153 @@
import { ChildProcess, spawn } from "child_process"
import kill from 'tree-kill'
import { Nightwatch, NightwatchBrowser } from "nightwatch"
import { spawnGitServer, getGitLog, cloneOnServer, onLocalGitRepoAddFile, createCommitOnLocalServer, onLocalGitRepoPush, getBranches } from "../../lib/git"
let gitserver: ChildProcess
/*
/ uses the git-http-backend package to create a git server ( if needed kill the server: kill -9 $(sudo lsof -t -i:6868) )
/ GROUP 1: file operations PUSH PULL COMMIT SYNC FETCH CLONE ADD
/ GROUP 2: branch operations CREATE & PUBLISH
/ GROUP 3: file operations rename delete
*/
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
browser.hideToolTips()
done()
},
after: function (browser: NightwatchBrowser) {
browser.perform((done) => {
console.log('kill server', gitserver.pid)
kill(gitserver.pid)
done()
})
},
'run server #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
gitserver = await spawnGitServer('/tmp/')
console.log('working directory', process.cwd())
done()
})
},
'clone a repo #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.pause(5000)
.waitForElementVisible('*[data-id="cloneButton"]')
.click('*[data-id="cloneButton"]')
.pause(1000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalBody-react"]')
.click('[data-id="fileSystemModalDialogModalBody-react"]')
.waitForElementVisible('[data-id="modalDialogCustomPromptTextClone"]')
.setValue('*[data-id="modalDialogCustomPromptTextClone"]', 'http://localhost:6868/bare.git')
.click('[data-id="fileSystem-modal-footer-ok-react"]')
.pause(5000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.git"]')
.hideToolTips()
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemREADME.md"]')
},
'Update settings for git #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.saveScreenshot('./reports/screenshots/gitui.png')
.waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.saveScreenshot('./reports/screenshots/gitui2.png')
.pause(1000)
.saveScreenshot('./reports/screenshots/gitui3.png')
.click('*[data-id="github-panel"]')
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]')
.pause(1000)
.modalFooterOKClick('github-credentials-error')
},
'check file added #group1 #group3': function (browser: NightwatchBrowser) {
browser
.addFile('test.txt', { content: 'hello world' }, 'README.md')
.clickLaunchIcon('dgit')
.pause(1000)
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.pause(1000)
.click('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='added-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.setValue('*[data-id="commitMessage"]', 'testcommit')
.click('*[data-id="commitButton"]')
},
// group 3
'rename a file #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.txt"]')
.click('*[data-id="treeViewLitreeViewItemtest.txt"]')
.renamePath('test.txt', 'test_rename', 'test_rename.txt')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest_rename.txt"]')
.pause(1000)
},
'stage renamed file #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.waitForElementVisible({
selector: "//*[@data-status='deleted-unstaged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/test_rename.txt']",
locateStrategy: 'xpath'
})
.pause(2000)
.click('*[data-id="sourcecontrol-add-all"]')
.waitForElementVisible({
selector: "//*[@data-status='added-staged' and @data-file='/test_rename.txt']",
locateStrategy: 'xpath'
})
},
'undo the rename #group3': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="unStageStagedtest_rename.txt"]')
.click('*[data-id="unStageStagedtest_rename.txt"]')
.pause(1000)
.click('*[data-id="unDoStagedtest.txt"]')
.pause(1000)
.waitForElementNotPresent({
selector: "//*[@data-file='/test.txt']",
locateStrategy: 'xpath'
})
},
'check if file is returned #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.txt"]')
},
}
const useIsoGit = process.argv.includes('--use-isogit');
if (process.platform.startsWith('win')) {
module.exports = {}
}
else
module.exports = { ...tests }

@ -0,0 +1,200 @@
import { ChildProcess, spawn } from "child_process"
import kill from 'tree-kill'
import { Nightwatch, NightwatchBrowser } from "nightwatch"
import { spawnGitServer, getGitLog, cloneOnServer, onLocalGitRepoAddFile, createCommitOnLocalServer, onLocalGitRepoPush, getBranches } from "../../lib/git"
let gitserver: ChildProcess
/*
/ uses the git-http-backend package to create a git server ( if needed kill the server: kill -9 $(sudo lsof -t -i:6868) )
/ GROUP 1: file operations PUSH PULL COMMIT SYNC FETCH CLONE ADD
/ GROUP 2: branch operations CREATE & PUBLISH
/ GROUP 3: file operations rename delete
*/
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
browser.hideToolTips()
done()
},
after: function (browser: NightwatchBrowser) {
browser.perform((done) => {
console.log('kill server', gitserver.pid)
kill(gitserver.pid)
done()
})
},
'run server #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
gitserver = await spawnGitServer('/tmp/')
console.log('working directory', process.cwd())
done()
})
},
'clone a repo #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.pause(5000)
.waitForElementVisible('*[data-id="cloneButton"]')
.click('*[data-id="cloneButton"]')
.pause(1000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalBody-react"]')
.click('[data-id="fileSystemModalDialogModalBody-react"]')
.waitForElementVisible('[data-id="modalDialogCustomPromptTextClone"]')
.setValue('*[data-id="modalDialogCustomPromptTextClone"]', 'http://localhost:6868/bare.git')
.click('[data-id="fileSystem-modal-footer-ok-react"]')
.pause(5000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.git"]')
.hideToolTips()
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemREADME.md"]')
},
'Update settings for git #group1 #group2 #group3': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]')
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]')
.pause(1000)
.modalFooterOKClick('github-credentials-error')
},
// GROUP 1
'check file added #group1 #group3': function (browser: NightwatchBrowser) {
browser
.addFile('test.txt', { content: 'hello world' }, 'README.md')
.clickLaunchIcon('dgit')
.pause(1000)
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible({
selector: "//*[@data-status='new-untracked' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.waitForElementVisible('*[data-id="addToGitChangestest.txt"]')
.pause(1000)
.click('*[data-id="addToGitChangestest.txt"]')
.waitForElementVisible({
selector: "//*[@data-status='added-staged' and @data-file='/test.txt']",
locateStrategy: 'xpath'
})
.setValue('*[data-id="commitMessage"]', 'testcommit')
.click('*[data-id="commitButton"]')
},
'look at the commit #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit-ahead"]',
locateStrategy: 'xpath'
})
},
'add second remote #group4': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.click('*[data-id="remotes-panel"]')
.waitForElementVisible('*[data-id="add-manual-remoteurl"]')
.setValue('*[data-id="add-manual-remoteurl"]', 'http://localhost:6868/bare2.git')
.waitForElementVisible('*[data-id="add-manual-remotename"]')
.setValue('*[data-id="add-manual-remotename"]', 'origin2')
.waitForElementVisible('*[data-id="add-manual-remotebtn"]')
.click('*[data-id="add-manual-remotebtn"]')
},
'check the buttons #group4': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="default-remote-check-origin"]')
.waitForElementVisible('*[data-id="set-as-default-origin2"]')
},
'check the commands #group4': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'origin')]",
locateStrategy: 'xpath'
})
},
'switch to origin2 #group4': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="remotes-panel"]')
.waitForElementVisible('*[data-id="set-as-default-origin2"]')
.click('*[data-id="set-as-default-origin2"]')
},
'check the commands for origin2 #group4': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'origin2')]",
locateStrategy: 'xpath'
})
},
'sync the commit #group4': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="sourcecontrol-panel"]')
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible('*[data-id="syncButton"]')
.click('*[data-id="syncButton"]')
.waitForElementVisible('*[data-id="commitButton"]')
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit-"]',
locateStrategy: 'xpath'
})
},
'check the log #group4': async function (browser: NightwatchBrowser) {
const logs = await getGitLog('/tmp/git/bare2.git')
console.log(logs)
browser.assert.ok(logs.includes('testcommit'))
const logs2 = await getGitLog('/tmp/git/bare.git')
console.log(logs2)
console.log(logs2.includes('testcommit3'))
browser.assert.ok(logs2.includes('testcommit3'))
},
'switch to origin #group4': function (browser: NightwatchBrowser) {
browser
.pause(5000)
.click('*[data-id="remotes-panel"]')
.waitForElementVisible('*[data-id="set-as-default-origin"]')
.pause(1000)
.click('*[data-id="set-as-default-origin"]')
},
'check the commands for origin #group4': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'origin')]",
locateStrategy: 'xpath'
})
},
'check the commit ahead #group4': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="sourcecontrol-panel"]')
.click('*[data-id="sourcecontrol-panel"]')
.waitForElementVisible('*[data-id="syncButton"]')
// do not sync
.click('*[data-id="commits-panel"]')
.waitForElementPresent({
selector: '//*[@data-id="commit-summary-testcommit-ahead"]',
locateStrategy: 'xpath'
})
},
}
const useIsoGit = process.argv.includes('--use-isogit');
if (process.platform.startsWith('win')) {
module.exports = {}
}
else
module.exports = { ...tests }

@ -0,0 +1,28 @@
import { NightwatchBrowser } from 'nightwatch'
module.exports = {
'@isogit': true,
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
'clone a repo': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromGit"]')
.click('button[data-id="landingPageImportFromGit"]')
.pause(1000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalBody-react"]')
.click('[data-id="fileSystemModalDialogModalBody-react"]')
.waitForElementVisible('[data-id="modalDialogCustomPromptTextClone"]')
.setValue('[data-id="modalDialogCustomPromptTextClone"]', 'https://github.com/ethereum/awesome-remix')
.click('[data-id="fileSystem-modal-footer-ok-react"]')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.git"]')
})
.end()
}
}

@ -0,0 +1,255 @@
import { NightwatchBrowser } from "nightwatch"
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
browser.hideToolTips()
done()
},
'open default template': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromTemplate"]')
.click('button[data-id="landingPageImportFromTemplate"]')
.waitForElementPresent('*[data-id="create-remixDefault"]')
.scrollAndClick('*[data-id="create-remixDefault"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementPresent('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.click('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
})
},
'Update settings for git #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.pause(1000)
.waitForElementVisible('*[data-id="initgit-btn"]')
.click('*[data-id="initgit-btn"]')
.waitForElementNotPresent('*[data-id="initgit-btn"]')
},
'launch github login via FE #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.pause(1000)
.waitForElementVisible('*[data-id="filepanel-login-github"]')
.click('*[data-id="filepanel-login-github"]')
},
'login to github #group1 #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="github-panel"]')
.waitForElementVisible('*[data-id="gitubUsername"]')
.setValue('*[data-id="githubToken"]', process.env.dgit_token)
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]')
},
'check if the settings are loaded #group1 #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="connected-as-bunsenstraat"]')
.waitForElementVisible('*[data-id="connected-img-bunsenstraat"]')
.waitForElementVisible('*[data-id="connected-link-bunsenstraat"]')
.waitForElementVisible('*[data-id="remotes-panel"]')
},
'check the FE for the auth user #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="filepanel-connected-img-bunsenstraat"]')
},
'clone a repository #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.click('*[data-id="clone-panel"]')
.click({
selector: '//*[@data-id="clone-panel-content"]//*[@data-id="fetch-repositories"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="clone-panel-content"]//*[@id="repository-select"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[@id="repository-select"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="clone-panel-content"]//*[contains(text(), "awesome-remix")]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[contains(text(), "awesome-remix")]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="clone-panel-content"]//*[@id="branch-select"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[@id="branch-select"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[contains(text(), "master")]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="clone-panel-content"]//*[@data-id="clonebtn-ethereum/awesome-remix-master"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="clone-panel-content"]//*[@data-id="clonebtn-ethereum/awesome-remix-master"]',
locateStrategy: 'xpath'
})
.pause(5000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[2])
.pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.git"]')
})
},
'check if there is a README.md file #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="treeViewLitreeViewItemREADME.md"]')
},
'check the commands panel #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'master')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'origin')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-local-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'master')]",
locateStrategy: 'xpath'
})
},
'check the remotes #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="remotes-panel"]')
.waitForElementVisible('*[data-id="remotes-panel-content"]')
.pause(2000)
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-detail-origin-default"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-current-branch-master"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-sync-origin"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-branch-links"]',
locateStrategy: 'xpath',
timeout: 10000
})
},
'check the commits of branch links #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-branch-links"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="branches-branch-links"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="commit-summary-linking fixed-"]',
locateStrategy: 'xpath'
})
},
'switch to branch links #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="branches-panel"]')
.waitForElementVisible({
selector: '//*[@data-id="branches-panel-content-remote-branches"]//*[@data-id="branches-branch-links"]',
locateStrategy: 'xpath'
})
.pause(1000)
.click({
selector: '//*[@data-id="branches-panel-content-remote-branches"]//*[@data-id="branches-toggle-branch-links"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="branches-panel-content-remote-branches"]//*[@data-id="branches-toggle-current-branch-links"]',
locateStrategy: 'xpath'
})
},
'check the local branches #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible({
selector: '//*[@data-id="branches-panel-content-local-branches"]//*[@data-id="branches-toggle-current-branch-links"]',
locateStrategy: 'xpath'
})
},
'check the local commits #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commits-panel"]')
.pause(1000)
.waitForElementVisible({
selector: '//*[@data-id="commits-current-branch-links"]//*[@data-id="commit-summary-linking fixed-"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="commits-current-branch-links"]//*[@data-id="commit-summary-linking fixed-"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="commits-current-branch-links"]//*[@data-id="commit-change-modified-README.md"]',
locateStrategy: 'xpath'
})
},
'check the commands panel for links #group1': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'links')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'origin')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-local-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'links')]",
locateStrategy: 'xpath'
})
},
'disconnect github #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]')
.waitForElementVisible('*[data-id="disconnect-github"]')
.pause(1000)
.click('*[data-id="disconnect-github"]')
.waitForElementNotPresent('*[data-id="connected-as-bunsenstraat"]')
},
'check the FE for the disconnected auth user #group1': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementNotPresent('*[data-id="filepanel-connected-img-bunsenstraat"]')
.waitForElementVisible('*[data-id="filepanel-login-github"]')
},
}
module.exports = tests

@ -0,0 +1,190 @@
import { NightwatchBrowser } from "nightwatch"
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
browser.hideToolTips()
done()
},
'open default template': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromTemplate"]')
.click('button[data-id="landingPageImportFromTemplate"]')
.waitForElementPresent('*[data-id="create-remixDefault"]')
.scrollAndClick('*[data-id="create-remixDefault"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementPresent('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.click('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
})
},
'Update settings for git #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.pause(1000)
.waitForElementVisible('*[data-id="initgit-btn"]')
.click('*[data-id="initgit-btn"]')
.waitForElementNotPresent('*[data-id="initgit-btn"]')
},
'launch github login via FE #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.pause(1000)
.waitForElementVisible('*[data-id="filepanel-login-github"]')
.click('*[data-id="filepanel-login-github"]')
},
'login to github #group1 #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="github-panel"]')
.waitForElementVisible('*[data-id="gitubUsername"]')
.setValue('*[data-id="githubToken"]', process.env.dgit_token)
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]')
},
'check if the settings are loaded #group1 #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="connected-as-bunsenstraat"]')
.waitForElementVisible('*[data-id="connected-img-bunsenstraat"]')
.waitForElementVisible('*[data-id="connected-link-bunsenstraat"]')
.waitForElementVisible('*[data-id="remotes-panel"]')
},
'check the FE for the auth user #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="filepanel-connected-img-bunsenstraat"]')
},
'add a remote #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="remotes-panel"]')
.click('*[data-id="remotes-panel"]')
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="fetch-repositories"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@id="repository-select"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@id="repository-select"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[contains(text(), "awesome-remix")]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[contains(text(), "awesome-remix")]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-panel-remotename"]',
locateStrategy: 'xpath'
})
.setValue({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-panel-remotename"]',
locateStrategy: 'xpath'
}, 'newremote')
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-panel-addremote"]',
locateStrategy: 'xpath'
})
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-panel-addremote"]',
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-detail-newremote-default"]',
locateStrategy: 'xpath'
})
},
'check the commands panel for newremote #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'main')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'newremote')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-local-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'main')]",
locateStrategy: 'xpath'
})
.pause(1000)
.getAttribute({
selector: '//*[@data-id="sourcecontrol-pull"]',
locateStrategy: 'xpath'
}, 'disabled', (result) => {
if (result.value) {
browser.assert.fail('Button is disabled')
} else {
browser.assert.ok(true)
}
})
},
'remove the remote #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.click('*[data-id="remotes-panel"]')
.waitForElementVisible({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-rm-newremote"]',
locateStrategy: 'xpath'
})
.pause(2000)
.click({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-rm-newremote"]',
locateStrategy: 'xpath'
})
.pause(1000)
.waitForElementNotPresent({
selector: '//*[@data-id="remotes-panel-content"]//*[@data-id="remote-detail-newremote-default"]',
locateStrategy: 'xpath'
})
},
'check the commands panel for removed remote #group2': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.click('*[data-id="commands-panel"]')
.waitForElementVisible({
selector: "//div[@id='commands-remote-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'main')]",
locateStrategy: 'xpath'
})
.waitForElementNotPresent({
selector: "//div[@id='commands-remote-origin-select']//div[contains(@class, 'singleValue') and contains(text(), 'newremote')]",
locateStrategy: 'xpath'
})
.waitForElementVisible({
selector: "//div[@id='commands-local-branch-select']//div[contains(@class, 'singleValue') and contains(text(), 'main')]",
locateStrategy: 'xpath'
})
.getAttribute({
selector: '//*[@data-id="sourcecontrol-pull"]',
locateStrategy: 'xpath'
}, 'disabled', (result) => {
if (result.value) {
browser.assert.ok(true)
} else {
browser.assert.fail('Button is not disabled')
}
})
},
}
module.exports = tests

@ -0,0 +1,180 @@
import { NightwatchBrowser } from "nightwatch"
const useIsoGit = process.argv.includes('--use-isogit');
let commitCount = 0
let branchCount = 0
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
browser.hideToolTips()
done()
},
'open default template': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromTemplate"]')
.click('button[data-id="landingPageImportFromTemplate"]')
.waitForElementPresent('*[data-id="create-remixDefault"]')
.scrollAndClick('*[data-id="create-remixDefault"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementPresent('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.click('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
})
},
'Update settings for git #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.pause(1000)
.waitForElementVisible('*[data-id="initgit-btn"]')
.click('*[data-id="initgit-btn"]')
.waitForElementNotPresent('*[data-id="initgit-btn"]')
},
'launch github login via FE #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.pause(1000)
.waitForElementVisible('*[data-id="filepanel-login-github"]')
.click('*[data-id="filepanel-login-github"]')
},
'login to github #group1 #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="github-panel"]')
.waitForElementVisible('*[data-id="gitubUsername"]')
.setValue('*[data-id="githubToken"]', process.env.dgit_token)
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]')
},
'check if the settings are loaded #group1 #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="connected-as-bunsenstraat"]')
.waitForElementVisible('*[data-id="connected-img-bunsenstraat"]')
.waitForElementVisible('*[data-id="connected-link-bunsenstraat"]')
.waitForElementVisible('*[data-id="remotes-panel"]')
},
'check the FE for the auth user #group1 #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="filepanel-connected-img-bunsenstraat"]')
},
// pagination test
'clone repo #group3': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="clone-panel"]')
.click('*[data-id="clone-panel"]')
.waitForElementVisible('*[data-id="clone-url"]')
.setValue('*[data-id="clone-url"]', 'https://github.com/yann300/remix-reward')
.waitForElementVisible('*[data-id="clone-branch"]')
.setValue('*[data-id="clone-branch"]', 'master')
.waitForElementVisible('*[data-id="clone-btn"]')
.click('*[data-id="clone-btn"]')
.clickLaunchIcon('filePanel')
.pause(5000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[2])
.pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem.git"]')
})
},
'Update settings for git #group3': function (browser: NightwatchBrowser) {
browser.
clickLaunchIcon('dgit')
.waitForElementVisible('*[data-id="github-panel"]')
.pause(1000)
.click('*[data-id="github-panel"]')
.pause(1000)
.setValue('*[data-id="githubToken"]', 'invalidtoken')
.pause(1000)
.setValue('*[data-id="gitubUsername"]', 'git')
.pause(1000)
.setValue('*[data-id="githubEmail"]', 'git@example.com')
.pause(1000)
.click('*[data-id="saveGitHubCredentials"]')
.pause(1000)
.modalFooterOKClick('github-credentials-error')
},
'check the commits panel for pagination #group3': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="commits-panel"]')
.click('*[data-id="commits-panel"]')
.elements('xpath', '//*[@data-id="commits-current-branch-master"]//*[@data-type="commit-summary"]', function (result) {
console.log('Number of commit-summary elements:', (result.value as any).length);
if (useIsoGit) {
commitCount = (result.value as any).length
browser.assert.ok((result.value as any).length == 1)
} else {
commitCount = (result.value as any).length
browser.assert.ok((result.value as any).length > 2)
}
})
},
'load more commits #group3': function (browser: NightwatchBrowser) {
console.log('commitCount:', commitCount)
browser
.waitForElementVisible('*[data-id="load-more-commits"]')
.click('*[data-id="load-more-commits"]')
.waitForElementVisible('*[data-id="loader-indicator"]')
.waitForElementNotPresent('*[data-id="loader-indicator"]')
.pause(2000)
.elements('xpath', '//*[@data-id="commits-current-branch-master"]//*[@data-type="commit-summary"]', function (result) {
console.log('Number of commit-summary elements:', (result.value as any).length);
browser.assert.ok((result.value as any).length > commitCount)
})
},
'load more branches from remote #group3': function (browser: NightwatchBrowser) {
browser
.click('*[data-id="branches-panel"]')
.waitForElementVisible({
selector: '//*[@data-id="branches-panel-content-remote-branches"]',
locateStrategy: 'xpath'
})
.elements('xpath', '//*[@data-id="branches-panel-content-remote-branches"]//*[@data-type="branches-branch"]', function (result) {
console.log('Number of branches elements:', (result.value as any).length);
if (useIsoGit) {
branchCount = (result.value as any).length
browser.assert.ok((result.value as any).length == 1)
} else {
branchCount = (result.value as any).length
browser.assert.ok((result.value as any).length > 2)
}
})
if (useIsoGit) {
browser.waitForElementVisible('*[data-id="remote-sync-origin"]')
.click('*[data-id="remote-sync-origin"]')
.waitForElementVisible('*[data-id="loader-indicator"]')
.waitForElementNotPresent('*[data-id="loader-indicator"]')
.pause(2000)
.elements('xpath', '//*[@data-id="branches-panel-content-remote-branches"]//*[@data-type="branches-branch"]', function (result) {
console.log('Number of branches elements:', (result.value as any).length);
browser.assert.ok((result.value as any).length > branchCount)
})
} else {
browser.waitForElementVisible('*[data-id="show-more-branches-on-remote"]')
.click('*[data-id="show-more-branches-on-remote"]')
.pause(1000)
.elements('xpath', '//*[@data-id="branches-panel-content-remote-branches"]//*[@data-type="branches-branch"]', function (result) {
console.log('Number of branches elements:', (result.value as any).length);
browser.assert.ok((result.value as any).length > branchCount)
})
}
}
}
module.exports = tests

@ -0,0 +1,90 @@
import { NightwatchBrowser } from 'nightwatch'
import { ChildProcess, spawn, execSync } from 'child_process'
import { homedir } from 'os'
import path from 'path'
import os from 'os'
const dir = path.join('remix-desktop-test-' + Date.now().toString())
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
setuphardhat: function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
await setupHardhatProject()
done()
})
},
addScript: function (browser: NightwatchBrowser) {
// run script in console
browser.executeAsync(function (dir, done) {
(window as any).electronAPI.openFolderInSameWindow('/tmp/' + dir).then(done)
}, [dir], () => {
console.log('done window opened')
})
.waitForElementVisible('*[data-id="treeViewDivDraggableItemhardhat.config.js"]', 10000)
},
compile: function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
console.log('generating compilation result')
await compileHardhatProject()
done()
})
.expect.element('*[data-id="terminalJournal"]').text.to.contain('receiving compilation result from Hardhat').before(60000)
let addressRef
browser.clickLaunchIcon('filePanel')
.openFile('contracts')
.openFile('contracts/Token.sol')
.clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c')
.selectContract('Token')
.createContract('')
.clickInstance(0)
.clickFunction('balanceOf - call', { types: 'address account', values: '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c' })
.getAddressAtPosition(0, (address) => {
addressRef = address
})
.perform((done) => {
browser.verifyCallReturnValue(addressRef, ['0:uint256: 1000000'])
.perform(() => done())
})
}
}
async function compileHardhatProject(): Promise<void> {
console.log(process.cwd())
try {
const server = spawn('npx hardhat compile', [], { cwd: '/tmp/' + dir, shell: true, detached: true })
return new Promise((resolve, reject) => {
server.on('exit', function (exitCode) {
console.log("Child exited with code: " + exitCode);
console.log('end')
resolve()
})
})
} catch (e) {
console.log(e)
}
}
async function setupHardhatProject(): Promise<void> {
console.log('setup hardhat project', dir)
try {
const server = spawn(`git clone https://github.com/NomicFoundation/hardhat-boilerplate ${dir} && cd ${dir} && yarn install && yarn add "@typechain/ethers-v5@^10.1.0" && yarn add "@typechain/hardhat@^6.1.2" && yarn add "typechain@^8.1.0" && echo "END"`, [], { cwd: '/tmp/', shell: true, detached: true })
return new Promise((resolve, reject) => {
server.on('exit', function (exitCode) {
console.log("Child exited with code: " + exitCode);
console.log('end')
resolve()
})
})
} catch (e) {
console.log(e)
}
}
module.exports = {
...tests
}

@ -0,0 +1,48 @@
import { NightwatchBrowser } from 'nightwatch'
module.exports = {
'@offline': true,
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
'open default template': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromTemplate"]')
.click('button[data-id="landingPageImportFromTemplate"]')
.waitForElementPresent('*[data-id="create-remixDefault"]')
.scrollAndClick('*[data-id="create-remixDefault"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementPresent('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.click('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.click('*[data-id="treeViewLitreeViewItemtests"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]')
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('*[id="editorView"]', 10000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('function retrieve() public view returns (uint256){'))
})
})
},
'compile storage': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('solidity')
.pause(1000)
.waitForElementVisible('*[data-id="compilerContainerCompileBtn"]')
.click('[data-id="compilerContainerCompileBtn"]')
.clickLaunchIcon('filePanel')
.clickLaunchIcon('solidity')
.pause(5000)
.waitForElementPresent('*[data-id="compiledContracts"] option', 60000)
.click('*[data-id="compilation-details"]')
.waitForElementVisible('*[data-id="remixui_treeviewitem_metadata"]')
}
}

@ -0,0 +1,269 @@
import { NightwatchBrowser } from 'nightwatch'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
'open default template': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromTemplate"]')
.click('button[data-id="landingPageImportFromTemplate"]')
.waitForElementPresent('*[data-id="create-remixDefault"]')
.scrollAndClick('*[data-id="create-remixDefault"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementPresent('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.click('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.click('*[data-id="treeViewLitreeViewItemtests"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]')
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('*[id="editorView"]', 10000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('function retrieve() public view returns (uint256){'))
})
})
},
'Should find text #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.click('*[plugin="search"]').waitForElementVisible('*[id="search_input"]')
.waitForElementVisible('*[id="search_include"]')
.setValue('*[id="search_include"]', ', *.*').pause(2000)
.setValue('*[id="search_input"]', 'read').sendKeys('*[id="search_input"]', browser.Keys.ENTER)
.pause(1000)
.waitForElementContainsText('*[data-id="search_results"]', '3_BALLOT.SOL', 60000)
.waitForElementContainsText('*[data-id="search_results"]', 'contracts', 60000)
.waitForElementContainsText('*[data-id="search_results"]', 'README.TXT', 60000)
.waitForElementContainsText('*[data-id="search_results"]', 'file must')
.waitForElementContainsText('*[data-id="search_results"]', 'be compiled')
.waitForElementContainsText('*[data-id="search_results"]', 'that person al')
.waitForElementContainsText('*[data-id="search_results"]', 'sender.voted')
.waitForElementContainsText('*[data-id="search_results"]', 'read')
.elements('css selector', '.search_plugin_search_line', (res) => {
Array.isArray(res.value) && browser.assert.equal(res.value.length, 6)
})
}, 'Should find text with exclude #group1': function (browser: NightwatchBrowser) {
browser
.clearValue('*[id="search_input"]')
.setValue('*[id="search_input"]', 'contract').pause(1000)
.clearValue('*[id="search_include"]').pause(2000)
.setValue('*[id="search_include"]', '**').sendKeys('*[id="search_include"]', browser.Keys.ENTER).pause(4000)
.elements('css selector', '.search_plugin_search_line', (res) => {
Array.isArray(res.value) && browser.assert.equal(res.value.length, 62)
})
.setValue('*[id="search_exclude"]', ',contracts/**').sendKeys('*[id="search_exclude"]', browser.Keys.ENTER).pause(4000)
.elements('css selector', '.search_plugin_search_line', (res) => {
Array.isArray(res.value) && browser.assert.equal(res.value.length, 56)
})
.clearValue('*[id="search_include"]').setValue('*[id="search_include"]', '*.sol, *.js, *.txt')
.clearValue('*[id="search_exclude"]').setValue('*[id="search_exclude"]', '.*/**/*')
},
'Should find regex #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[id="search_input"]')
.clearValue('*[id="search_input"]').pause(2000)
.setValue('*[id="search_input"]', '^contract').sendKeys('*[id="search_input"]', browser.Keys.ENTER).pause(3000)
.waitForElementVisible('*[data-id="search_use_regex"]').click('*[data-id="search_use_regex"]').pause(3000)
.waitForElementContainsText('*[data-id="search_results"]', '3_BALLOT.SOL', 60000)
.waitForElementContainsText('*[data-id="search_results"]', '2_OWNER.SOL', 60000)
.waitForElementContainsText('*[data-id="search_results"]', '1_STORAGE.SOL', 60000)
.waitForElementContainsText('*[data-id="search_results"]', 'BALLOT_TEST.SOL', 60000)
.waitForElementContainsText('*[data-id="search_results"]', 'tests', 60000)
.elements('css selector', '.search_plugin_search_line', (res) => {
Array.isArray(res.value) && browser.assert.equal(res.value.length, 4)
})
},
'Should find matchcase #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="search_use_regex"]').click('*[data-id="search_use_regex"]')
.waitForElementVisible('*[data-id="search_case_sensitive"]').click('*[data-id="search_case_sensitive"]').pause(4000)
.elements('css selector', '.search_plugin_search_line', (res) => {
Array.isArray(res.value) && browser.assert.equal(res.value.length, 0)
})
.clearValue('*[id="search_input"]')
.setValue('*[id="search_input"]', 'Contract').sendKeys('*[id="search_input"]', browser.Keys.ENTER).pause(3000)
.elements('css selector', '.search_plugin_search_line', (res) => {
Array.isArray(res.value) && browser.assert.equal(res.value.length, 3)
})
.waitForElementContainsText('*[data-id="search_results"]', 'STORAGE.TEST.JS', 60000)
},
'Should find matchword #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="search_case_sensitive"]').click('*[data-id="search_case_sensitive"]')
.waitForElementVisible('*[data-id="search_whole_word"]').click('*[data-id="search_whole_word"]').pause(2000)
.clearValue('*[id="search_input"]')
.setValue('*[id="search_input"]', 'contract').sendKeys('*[id="search_input"]', browser.Keys.ENTER).pause(4000)
.elements('css selector', '.search_plugin_search_line', (res) => {
Array.isArray(res.value) && browser.assert.equal(res.value.length, 15)
})
},
'Should replace text #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="toggle_replace"]').click('*[data-id="toggle_replace"]')
.waitForElementVisible('*[id="search_replace"]')
.clearValue('*[id="search_include"]').setValue('*[id="search_include"]', 'contracts/2_*.sol')
.setValue('*[id="search_replace"]', 'replacing').sendKeys('*[id="search_include"]', browser.Keys.ENTER).pause(1000)
.waitForElementVisible('*[data-id="contracts/2_Owner.sol-33-71"]')
.moveToElement('*[data-id="contracts/2_Owner.sol-33-71"]', 10, 10)
.waitForElementVisible('*[data-id="replace-contracts/2_Owner.sol-33-71"]')
.click('*[data-id="replace-contracts/2_Owner.sol-33-71"]').pause(2000).
modalFooterOKClick('confirmreplace').pause(2000).
getEditorValue((content) => {
browser.assert.ok(content.includes('replacing deployer for a constructor'), 'should replace text ok')
})
},
'Should replace text without confirmation #group1': function (browser: NightwatchBrowser) {
browser.click('*[data-id="confirm_replace_label"]').pause(500)
.clearValue('*[id="search_input"]')
.setValue('*[id="search_input"]', 'replacing').sendKeys('*[id="search_input"]', browser.Keys.ENTER).pause(1000)
.setValue('*[id="search_replace"]', 'replacing2').pause(1000)
.waitForElementVisible('*[data-id="contracts/2_Owner.sol-33-71"]')
.moveToElement('*[data-id="contracts/2_Owner.sol-33-71"]', 10, 10)
.waitForElementVisible('*[data-id="replace-contracts/2_Owner.sol-33-71"]')
.click('*[data-id="replace-contracts/2_Owner.sol-33-71"]').pause(2000).
getEditorValue((content) => {
browser.assert.ok(content.includes('replacing2 deployer for a constructor'), 'should replace text ok')
})
},
'Should replace all & undo #group1': function (browser: NightwatchBrowser) {
browser
.clearValue('*[id="search_input"]')
.clearValue('*[id="search_include"]').setValue('*[id="search_include"]', 'contracts/1_*.sol')
.setValue('*[id="search_input"]', 'storage').sendKeys('*[id="search_include"]', browser.Keys.ENTER)
.clearValue('*[id="search_replace"]')
.setValue('*[id="search_replace"]', '123test').pause(1000)
.waitForElementVisible('*[data-id="replace-all-contracts/1_Storage.sol"]')
.click('*[data-id="replace-all-contracts/1_Storage.sol"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('contract 123test'), 'should replace text ok')
browser.assert.ok(content.includes('title 123test'), 'should replace text ok')
})
.waitForElementVisible('*[data-id="undo-replace-contracts/1_Storage.sol"]')
.click('*[data-id="undo-replace-contracts/1_Storage.sol"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('contract Storage'), 'should undo text ok')
browser.assert.ok(content.includes('title Storage'), 'should undo text ok')
})
},
'Should replace all & undo & switch between files #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[id="search_input"]')
.clearValue('*[id="search_input"]')
.clearValue('*[id="search_include"]').setValue('*[id="search_include"]', '*.sol, *.js, *.txt')
.setValue('*[id="search_input"]', 'storage').sendKeys('*[id="search_include"]', browser.Keys.ENTER)
.clearValue('*[id="search_replace"]')
.setValue('*[id="search_replace"]', '123test').pause(1000)
.waitForElementVisible('*[data-id="replace-all-contracts/1_Storage.sol"]')
.click('*[data-id="replace-all-contracts/1_Storage.sol"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('contract 123test'), 'should replace text ok')
browser.assert.ok(content.includes('title 123test'), 'should replace text ok')
})
.waitForElementVisible('*[data-id="undo-replace-contracts/1_Storage.sol"]')
.openFile('README.txt')
.click('*[plugin="search"]').pause(2000)
.waitForElementNotPresent('*[data-id="undo-replace-contracts/1_Storage.sol"]')
.waitForElementVisible('*[data-id="replace-all-README.txt"]')
.click('*[data-id="replace-all-README.txt"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes("123test' contract"), 'should replace text ok')
})
.waitForElementVisible('*[data-id="undo-replace-README.txt"]')
.click('div[data-path="/contracts/1_Storage.sol"]').pause(2000)
.waitForElementVisible('*[data-id="undo-replace-contracts/1_Storage.sol"]')
.click('*[data-id="undo-replace-contracts/1_Storage.sol"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('contract Storage'), 'should undo text ok')
browser.assert.ok(content.includes('title Storage'), 'should undo text ok')
})
.click('div[data-path="/README.txt"]').pause(2000)
.waitForElementVisible('*[data-id="undo-replace-README.txt"]')
.click('*[data-id="undo-replace-README.txt"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes("Storage' contract"), 'should replace text ok')
})
},
'Should hide button when edited content is the same #group2': function (browser: NightwatchBrowser) {
browser.refresh()
.waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.addFile('test.sol', { content: '123' })
.pause(4000)
.click('*[plugin="search"]')
.waitForElementVisible('*[id="search_input"]')
.waitForElementVisible('*[data-id="toggle_replace"]')
.click('*[data-id="toggle_replace"]')
.clearValue('*[id="search_input"]')
.setValue('*[id="search_input"]', '123')
.sendKeys('*[id="search_input"]', browser.Keys.ENTER)
.waitForElementVisible('*[id="search_replace"]')
.clearValue('*[id="search_replace"]')
.setValue('*[id="search_replace"]', '456').pause(1000)
.click('*[data-id="confirm_replace_label"]').pause(500)
.waitForElementVisible('*[data-id="replace-all-test.sol"]')
.click('*[data-id="replace-all-test.sol"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('456'), 'should replace text ok')
}
)
.setEditorValue('123')
.getEditorValue((content) => {
browser.assert.ok(content.includes('123'), 'should have text ok')
}
).pause(5000)
.waitForElementNotPresent('*[data-id="undo-replace-test.sol"]')
},
'Should disable/enable button when edited content changed #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[id="search_input"]')
.clearValue('*[id="search_input"]')
.clearValue('*[id="search_input"]')
.setValue('*[id="search_input"]', '123').sendKeys('*[id="search_input"]', browser.Keys.ENTER)
.clearValue('*[id="search_replace"]')
.setValue('*[id="search_replace"]', 'replaced').pause(1000)
.waitForElementVisible('*[data-id="replace-all-test.sol"]')
.click('*[data-id="replace-all-test.sol"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('replaced'), 'should replace text ok')
}
)
.setEditorValue('changed')
.getEditorValue((content) => {
browser.assert.ok(content.includes('changed'), 'should have text ok')
}
).pause(5000)
.waitForElementVisible('*[data-id="undo-replace-test.sol"]')
.getAttribute('[data-id="undo-replace-test.sol"]', 'disabled', (result) => {
browser.assert.equal(result.value, 'true', 'should be disabled')
})
.setEditorValue('replaced')
.getEditorValue((content) => {
browser.assert.ok(content.includes('replaced'), 'should have text ok')
}
).pause(1000)
.waitForElementVisible('*[data-id="undo-replace-test.sol"]')
.getAttribute('[data-id="undo-replace-test.sol"]', 'disabled', (result) => {
browser.assert.equal(result.value, null, 'should not be disabled')
})
.click('*[data-id="undo-replace-test.sol"]').pause(2000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('123'), 'should have text ok')
})
.waitForElementNotPresent('*[data-id="undo-replace-test.sol"]')
},
'should clear search #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[id="search_input"]')
.setValue('*[id="search_input"]', 'nodata').sendKeys('*[id="search_input"]', browser.Keys.ENTER).pause(1000)
.elements('css selector', '.search_plugin_search_line', (res) => {
Array.isArray(res.value) && browser.assert.equal(res.value.length, 0)
})
}
}

@ -0,0 +1,107 @@
import {NightwatchBrowser} from 'nightwatch'
import { ChildProcess, spawn, execSync } from 'child_process'
import { homedir } from 'os'
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
open: function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="openFolderButton"]', 10000).click('*[data-id="openFolderButton"]')
},
'open default template': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromTemplate"]')
.click('button[data-id="landingPageImportFromTemplate"]')
.waitForElementPresent('*[data-id="create-remixDefault"]')
.scrollAndClick('*[data-id="create-remixDefault"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementPresent('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.click('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.click('*[data-id="treeViewLitreeViewItemtests"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]')
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('*[id="editorView"]', 10000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('function retrieve() public view returns (uint256){'))
})
})
},
'Should install slither #group6': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
await installSlither()
done()
})
},
'run slither': function (browser: NightwatchBrowser) {
browser
.click('[data-id="verticalIconsKindpluginManager"]')
.scrollAndClick('[data-id="pluginManagerComponentActivateButtonsolidityStaticAnalysis"]')
.clickLaunchIcon('solidity').click('*[data-id="compilerContainerCompileBtn"]')
.pause(1000)
.clickLaunchIcon('solidityStaticAnalysis')
.useXpath()
.click('//*[@id="staticAnalysisRunBtn"]')
.waitForElementPresent('//*[@id="staticanalysisresult"]', 5000)
.waitForElementVisible({
selector: "//*[@data-id='nolibslitherwarnings'][contains(text(), '1')]",
locateStrategy: 'xpath',
timeout: 5000
})
.waitForElementVisible({
selector: "//div[@data-id='block']/span[contains(text(), '1 warnings found.')]",
locateStrategy: 'xpath',
timeout: 5000
})
},
after: function (browser: NightwatchBrowser) {
browser.end()
},
}
async function installSlither(): Promise<void> {
console.log('installSlither', process.cwd())
try {
try {
const solcVersion = '0.8.15'
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: requires Python3.6+ (pip3) to be installed on your system`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: solc-select will be installed along with Slither to set different solc compiler versions.`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: checking pip3 availability ...`)
const pip3OP = execSync('pip3 --version')
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: pip3 found: ${pip3OP.toString()}`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: installing slither...`)
const slitherOP = execSync('pip3 install slither-analyzer')
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: slither installation output: ${slitherOP.toString()}`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: installing solc-select...`)
const solcSelectOP = execSync('pip3 install solc-select')
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: solc-select installation output: ${solcSelectOP.toString()}`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: installing solc ${solcVersion}...`)
const solcInstallOP = execSync(`solc-select install ${solcVersion}`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: solc installation output: ${solcInstallOP.toString()}`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: setting solc version to ${solcVersion}...`)
const solcUseOP = execSync(`solc-select use ${solcVersion}`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: solc setting installation output: ${solcUseOP.toString()}`)
console.log('\x1b[32m%s\x1b[0m', `[Slither Installation]: Slither is ready to use!`)
} catch (err) {
console.log('\x1b[31m%s\x1b[0m', `[Slither Installation]: Error occurred: ${err}`)
}
} catch (e) {
console.log(e)
}
}
module.exports = {
...process.platform.startsWith('linux')?tests:{}
}

@ -0,0 +1,35 @@
import { NightwatchBrowser } from 'nightwatch'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
'open default template': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromTemplate"]')
.click('button[data-id="landingPageImportFromTemplate"]')
.waitForElementPresent('*[data-id="create-remixDefault"]')
.scrollAndClick('*[data-id="create-remixDefault"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementPresent('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.click('[data-id="TemplatesSelectionModalDialogContainer-react"] .modal-ok')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.click('*[data-id="treeViewLitreeViewItemtests"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]')
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('*[id="editorView"]', 10000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('function retrieve() public view returns (uint256){'))
})
})
.end()
}
}

@ -0,0 +1,246 @@
import {NightwatchBrowser} from 'nightwatch'
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
open: function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="openFolderButton"]', 10000).click('*[data-id="openFolderButton"]')
},
'open xterm linux and create a file': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="tabXTerm"]', 10000)
.click('*[data-id="tabXTerm"]')
.waitForElementVisible('*[data-type="remixUIXT"]', 10000)
.click('*[data-type="remixUIXT"]')
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('echo test > example.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemexample.txt"]', 10000)
},
'rename that file': function (browser: NightwatchBrowser) {
browser
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('mv example.txt newExample.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemnewExample.txt"]', 10000)
},
'create a file and delete it': function (browser: NightwatchBrowser) {
browser
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('touch newExample2.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemnewExample2.txt"]', 10000)
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('rm newExample2.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemnewExample2.txt"]', 10000)
},
'run a git clone command': function (browser: NightwatchBrowser) {
browser
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('git clone https://github.com/ethereum/awesome-remix').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemawesome-remix"]', 10000)
.click('*[data-id="treeViewLitreeViewItemawesome-remix"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemawesome-remix/README.md"]', 10000)
},
'remove the cloned repo': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-type="remixUIXT"]', 10000)
.click('*[data-type="remixUIXT"]')
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('rm -rf awesome-remix').sendKeys(this.Keys.ENTER)
})
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemawesome-remix"]', 10000)
},
'list files': function (browser: NightwatchBrowser) {
browser
.pause(2000)
.waitForElementVisible({
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
})
.click({
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
})
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('ls').sendKeys(this.Keys.ENTER)
}).pause(3000)
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok((result.value as string).includes('newExample.txt'))
}
)
},
'switch to a new terminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="createTerminalButton"]', 10000)
.click('*[data-id="createTerminalButton"]')
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.assert.ok((result.value as any).length === 2)
})
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok(!(result.value as string).includes('newExample.txt'))
}
)
},
'switch to a third terminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="createTerminalButton"]', 10000)
.click('*[data-id="createTerminalButton"]')
.waitForElementVisible(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
10000
)
.click({
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
})
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.assert.ok((result.value as any).length === 3)
})
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('echo thirdterminal').sendKeys(this.Keys.ENTER)
})
},
'switch back to the second terminal': function (browser: NightwatchBrowser) {
browser
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.elementIdClick(Object.values((result.value as any)[1])[0] as any)
})
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok(!(result.value as string).includes('newExample.txt'))
}
)
},
'close the second terminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="closeTerminalButton"]', 10000)
.click('*[data-id="closeTerminalButton"]')
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.assert.ok((result.value as any).length === 2)
})
},
'switch back to the first terminal': function (browser: NightwatchBrowser) {
browser
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.elementIdClick(Object.values((result.value as any)[0])[0] as any)
})
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok((result.value as string).includes('newExample.txt'))
}
)
},
'switch to the output panel': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="tabOutput"]', 10000).click('*[data-id="tabOutput"]').waitForElementNotPresent('*[data-id="createTerminalButton"]', 10000)
},
'switch back to xterminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="tabXTerm"]', 10000)
.click('*[data-id="tabXTerm"]')
.waitForElementVisible('*[data-type="remixUIXT"]', 10000)
.click('*[data-type="remixUIXT"]')
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok((result.value as string).includes('newExample.txt'))
}
)
},
'clear the terminal and type exit': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="clearTerminalButton"]', 10000)
.click('*[data-id="clearTerminalButton"]')
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok(!(result.value as string).includes('newExample.txt'))
.waitForElementVisible(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
10000
)
.click({
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
})
.pause(1000)
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('exit').sendKeys(this.Keys.ENTER)
})
.pause(1000)
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.assert.ok((result.value as any).length === 1)
}).end()
}
).pause(3000)
},
}
module.exports = {
...process.platform.startsWith('win')?{}:tests
}

@ -0,0 +1,249 @@
import {NightwatchBrowser} from 'nightwatch'
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
open: function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="openFolderButton"]', 10000).click('*[data-id="openFolderButton"]')
},
'open xterm window and create a file': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="tabXTerm"]', 10000)
.click('*[data-id="tabXTerm"]')
.waitForElementVisible('*[data-id="select_shell"]')
.click('*[data-id="select_shell"]')
.waitForElementVisible('*[data-id="select_powershell.exe"]')
.click('*[data-id="select_powershell.exe"]')
.pause(3000)
.waitForElementVisible("[data-active='1'][data-type='remixUIXT']", 10000)
.click("[data-active='1'][data-type='remixUIXT']")
.pause(1000)
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('"test" | Out-File -FilePath example.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemexample.txt"]', 10000)
},
'rename that file': function (browser: NightwatchBrowser) {
browser
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('Move-Item -Path example.txt -Destination newExample.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemnewExample.txt"]', 10000)
},
'create a file and delete it': function (browser: NightwatchBrowser) {
browser
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('touch newExample2.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemnewExample2.txt"]', 10000)
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('Remove-Item -Path newExample2.txt').sendKeys(this.Keys.ENTER)
})
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemnewExample2.txt"]', 10000)
},
'run a git clone command': function (browser: NightwatchBrowser) {
browser
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('git clone https://github.com/ethereum/awesome-remix').sendKeys(this.Keys.ENTER)
})
.waitForElementVisible('*[data-id="treeViewLitreeViewItemawesome-remix"]', 10000)
.click('*[data-id="treeViewLitreeViewItemawesome-remix"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemawesome-remix/README.md"]', 10000)
},
'remove the cloned repo': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible("[data-active='1'][data-type='remixUIXT']", 10000)
.click("[data-active='1'][data-type='remixUIXT']")
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('Remove-Item -Path awesome-remix -Recurse -Force').sendKeys(this.Keys.ENTER)
})
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemawesome-remix"]', 10000)
},
'list files': function (browser: NightwatchBrowser) {
browser
.pause(3000)
.waitForElementVisible("[data-active='1'][data-type='remixUIXT']", 10000)
.click("[data-active='1'][data-type='remixUIXT']")
.saveScreenshot('./reports/screenshots/list-files.png')
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('ls').sendKeys(this.Keys.ENTER)
})
.saveScreenshot('./reports/screenshots/list-files-after.png')
.waitForElementVisible({
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
})
.pause(2000)
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok((result.value as string).includes('newExample'))
}
)
},
'switch to a new terminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="select_shell"]')
.click('*[data-id="select_shell"]')
.waitForElementVisible('*[data-id="select_powershell.exe"]')
.click('*[data-id="select_powershell.exe"]')
.pause(3000)
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
console.log(result)
browser.assert.ok((result.value as any).length === 3)
})
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok(!(result.value as string).includes('newExample'))
}
)
},
'switch to a third terminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="select_shell"]')
.click('*[data-id="select_shell"]')
.waitForElementVisible('*[data-id="select_powershell.exe"]')
.click('*[data-id="select_powershell.exe"]')
.pause(3000)
.waitForElementVisible(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
10000
)
.click({
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
})
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.assert.ok((result.value as any).length === 4)
})
.perform(function () {
const actions = this.actions({async: true})
return actions.sendKeys('echo thirdterminal').sendKeys(this.Keys.ENTER)
})
},
'switch back to the second terminal': function (browser: NightwatchBrowser) {
browser
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.elementIdClick(Object.values((result.value as any)[2])[0] as any)
})
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok(!(result.value as string).includes('newExample'))
}
)
},
'close the second terminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="closeTerminalButton"]', 10000)
.click('*[data-id="closeTerminalButton"]')
.pause(1000)
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.assert.ok((result.value as any).length === 3)
})
},
'switch back to the first terminal': function (browser: NightwatchBrowser) {
browser
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.elementIdClick(Object.values((result.value as any)[1])[0] as any)
})
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok((result.value as string).includes('newExample'))
}
)
},
'switch to the output panel': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="tabOutput"]', 10000).click('*[data-id="tabOutput"]').waitForElementNotPresent('*[data-id="createTerminalButton"]', 10000)
},
'switch back to xterminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="tabXTerm"]', 10000)
.click('*[data-id="tabXTerm"]')
.waitForElementVisible("[data-active='1'][data-type='remixUIXT']", 10000)
.click("[data-active='1'][data-type='remixUIXT']")
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok((result.value as string).includes('newExample'))
}
)
},
'clear the terminal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="clearTerminalButton"]', 10000)
.click('*[data-id="clearTerminalButton"]')
.getText(
{
selector: "//*[@data-type='remixUIXT' and @data-active='1']",
timeout: 10000,
locateStrategy: 'xpath',
},
function (result) {
console.log('Text content of the element:', result.value)
browser.assert.ok(!(result.value as string).includes('newExample'))
}
)
},
'close all terminals': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="closeTerminalButton"]', 10000)
.click('*[data-id="closeTerminalButton"]')
.pause(3000)
.click('*[data-id="closeTerminalButton"]')
.pause(3000)
.click('*[data-id="closeTerminalButton"]')
.pause(3000)
.elements('css selector', '[data-type="remixUIXTSideButton"]', function (result) {
browser.assert.ok((result.value as any).length === 0)
}).end()
},
after: function (browser: NightwatchBrowser) {
browser.end()
},
}
module.exports = {
...process.platform.startsWith('win')?tests:{}
}

@ -0,0 +1,108 @@
// Merge custom command types with nightwatch types
/* eslint-disable no-use-before-define */
import {NightwatchBrowser} from 'nightwatch' // eslint-disable-line @typescript-eslint/no-unused-vars
export type callbackCheckVerifyCallReturnValue = (values: string[]) => {message: string; pass: boolean}
declare module 'nightwatch' {
export interface NightwatchCustomCommands {
clickLaunchIcon(icon: string): NightwatchBrowser
switchBrowserTab(index: number): NightwatchBrowser
scrollAndClick(target: string): NightwatchBrowser
scrollInto(target: string): NightwatchBrowser
testContracts(fileName: string, contractCode: NightwatchContractContent, compiledContractNames: string[]): NightwatchBrowser
setEditorValue(value: string, callback?: () => void): NightwatchBrowser
addFile(name: string, content: NightwatchContractContent, readMeFile?: string): NightwatchBrowser
verifyContracts(compiledContractNames: string[], opts?: {wait: number; version?: string; runs?: string}): NightwatchBrowser
selectAccount(account?: string): NightwatchBrowser
clickFunction(fnFullName: string, expectedInput?: NightwatchClickFunctionExpectedInput): NightwatchBrowser
testFunction(txHash: string, expectedInput: NightwatchTestFunctionExpectedInput): NightwatchBrowser
goToVMTraceStep(step: number, incr?: number): NightwatchBrowser
checkVariableDebug(id: string, debugValue: NightwatchCheckVariableDebugValue): NightwatchBrowser
addAtAddressInstance(address: string, isValidFormat: boolean, isValidChecksum: boolean, isAbi?: boolean): NightwatchBrowser
modalFooterOKClick(id?: string): NightwatchBrowser
clickInstance(index: number): NightwatchBrowser
journalLastChildIncludes(val: string): NightwatchBrowser
executeScriptInTerminal(script: string): NightwatchBrowser
clearEditableContent(cssSelector: string): NightwatchBrowser
journalChildIncludes(val: string, opts = {shouldHaveOnlyOneOccurence: boolean}): NightwatchBrowser
debugTransaction(index: number): NightwatchBrowser
checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser
openFile(name: string): NightwatchBrowser
refreshPage(): NightwatchBrowser
verifyLoad(): NightwatchBrowser
renamePath(path: string, newFileName: string, renamedPath: string): NightwatchBrowser
rightClickCustom(cssSelector: string): NightwatchBrowser
scrollToLine(line: number): NightwatchBrowser
waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser
getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser
modalFooterCancelClick(id?: string): NightwatchBrowser
selectContract(contractName: string): NightwatchBrowser
createContract(inputParams: string): NightwatchBrowser
getAddressAtPosition(index: number, cb: (pos: string) => void): NightwatchBrowser
testConstantFunction(address: string, fnFullName: string, expectedInput: NightwatchTestConstantFunctionExpectedInput | null, expectedOutput: string): NightwatchBrowser
getEditorValue(callback: (content: string) => void): NightwatchBrowser
getInstalledPlugins(cb: (plugins: string[]) => void): NightwatchBrowser
verifyCallReturnValue(address: string, checks: string[] | callbackCheckVerifyCallReturnValue): NightwatchBrowser
testEditorValue(testvalue: string): NightwatchBrowser
removeFile(path: string, workspace: string): NightwatchBrowser
switchBrowserWindow(url: string, windowName: string, cb: (browser: NightwatchBrowser, window?: NightwatchCallbackResult<Window>) => void): NightwatchBrowser
setupMetamask(passphrase: string, password: string): NightwatchBrowser
signMessage(msg: string, callback: (hash: {value: string}, signature: {value: string}) => void): NightwatchBrowser
setSolidityCompilerVersion(version: string): NightwatchBrowser
clickElementAtPosition(cssSelector: string, index: number, opt?: {forceSelectIfUnselected: boolean}): NightwatchBrowser
notContainsText(cssSelector: string, text: string): NightwatchBrowser
sendLowLevelTx(address: string, value: string, callData: string): NightwatchBrowser
journalLastChild(val: string): NightwatchBrowser
checkTerminalFilter(filter: string, test: string): NightwatchBrowser
noWorkerErrorFor(version: string): NightwatchBrowser
validateValueInput(selector: string, valueTosSet: string[], expectedValue: string): NightwatchBrowser
checkAnnotations(type: string): NightwatchBrowser
checkAnnotationsNotPresent(type: string): NightwatchBrowser
getLastTransactionHash(callback: (hash: string) => void)
currentWorkspaceIs(name: string): NightwatchBrowser
addLocalPlugin(this: NightwatchBrowser, profile: Profile & LocationProfile & ExternalProfile): NightwatchBrowser
acceptAndRemember(this: NightwatchBrowser, remember: boolean, accept: boolean): NightwatchBrowser
clearConsole(this: NightwatchBrowser): NightwatchBrowser
clearTransactions(this: NightwatchBrowser): NightwatchBrowser
getBrowserLogs(this: NightwatchBrowser): NightwatchBrowser
currentSelectedFileIs(name: string): NightwatchBrowser
switchWorkspace: (workspaceName: string) => NightwatchBrowser
switchEnvironment: (provider: string) => NightwatchBrowser
connectToExternalHttpProvider: (url: string, identifier: string) => NightwatchBrowser
waitForElementNotContainsText: (id: string, value: string, timeout: number = 10000) => NightwatchBrowser
hideToolTips: (this: NightwatchBrowser) => NightwatchBrowser
enableClipBoard: () => NightwatchBrowser
}
export interface NightwatchBrowser {
api: this
emit: (status: string) => void
fullscreenWindow: (result?: any) => this
keys(keysToSend: string, callback?: (this: NightwatchAPI, result: NightwatchCallbackResult<void>) => void): NightwatchBrowser
sendKeys: (selector: string, inputValue: string | string[], callback?: (this: NightwatchAPI, result: NightwatchCallbackResult<void>) => void) => NightwatchBrowser
}
export interface NightwatchAPI {
keys(keysToSend: string, callback?: (this: NightwatchAPI, result: NightwatchCallbackResult<void>) => void): NightwatchAPI
}
export interface NightwatchContractContent {
content: string
}
export interface NightwatchClickFunctionExpectedInput {
types: string
values: string
}
export interface NightwatchTestFunctionExpectedInput {
[key: string]: any
}
export interface NightwatchTestConstantFunctionExpectedInput {
types: string
values: string
}
export type NightwatchCheckVariableDebugValue = NightwatchTestFunctionExpectedInput
}

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "build-e2e",
"target": "ES6"
},
"include": ["test/**/*.ts", "test/**/*.js", "../remix-ide-e2e/src/commands"]
}

@ -1,17 +1,35 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "ES2018",
"module": "commonjs", "allowJs": true,
"module": "CommonJS",
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"noImplicitAny": true, "noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"sourceMap": true, "sourceMap": true,
"strictPropertyInitialization": false, "baseUrl": ".",
"strict": true, "outDir": "./build",
"outDir": "build", "moduleResolution": "node",
"rootDir": "./src/", "resolveJsonModule": true,
"noEmitOnError": true, "paths": {
"typeRoots": ["node_modules/@types", "./types"] "*": [
} "node_modules/*"
],
"@remix-api": [
"../../libs/remix-api/src/lib/types/git.ts"
],
"@remix-git": [
"../../libs/remix-git/"
],
},
"typeRoots": [
"src/**/*.d.ts",
"node_modules/@types",
"test/**/*.d.ts",
"../remix-ide-e2e/src/**/*.d.ts"
]
},
"include": [
"src/**/*",
]
} }

@ -0,0 +1,44 @@
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const mode = process.env.NODE_ENV || 'development';
const webpack = require('webpack');
module.exports = {
mode,
entry: {
main: './src/main.ts',
preload: './src/preload.ts',
},
target: 'electron-main',
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.ts$/,
include: /src/,
use: [{ loader: 'ts-loader' }]
},
{
test: /\.node$/,
use: 'node-loader'
}
]
},
resolve: {
extensions: ['.ts', '.js'],
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || mode)
})
],
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
},
node: {
__dirname: false,
__filename: false
}
}

File diff suppressed because it is too large Load Diff

@ -55,4 +55,8 @@ export class CompilerClientApi extends CompilerApiMixin(PluginClient) implements
getFileManagerMode () { getFileManagerMode () {
return 'browser' return 'browser'
} }
isDesktop() {
return false
}
} }

@ -1 +1,2 @@
export * from './lib/remix-api' export * from './lib/remix-api'
export * from './lib/types/git'

@ -7,6 +7,6 @@ export interface IFilePanelApi {
switchToWorkspace: (workspace: string) => Promise<void>; switchToWorkspace: (workspace: string) => Promise<void>;
} & StatusEvents } & StatusEvents
methods: IFilePanel['methods'] & { methods: IFilePanel['methods'] & {
clone: () => Promise<void>;
} }
} }

@ -1,10 +1,13 @@
import { commitChange } from "@remix-ui/git"; import { commitChange } from "@remix-api";
import { IFileSystem } from "@remixproject/plugin-api" import { IFileSystem } from "@remixproject/plugin-api"
// Extended interface with 'diff' method // Extended interface with 'diff' method
export interface IExtendedFileSystem extends IFileSystem { export interface IExtendedFileSystem extends IFileSystem {
methods: IFileSystem['methods'] & { methods: IFileSystem['methods'] & {
/** Compare the differences between two files */
diff(change: commitChange): Promise<void> diff(change: commitChange): Promise<void>
refresh(): Promise<void>
hasGitSubmodules(): Promise<boolean>
isGitRepo(): Promise<boolean> isGitRepo(): Promise<boolean>
}; };
} }

@ -0,0 +1,13 @@
import { StatusEvents } from "@remixproject/plugin-utils";
export interface IFs {
events: {
workingDirChanged(path: string): Promise<void>,
} & StatusEvents,
methods: {
selectFolder(path?: string, title?: string, button?: string): Promise<string>
openWindow(path?: string): Promise<void>,
getWorkingDir(): Promise<string>,
openFolderInSameWindow(path: string): Promise<void>,
}
}

@ -0,0 +1,43 @@
import { StatusEvents } from "@remixproject/plugin-utils"
import { ReadBlobResult, ReadCommitResult, StatusRow } from "isomorphic-git"
import { commitChange, repositoriesInput, repository, cloneInputType, branchesInputType, branch, remote, logInputType, remoteCommitsInputType, pagedCommits, fetchInputType, pullInputType, pushInputType, currentBranchInput, branchInputType, checkoutInputType, addInputType, rmInputType, resolveRefInput, readBlobInput, commitInputType, statusInput, compareBranchesInput, branchDifference, initInputType, updateSubmodulesInput } from "../types/git"
export interface IGitApi {
events: {
"checkout": () => void
"clone": () => void
"add": () => void
"rm": () => void
"commit": () => void
"branch": () => void
"init": () => void
} & StatusEvents,
methods: {
getCommitChanges(oid1: string, oid2: string): Promise<commitChange[]>
repositories(input: repositoriesInput): Promise<repository[]>
clone(input: cloneInputType): Promise<any>
branches(input?: branchesInputType): Promise<branch[]>,
remotes(): Promise<remote[]>,
log(input: logInputType): Promise<ReadCommitResult[]>,
remotecommits(input: remoteCommitsInputType): Promise<pagedCommits[]>
fetch(input: fetchInputType): Promise<any>
pull(input: pullInputType): Promise<any>
push(input: pushInputType): Promise<any>
currentbranch(input?: currentBranchInput): Promise<branch>
branch(input: branchInputType): Promise<void>
checkout(input: checkoutInputType): Promise<void>
add(input: addInputType): Promise<void>
rm(input: rmInputType): Promise<void>
resolveref(input: resolveRefInput): Promise<string>
readblob(input: readBlobInput): Promise<ReadBlobResult>
commit(input: commitInputType): Promise<string>
addremote(input: remote): Promise<void>
delremote(input: remote): Promise<void>
status(input?: statusInput): Promise<Array<StatusRow>>
compareBranches(input: compareBranchesInput): Promise<branchDifference>
init(input?: initInputType): Promise<void>
updateSubmodules: (input: updateSubmodulesInput) => Promise<void>
version: () => Promise<string>
}
}

@ -0,0 +1,10 @@
import { ITerminal } from "@remixproject/plugin-api/src/lib/terminal"
import { StatusEvents } from "@remixproject/plugin-utils"
export interface IExtendedTerminalApi extends ITerminal {
events: {
} & StatusEvents
methods: ITerminal['methods'] & {
logHtml(html: string): void
}
}

@ -1,13 +1,14 @@
import { IGitApi } from "@remix-ui/git"
import { IRemixApi } from "@remixproject/plugin-api" import { IRemixApi } from "@remixproject/plugin-api"
import { StatusEvents } from "@remixproject/plugin-utils" import { StatusEvents } from "@remixproject/plugin-utils"
import { IConfigApi } from "./plugins/config-api" import { IConfigApi } from "./plugins/config-api"
import { IFileDecoratorApi } from "./plugins/filedecorator-api" import { IFileDecoratorApi } from "./plugins/filedecorator-api"
import { IExtendedFileSystem } from "./plugins/fileSystem-api" import { IExtendedFileSystem } from "./plugins/fileSystem-api"
import { IFs } from "./plugins/fs-api"
import { IGitApi } from "./plugins/git-api"
import { INotificationApi } from "./plugins/notification-api" import { INotificationApi } from "./plugins/notification-api"
import { ISettings } from "./plugins/settings-api" import { ISettings } from "./plugins/settings-api"
import { IExtendedTerminalApi } from "./plugins/terminal-api"
import { IFilePanelApi } from "./plugins/filePanel-api" import { IFilePanelApi } from "./plugins/filePanel-api"
import { Plugin } from "@remixproject/engine"
import { ISidePanelApi } from "./plugins/sidePanel-api" import { ISidePanelApi } from "./plugins/sidePanel-api"
import { IPinnedPanelApi } from "./plugins/pinned-panel-api" import { IPinnedPanelApi } from "./plugins/pinned-panel-api"
import { ILayoutApi } from "./plugins/layout-api" import { ILayoutApi } from "./plugins/layout-api"
@ -20,6 +21,9 @@ export interface ICustomRemixApi extends IRemixApi {
settings: ISettings settings: ISettings
fileDecorator: IFileDecoratorApi fileDecorator: IFileDecoratorApi
fileManager: IExtendedFileSystem fileManager: IExtendedFileSystem
isogit: IGitApi,
terminal: IExtendedTerminalApi
fs: IFs
filePanel: IFilePanelApi filePanel: IFilePanelApi
sidePanel: ISidePanelApi sidePanel: ISidePanelApi
pinnedPanel: IPinnedPanelApi pinnedPanel: IPinnedPanelApi

@ -0,0 +1,206 @@
import { Endpoints } from "@octokit/types"
import { AuthCallback, HttpClient, ReadCommitResult } from "isomorphic-git"
export type branchDifference = {
uniqueHeadCommits: ReadCommitResult[],
uniqueRemoteCommits: ReadCommitResult[],
}
export type commitChange = {
type: commitChangeType
path: string,
hashModified: string,
hashOriginal: string,
original?: string,
modified?: string,
readonly?: boolean
}
export type commitChangeTypes = {
"deleted": "D"
"modified": "M"
"added": "A",
"unknown": "?"
}
export type pagedCommits = {
page: number,
perPage: number,
total: number,
hasNextPage: boolean,
commits: ReadCommitResult[]
}
export enum syncStatus {
"sync" = "sync",
"publishBranch" = "publishBranch",
"none" = "none",
}
export type repository = {
name: string
html_url: string
owner: {
login: string
},
full_name: string
default_branch: string
id: number
url: string
}
export type branch = {
name: string
remote: remote
}
export type remote = {
name: string
url: string
}
export type remoteBranch = {
name: string
}
export type commitChangeType = keyof commitChangeTypes
export type initInputType = {
defaultBranch: string
}
export type author = {
name: string,
email: string,
}
export type updateSubmodulesInput = {
dir?: string
token?: string
}
export type remoteCommitsInputType = {
owner: string, repo: string, token: string, branch: string, length: number, page: number
}
export type compareBranchesInput = {
branch: branch, remote: remote
}
export type fetchInputType = {
remote: remote,
ref?: branch,
remoteRef?: branch,
depth?: number,
singleBranch?: boolean,
relative?: boolean,
quiet?: boolean
author?: author
token?: string
}
export type logInputType = {
ref: string,
depth?: number,
}
export type pullInputType = {
remote: remote,
ref: branch,
remoteRef?: branch
author?: author
token?: string
}
export type pushInputType = {
remote: remote,
ref: branch,
remoteRef?: branch,
force?: boolean,
author?: author,
token?: string
}
export type branchInputType = {
ref: string,
checkout?: boolean
refresh?: boolean
force?: boolean
}
export type currentBranchInput = {
fs: any,
dir: string
}
export type checkoutInputType = {
ref: string,
force?: boolean,
remote?: string
refresh?: boolean
fetch?: boolean
}
export type addInputType = {
filepath: string | string[]
}
export type rmInputType = {
filepath: string
}
export type resolveRefInput = {
ref: string
}
export type readBlobInput = {
oid: string,
filepath: string
}
export type commitInputType = {
author: {
name: string,
email: string,
},
message: string,
}
export type branchesInputType = {
fs?: any
dir?: string
}
export interface cloneInputType {
url: string,
branch?: string,
depth?: number,
singleBranch?: boolean
workspaceName?: string
workspaceExists?: boolean
token?: string
dir?: string // where the clone should happen on desktop
}
export interface repositoriesInput { token: string, page?: number, per_page?: number }
export interface statusInput { ref: string, filepaths?: string[] }
export type isoGitFSConfig = {
fs: any,
dir: string,
}
export type isoGitProxyConfig = {
corsProxy: string
http: HttpClient
onAuth: AuthCallback
}
export type GitHubUser = Partial<Endpoints["GET /user"]["response"]['data']> & {
isConnected: boolean
}
export type userEmails = Endpoints["GET /user/emails"]["response"]["data"]

@ -2,6 +2,7 @@
'use strict' 'use strict'
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import isElectron from 'is-electron' import isElectron from 'is-electron'
import { Registry } from '@remix-project/remix-lib'
interface StringByString { interface StringByString {
[key: string]: string; [key: string]: string;
@ -17,12 +18,16 @@ const profile = {
type GistCallBackFn = (gistId: string) => void type GistCallBackFn = (gistId: string) => void
export class GistHandler extends Plugin { export class GistHandler extends Plugin {
constructor () { isDesktop: boolean = false
constructor() {
super(profile) super(profile)
if (Registry.getInstance().get('platform').api.isDesktop()) {
this.isDesktop = true
}
} }
async handleLoad (gistId: string | null, cb: GistCallBackFn) { async handleLoad(gistId: string | null, cb: GistCallBackFn) {
if (!cb) cb = () => {} if (!cb) cb = () => { }
let loadingFromGist = false let loadingFromGist = false
if (!gistId) { if (!gistId) {
@ -36,7 +41,7 @@ export class GistHandler extends Plugin {
title: 'Load a Gist', title: 'Load a Gist',
message: 'Enter the ID of the Gist or URL you would like to load.', message: 'Enter the ID of the Gist or URL you would like to load.',
modalType: 'prompt', modalType: 'prompt',
okLabel: 'OK', okLabel: (this.isDesktop ? 'Load and select destination' : 'OK'),
cancelLabel: 'Cancel', cancelLabel: 'Cancel',
okFn: (value) => { okFn: (value) => {
setTimeout(() => resolve(value), 0) setTimeout(() => resolve(value), 0)
@ -86,7 +91,7 @@ export class GistHandler extends Plugin {
return loadingFromGist return loadingFromGist
} }
load (gistId: string | null) { load(gistId: string | null) {
const self = this const self = this
return self.handleLoad(gistId, async (gistId: string | null) => { return self.handleLoad(gistId, async (gistId: string | null) => {
let data: any let data: any
@ -115,34 +120,38 @@ export class GistHandler extends Plugin {
} }
const gistIdWorkspace = 'gist ' + gistId const gistIdWorkspace = 'gist ' + gistId
const workspaces = await this.call('filePanel', 'getWorkspaces')
const found = workspaces.find((workspace) => workspace.name === gistIdWorkspace)
if (found) {
await this.call('notification', 'alert', {
id: 'gistAlert',
message: `workspace "${gistIdWorkspace}" already exists`,
})
return
}
await this.call('filePanel', 'createWorkspace', 'gist ' + gistId, '', true)
await this.call('filePanel', 'switchToWorkspace', { name: 'gist ' + gistId, isLocalHost: false })
const obj: StringByString = {} const obj: StringByString = {}
Object.keys(data.files).forEach((element) => { Object.keys(data.files).forEach((element) => {
const path = element.replace(/\.\.\./g, '/') const path = element.replace(/\.\.\./g, '/')
obj['/' + path] = data.files[element] obj['/' + path] = data.files[element]
}) })
this.call('fileManager', 'setBatchFiles', obj, isElectron()? 'electron':'workspace', true, async (errorSavingFiles: any) => {
if (errorSavingFiles) {
const modalContent = {
id: 'gisthandler',
title: 'Gist load error',
message: errorSavingFiles.message || errorSavingFiles
} if (this.isDesktop) {
this.call('notification', 'alert', modalContent) await this.call('remix-templates', 'loadFilesInNewWindow', obj)
} else {
const workspaces = await this.call('filePanel', 'getWorkspaces')
const found = workspaces.find((workspace) => workspace.name === gistIdWorkspace)
if (found) {
await this.call('notification', 'alert', {
id: 'gistAlert',
message: `workspace "${gistIdWorkspace}" already exists`,
})
return
} }
}) await this.call('filePanel', 'createWorkspace', 'gist ' + gistId, '', true)
await this.call('filePanel', 'switchToWorkspace', { name: 'gist ' + gistId, isLocalHost: false })
this.call('fileManager', 'setBatchFiles', obj, isElectron() ? 'electron' : 'workspace', true, async (errorSavingFiles: any) => {
if (errorSavingFiles) {
const modalContent = {
id: 'gisthandler',
title: 'Gist load error',
message: errorSavingFiles.message || errorSavingFiles
}
this.call('notification', 'alert', modalContent)
}
})
}
}) })
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save