From 2bc07022915e658cb3f84ed5cc3957caa6b66608 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Mon, 5 Feb 2024 00:57:17 +0300 Subject: [PATCH] File tree initial support --- package-lock.json | 357 ++++++++++++++++++++-- package.json | 5 +- src/components/App/App.tsx | 155 +--------- src/components/App/spinner.svg | 1 - src/components/App/styles.module.css | 103 +++---- src/components/Editor/Editor.tsx | 304 ++++++++++++++++++ src/components/FileTree/FileTree.tsx | 78 +++++ src/components/FileTree/Node.module.css | 115 +++++++ src/components/FileTree/Node.tsx | 92 ++++++ src/components/FileTree/styles.module.css | 103 +++++++ src/engines/wasi/editorFS.ts | 5 +- src/stubs/bundler_stub.rb | 1 + 12 files changed, 1083 insertions(+), 236 deletions(-) delete mode 100644 src/components/App/spinner.svg create mode 100644 src/components/Editor/Editor.tsx create mode 100644 src/components/FileTree/FileTree.tsx create mode 100644 src/components/FileTree/Node.module.css create mode 100644 src/components/FileTree/Node.tsx create mode 100644 src/components/FileTree/styles.module.css diff --git a/package-lock.json b/package-lock.json index 3eb4e64..8415397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,11 @@ "@monaco-editor/react": "^4.4.4", "@ruby/3.3-wasm-wasi": "^2.5.0", "@ruby/wasm-wasi": "^2.5.0", + "nanoid": "^5.0.5", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-arborist": "^3.4.0", + "react-dom": "^18.2.0", + "react-icons": "^5.0.1" }, "devDependencies": { "@types/react": "^18.2.47", @@ -297,6 +300,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.22.15", "dev": true, @@ -428,6 +442,21 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@react-dnd/asap": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", + "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==" + }, + "node_modules/@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, "node_modules/@rollup/pluginutils": { "version": "5.1.0", "dev": true, @@ -732,7 +761,6 @@ }, "node_modules/@types/node": { "version": "20.11.10", - "dev": true, "license": "MIT", "optional": true, "peer": true, @@ -742,12 +770,12 @@ }, "node_modules/@types/prop-types": { "version": "15.7.11", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.2.48", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -765,7 +793,7 @@ }, "node_modules/@types/scheduler": { "version": "0.16.8", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@vitejs/plugin-react": { @@ -929,7 +957,7 @@ }, "node_modules/csstype": { "version": "3.1.3", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -948,6 +976,24 @@ } } }, + "node_modules/dnd-core": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", + "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", + "dependencies": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + } + }, + "node_modules/dnd-core/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/dot-case": { "version": "3.0.4", "dev": true, @@ -1039,6 +1085,11 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/fsevents": { "version": "2.3.3", "dev": true, @@ -1075,6 +1126,14 @@ "node": ">=4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "dev": true, @@ -1168,6 +1227,11 @@ "yallist": "^3.0.2" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/monaco-editor": { "version": "0.45.0", "license": "MIT", @@ -1179,20 +1243,20 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "dev": true, + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.5.tgz", + "integrity": "sha512-/Veqm+QKsyMY3kqi4faWplnY1u+VuKO3dD2binyPIybP31DRO29bPF+1mszgLnrR2KqSLceFLBNw0zmvDzN1QQ==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/no-case": { @@ -1288,6 +1352,24 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prettier": { "version": "3.2.4", "dev": true, @@ -1312,6 +1394,59 @@ "node": ">=0.10.0" } }, + "node_modules/react-arborist": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.4.0.tgz", + "integrity": "sha512-QI46oRGXJr0oaQfqqVobIiIoqPp5Y5gM69D2A2P7uHVif+X75XWnScR5drC7YDKgJ4CXVaDeFwnYKOWRRfncMg==", + "dependencies": { + "react-dnd": "^14.0.3", + "react-dnd-html5-backend": "^14.0.3", + "react-window": "^1.8.10", + "redux": "^5.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": ">= 16.14", + "react-dom": ">= 16.14" + } + }, + "node_modules/react-dnd": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz", + "integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==", + "dependencies": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz", + "integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==", + "dependencies": { + "dnd-core": "14.0.1" + } + }, "node_modules/react-dom": { "version": "18.2.0", "license": "MIT", @@ -1323,6 +1458,19 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", + "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-refresh": { "version": "0.14.0", "dev": true, @@ -1331,6 +1479,32 @@ "node": ">=0.10.0" } }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/resolve-from": { "version": "4.0.0", "dev": true, @@ -1448,7 +1622,6 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "dev": true, "license": "MIT", "optional": true, "peer": true @@ -1482,6 +1655,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "5.0.12", "dev": true, @@ -1721,6 +1902,14 @@ "@babel/helper-plugin-utils": "^7.22.5" } }, + "@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, "@babel/template": { "version": "7.22.15", "dev": true, @@ -1806,6 +1995,21 @@ "@monaco-editor/loader": "^1.4.0" } }, + "@react-dnd/asap": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", + "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==" + }, + "@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, "@rollup/pluginutils": { "version": "5.1.0", "dev": true, @@ -1960,7 +2164,6 @@ }, "@types/node": { "version": "20.11.10", - "dev": true, "optional": true, "peer": true, "requires": { @@ -1969,11 +2172,11 @@ }, "@types/prop-types": { "version": "15.7.11", - "dev": true + "devOptional": true }, "@types/react": { "version": "18.2.48", - "dev": true, + "devOptional": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1989,7 +2192,7 @@ }, "@types/scheduler": { "version": "0.16.8", - "dev": true + "devOptional": true }, "@vitejs/plugin-react": { "version": "4.2.1", @@ -2071,7 +2274,7 @@ }, "csstype": { "version": "3.1.3", - "dev": true + "devOptional": true }, "debug": { "version": "4.3.4", @@ -2080,6 +2283,26 @@ "ms": "2.1.2" } }, + "dnd-core": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", + "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", + "requires": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + }, + "dependencies": { + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + } + } + }, "dot-case": { "version": "3.0.4", "dev": true, @@ -2144,6 +2367,11 @@ "version": "2.0.2", "dev": true }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "fsevents": { "version": "2.3.3", "dev": true, @@ -2161,6 +2389,14 @@ "version": "3.0.0", "dev": true }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "import-fresh": { "version": "3.3.0", "dev": true, @@ -2219,6 +2455,11 @@ "yallist": "^3.0.2" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "monaco-editor": { "version": "0.45.0", "peer": true @@ -2228,8 +2469,9 @@ "dev": true }, "nanoid": { - "version": "3.3.7", - "dev": true + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.5.tgz", + "integrity": "sha512-/Veqm+QKsyMY3kqi4faWplnY1u+VuKO3dD2binyPIybP31DRO29bPF+1mszgLnrR2KqSLceFLBNw0zmvDzN1QQ==" }, "no-case": { "version": "3.0.4", @@ -2279,6 +2521,14 @@ "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" + }, + "dependencies": { + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + } } }, "prettier": { @@ -2291,6 +2541,38 @@ "loose-envify": "^1.1.0" } }, + "react-arborist": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.4.0.tgz", + "integrity": "sha512-QI46oRGXJr0oaQfqqVobIiIoqPp5Y5gM69D2A2P7uHVif+X75XWnScR5drC7YDKgJ4CXVaDeFwnYKOWRRfncMg==", + "requires": { + "react-dnd": "^14.0.3", + "react-dnd-html5-backend": "^14.0.3", + "react-window": "^1.8.10", + "redux": "^5.0.0", + "use-sync-external-store": "^1.2.0" + } + }, + "react-dnd": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz", + "integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz", + "integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==", + "requires": { + "dnd-core": "14.0.1" + } + }, "react-dom": { "version": "18.2.0", "requires": { @@ -2298,10 +2580,40 @@ "scheduler": "^0.23.0" } }, + "react-icons": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", + "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "requires": {} + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "react-refresh": { "version": "0.14.0", "dev": true }, + "react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, + "redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "resolve-from": { "version": "4.0.0", "dev": true @@ -2376,7 +2688,6 @@ }, "undici-types": { "version": "5.26.5", - "dev": true, "optional": true, "peer": true }, @@ -2388,6 +2699,12 @@ "picocolors": "^1.0.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "vite": { "version": "5.0.12", "dev": true, diff --git a/package.json b/package.json index 6b4e7c2..a7f849b 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,11 @@ "@monaco-editor/react": "^4.4.4", "@ruby/3.3-wasm-wasi": "^2.5.0", "@ruby/wasm-wasi": "^2.5.0", + "nanoid": "^5.0.5", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-arborist": "^3.4.0", + "react-dom": "^18.2.0", + "react-icons": "^5.0.1" }, "devDependencies": { "@types/react": "^18.2.47", diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index eb598e6..98f94b2 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -1,163 +1,14 @@ -import { useEffect, useState } from "react"; -import Editor from "@monaco-editor/react"; - -import { runWASI } from "../../engines/wasi"; import cs from "./styles.module.css"; -import { RbValue } from "@ruby/wasm-wasi"; -import { File, Directory } from "@bjorn3/browser_wasi_shim"; -import { decode, encode, workDir } from "../../engines/wasi/editorFS.ts"; - -export default function App() { - const [loading, setLoading] = useState(true); - // TODO: get first file from workDir - const [code, setCode] = useState(decode((workDir.dir.contents["main.rb"] as File).data)); - const [result, setResult] = useState("Press run..."); - const [log, setLog] = useState([]); - const [editorValueSource, setEditorValueSource] = useState<"result" | "logs">("result"); - // object of gems and their versions as values - const [currentFile, setCurrentFile] = useState(workDir.dir.contents["main.rb"] as File); - - const runVM = (code: string, onSuccess?: (result: RbValue) => void, onError?: Function) => { - setLoading(true); - setLog([]); - setResult(""); - setEditorValueSource("logs"); - const setStdout = (line: string) => { - console.log(line); - setLog((old) => [...old, line]); - }; - const setStderr = (line: string) => { - console.warn(line); - setLog((old) => [...old, `[error] ${line}`]); - }; - // setTimeout is needed to allow the loading status to render - setTimeout(() => - runWASI({ code, setResult, setStdout, setStderr }) - .then((result) => { - setEditorValueSource("result"); - onSuccess && onSuccess(result); - }) - .catch((err) => { - setLog((old) => [...old, `[error] ${err}`]); - setEditorValueSource("logs"); - onError && onError(err); - }) - .finally(() => setLoading(false)) - , 20); - }; +import { Editor } from "../Editor/Editor.tsx"; - const runCode = () => { - runVM(`require "bundler/setup";${code}`) - }; - const bundleInstall = () => { - runVM(`require "bundler/cli";require "bundler/cli/install";Bundler::CLI::Install.new({path: './gems'}).run`, - () => { - setResult("Bundle install successful (see logs for details)") - }, - () => { - setResult("Bundle install failed (see logs for details)") - } - ); - } - - const handleEditorChange = (value: string | undefined) => { - setCode(value || ""); - }; - - useEffect(() => { - currentFile.data = encode(code); - }, [code]) +export default function App() { return (

Ruby WASI Playground

-
- - -
- {Object.keys(workDir.dir.contents).map((file) => ( -
{ - const fileOrDir = workDir.dir.contents[file]; - if (fileOrDir instanceof Directory) { - // - } else if (fileOrDir instanceof File) { - setCurrentFile(fileOrDir); - setCode(decode(fileOrDir.data)); - } - }}> - {file} -
- ))} -
-
- -
-
- setLoading(false)} - options={{ - wordWrap: "on", - minimap: { enabled: false }, - overviewRulerBorder: false, - hideCursorInOverviewRuler: true - }} - /> -
-
-
- {loading && "loading..."} -
- - -
-
- -
-
- - -
-
- -
-
+
); } diff --git a/src/components/App/spinner.svg b/src/components/App/spinner.svg deleted file mode 100644 index 464aec8..0000000 --- a/src/components/App/spinner.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/components/App/styles.module.css b/src/components/App/styles.module.css index 02d396b..23aa3f2 100644 --- a/src/components/App/styles.module.css +++ b/src/components/App/styles.module.css @@ -39,75 +39,33 @@ padding: 16px 8px; } -.menuLabel { - font-size: 14px; +.menuHead { + font-size: 16px; font-weight: bold; color: #f1f1f1; - padding: 8px 8px 16px; - display: block; -} - -.menuFile { - cursor: default; - padding: 8px 8px 8px 16px; - border-radius: 8px; + margin-bottom: 8px; + display: flex; + align-items: center; } -.menuFileActive { - background-color: #424242; +.menuLabel { + flex: 1; } -.menuInputButton { - white-space: nowrap; +.menuButton { + border: none; + background: transparent; + display: inline-flex; + align-items: center; + font-size: 16px; + color: #424242; } -.menuInput { - background-color: transparent; +.menuButton:hover { color: #f1f1f1; - padding: 8px 50px 8px 16px; - text-align: left; - text-decoration: none; - display: inline-block; - font-size: 14px; - margin-bottom: 8px; - border: 1px solid #ffffff14; border-radius: 8px; } -.menuInstallButton { - transform: translateX(-100%); - background-color: #388E3C; - border: none; - color: #f1f1f1; - font-weight: bold; - padding: 8px 16px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 14px; - cursor: pointer; - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; - margin-top: 8px; -} - -.menuDependencies { - padding-left: 16px; -} - -.menuDependency { - padding-bottom: 4px; -} - -.menuSpinner { - fill: #f1f1f1; - width: 16px; - height: 16px; - margin-left: 8px; - display: inline-block; - vertical-align: middle; -} - .editor { grid-area: editor; display: flex; @@ -118,6 +76,29 @@ margin: 0 4px; } +.editorHeader { + display: flex; + flex-direction: row; + padding: 0 16px 0 16px; + margin-bottom: 16px; + align-items: center; + justify-content: space-between; +} + +.editorLabel { + font-size: 16px; + color: #f1f1f1; + font-weight: bold; + padding: 8px 16px; + border-radius: 8px; + background-color: #424242; +} + +.editorPlaceholder { + font-size: 16px; + padding: 8px 16px; +} + .editorText { padding: 0 8px; flex: 1; @@ -149,7 +130,7 @@ text-decoration: none; display: inline-block; font-size: 14px; - cursor: pointer; + cursor: default; border: none; border-radius: 8px; } @@ -172,7 +153,7 @@ text-decoration: none; display: inline-block; font-size: 14px; - cursor: pointer; + cursor: default; border-radius: 8px; } @@ -190,7 +171,7 @@ text-decoration: none; display: inline-block; font-size: 14px; - cursor: pointer; + cursor: default; border-radius: 8px; margin-left: 8px; } @@ -200,5 +181,5 @@ } .buttonDisabled { - background-color: #424242; + background-color: #424242 !important; } diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx new file mode 100644 index 0000000..f90a445 --- /dev/null +++ b/src/components/Editor/Editor.tsx @@ -0,0 +1,304 @@ +import cs from "../App/styles.module.css"; +import MonacoEditor from "@monaco-editor/react"; +import { decode, encode, workDir } from "../../engines/wasi/editorFS.ts"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { Directory, File, SyncOPFSFile } from "@bjorn3/browser_wasi_shim"; +import { RbValue } from "@ruby/wasm-wasi"; +import { runWASI } from "../../engines/wasi"; + +import Node from "../FileTree/Node"; + +export type Entity = { + id: string; + name: string; + object: Directory | File | SyncOPFSFile; +} + +import { CreateHandler, DeleteHandler, RenameHandler, Tree, TreeApi } from "react-arborist"; +import { NodeApi } from "react-arborist/dist/module/interfaces/node-api"; +import { VscNewFile, VscNewFolder } from "react-icons/vsc"; +import { nanoid } from "nanoid"; + +function sortChildren(node: Directory): Entity[] { + const entries = Object.entries(node.contents).map((entry) => { + const id = idsMap.get(entry[1]) || nanoid(); + idsMap.set(entry[1], id); + return { id, name: entry[0], object: entry[1] } + }); + entries.sort((a, b) => { + if (a.object instanceof Directory && b.object instanceof File) return -1; + if (b.object instanceof Directory && a.object instanceof File) return 1; + return a.name < b.name ? -1 : 1; + }); + return entries; +} + +const idsMap = new Map + +function getPath(node: NodeApi) { + let path = node.data.name; + let parent = node.parent; + while (parent && !parent.isRoot) { + path = `${parent.data.name}/${path}`; + parent = parent.parent; + } + return path; +} + +export const Editor = () => { + const [loading, setLoading] = useState(true); + const [result, setResult] = useState("Press run..."); + const [log, setLog] = useState([]); + const [editorValueSource, setEditorValueSource] = useState<"result" | "logs">("result"); + + const [currentFilePath, setCurrentFilePath] = useState("main.rb"); + + const currentFile = useMemo( + () => { + if (currentFilePath === null) return null; + + const pathParts = currentFilePath.split("/"); + let currentDir = workDir.dir; + let currentFile = currentDir.contents[pathParts[0]]; + for (let i = 1; i < pathParts.length; i++) { + if (currentFile instanceof Directory) { + currentDir = currentFile as Directory; + currentFile = currentDir.contents[pathParts[i]]; + } else { + throw new Error(`Invalid path: ${currentFilePath}`); + } + } + return currentFile as File; + }, + [currentFilePath] + ); + + const [code, setCode] = useState(null); + + useEffect(() => { + if (currentFile === null) return; + + setCode(decode(currentFile.data)); + }, [currentFilePath]); + + useEffect(() => { + if (currentFile === null || code === null) return; + + currentFile.data = encode(code); + }, [code]); + + const runVM = (code: string, onSuccess?: (result: RbValue) => void, onError?: Function) => { + setLoading(true); + setLog([]); + setResult(""); + setEditorValueSource("logs"); + const setStdout = (line: string) => { + console.log(line); + setLog((old) => [...old, line]); + }; + const setStderr = (line: string) => { + console.warn(line); + setLog((old) => [...old, `[error] ${line}`]); + }; + // setTimeout is needed to allow the loading status to render + setTimeout(() => + runWASI({ code, setResult, setStdout, setStderr }) + .then((result) => { + setEditorValueSource("result"); + onSuccess && onSuccess(result); + }) + .catch((err) => { + setLog((old) => [...old, `[error] ${err}`]); + setEditorValueSource("logs"); + onError && onError(err); + }) + .finally(() => { + setLoading(false) + setTreeData(sortChildren(workDir.dir)); + }) + , 20); + }; + + const runCode = () => { + if (!currentFilePath?.endsWith('.rb')) return; + if (currentFile === null) return; + + runVM(`require "bundler/setup";${decode(currentFile.data)}`); + }; + const bundleInstall = () => { + runVM(`require "bundler/cli";require "bundler/cli/install";Bundler::CLI::Install.new({path: './gems'}).run`, + () => { + setResult("Bundle install successful (see logs for details)"); + }, + () => { + setResult("Bundle install failed (see logs for details)"); + } + ); + }; + + const handleEditorChange = (value: string | undefined) => { + setCode(value || ""); + }; + const treeRef = useRef>(null); + + const [treeData, setTreeData] = useState(sortChildren(workDir.dir)); + + const onRename: RenameHandler = ({ name, node }) => { + const parent = (node.parent == null || node.parent.isRoot) ? workDir.dir : node.parent.data.object as Directory; + + if (node && node.data.name !== name) { + if (parent.contents[name] !== undefined) { + throw new Error(`File or directory with name ${name} already exists`); + } + parent.contents[name] = node.data.object; + delete parent.contents[node.data.name]; + node.data = {...node.data, name}; + setTreeData(sortChildren(workDir.dir)); + } + }; + + const onCreate: CreateHandler = ({ parentNode, type }) => { + const parent = (parentNode?.data?.object || workDir.dir) as Directory; + const object = (type === "leaf") ? new File(encode("")) : new Directory({}); + const name = (type === "leaf") ? `new_file_${Date.now()}.rb` : `new_dir_${Date.now()}`; + parent.contents[name] = object; + const id = nanoid(); + idsMap.set(object, id); + setTreeData(sortChildren(workDir.dir)); + return { id, name, object }; + }; + + const onDelete: DeleteHandler = ({ ids }) => { + ids.forEach((id) => { + const node = treeRef.current?.get(id); + if (node) { + const parent = (node.parent == null || node.parent.isRoot) ? workDir.dir : node.parent.data.object as Directory; + delete parent.contents[node.data.name]; + if (currentFilePath === getPath(node)) { + setCurrentFilePath(null); + } + } + }); + setTreeData(sortChildren(workDir.dir)); + }; + + const canRunCode = useMemo(() => !loading && currentFilePath?.endsWith('.rb'), [currentFilePath, loading]); + const canRunBundleInstall = useMemo(() => !loading && treeData.find((entry) => entry.name === 'Gemfile'), [loading, treeData]); + + return ( + <> +
+ +
+ + + +
+ (object instanceof Directory) ? sortChildren(object) : null} + ref={treeRef} + disableDrag={true} + disableDrop={true} + onRename={onRename} + onCreate={onCreate} + onDelete={onDelete} + onActivate={(node: NodeApi) => { + if (node.isLeaf) { + setCurrentFilePath(getPath(node)); + } + }} + > + {Node} + +
+ +
+
+ {currentFilePath && } +
+
+ { + currentFilePath && currentFile ? ( + setLoading(false)} + options={{ + wordWrap: "on", + minimap: { enabled: false }, + overviewRulerBorder: false, + hideCursorInOverviewRuler: true + }} + /> + + ) : ( +
+ Select a file to edit +
+ ) + } +
+
+
+ {loading && "loading..."} +
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + ); +}; diff --git a/src/components/FileTree/FileTree.tsx b/src/components/FileTree/FileTree.tsx new file mode 100644 index 0000000..90b1cf0 --- /dev/null +++ b/src/components/FileTree/FileTree.tsx @@ -0,0 +1,78 @@ +import { Directory } from "@bjorn3/browser_wasi_shim"; +import { useState } from "react"; + +import cs from "./styles.module.css"; + +const DirItem = ({ path, rootDir, currentFilePath, setCurrentFilePath }: { + currentFilePath: string, + setCurrentFilePath: (value: (((prevState: string) => string) | string)) => void, + rootDir: Directory + path: string +}) => { + const [isOpen, setIsOpen] = useState(false); + return ( +
+
setIsOpen(!isOpen)}> + {isOpen ? "▼" : "▶"}  {path} +
+
[+ dir]
+
[+ file]
+
[edit]
+
[del]
+
+
+ {isOpen &&
+ +
+ } +
+ ); +}; + +export const FileTree = ({ currentFilePath, setCurrentFilePath, rootDir, rootPath }: { + currentFilePath: string, + setCurrentFilePath: (value: (((prevState: string) => string) | string)) => void, + rootDir: Directory + rootPath?: string +}) => { + return ( +
+ {rootPath ? null : + + } + {Object.keys(rootDir.contents).map((path) => ( + (rootDir.contents[path] instanceof Directory) ? + + : +
{ + setCurrentFilePath(`${rootPath ? `${rootPath}/` : ""}${path}`); + }}> + {path} +
+
[edit]
+
[del]
+
+
+ ))} +
+ ); +}; diff --git a/src/components/FileTree/Node.module.css b/src/components/FileTree/Node.module.css new file mode 100644 index 0000000..1ed75b4 --- /dev/null +++ b/src/components/FileTree/Node.module.css @@ -0,0 +1,115 @@ +.nodeContainer, +.nodeContent { + display: flex; + height: 100%; + align-items: center; + width: 100%; + box-sizing: border-box; +} + +.nodeContent { + cursor: default; +} + +.nodeContent span.arrow { + width: 20px; + font-size: 20px; + display: flex; +} + +.fileFolderIcon { + margin-left: 16px; + margin-right: 8px; + display: flex; + align-items: center; + font-size: 20px; +} + +.nodeContent span.nodeText { + flex: 1; +} + +.nodeContent input[type="text"] { + padding: 0; + outline: none; + color: #f1f1f1; + border-radius: 2px; + font-size: 16px; +} + +.nodeContent input[type="text"] { + width: 97%; + border: 1px solid #878787; + background: transparent; + height: 22px; +} + +.fileActions { + height: 100%; + display: flex; + position: absolute; + right: 0; + /*height: 18px;*/ +} + + +[role="treeitem"]:hover .nodeContainer.isSelected .fileActions { + background: #343434; + color: #f1f1f1; +} + +[role="treeitem"]:hover .fileActions { + background: #202020; + color: #f1f1f1; +} + +.fileActions button { + cursor: default; +} + +.folderFileActions { + display: flex; + flex-direction: row; + align-items: center; +} + +.folderFileActions button { + display: flex; + align-items: center; + color: #424242; + background-color: inherit; + border: none; + font-size: 14px; + height: 100%; + padding: 0 4px; + margin: 0; +} + +.folderFileActions button:hover { + color: #f1f1f1; +} + +.nodeContainer .fileActions .folderFileActions { + visibility: hidden; +} + +.nodeContainer:hover .fileActions .folderFileActions { + visibility: visible; +} + +.nodeContainer.isSelected { + background: #343434; + color: #f1f1f1; + border-radius: 0; +} + +[role="treeitem"]:hover { + background: #202020; + color: #f1f1f1; +} + +[role="treeitem"]:focus-within { + background-color: rgba(52, 52, 52, 0.4); + outline: none; +} + diff --git a/src/components/FileTree/Node.tsx b/src/components/FileTree/Node.tsx new file mode 100644 index 0000000..db35bf7 --- /dev/null +++ b/src/components/FileTree/Node.tsx @@ -0,0 +1,92 @@ +import cs from "./Node.module.css"; +import { NodeRendererProps } from "react-arborist"; +import { VscEdit, VscFile, VscFolder, VscFolderOpened, VscTrash } from "react-icons/vsc"; +import { NodeApi } from "react-arborist/dist/module/interfaces/node-api"; +import { Entity } from "../Editor/Editor.tsx"; + +function isValidFileName(fileName: string) { + if (fileName.trim() === "") { + return false; + } + const invalidCharacters = ["<", ">", ":", "\"", "/", "\\", "|", "?", "*"]; + for (let i = 0; i < invalidCharacters.length; i++) { + if (fileName.includes(invalidCharacters[i])) { + return false; + } + } + return !(fileName.startsWith(" ") || fileName.endsWith(" ")); +} + +function submitNodeName(node: NodeApi, value: string) { + if (isValidFileName(value)) { + node.submit(value); + } +} + +const Node = ({ node, style, dragHandle, tree }: NodeRendererProps) => { + return ( +
+
node.isInternal && node.toggle()} + > + + {node.isLeaf ? ( + + ) : ( + node.isOpen ? ( + + ) : ( + + ) + )} + + + {node.isEditing ? ( + { + if (node.isLeaf) { + e.currentTarget.setSelectionRange(0, node.data.name.lastIndexOf(".")); + } else { + e.currentTarget.select(); + } + }} + onBlur={(e) => submitNodeName(node, e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Escape") node.reset(); + if (e.key === "Enter") submitNodeName(node, e.currentTarget.value); + }} + autoFocus + /> + ) : ( + {node.data.name} + )} + +
+ +
+
+ + +
+
+
+ ); +}; + +export default Node; diff --git a/src/components/FileTree/styles.module.css b/src/components/FileTree/styles.module.css new file mode 100644 index 0000000..f4d3e74 --- /dev/null +++ b/src/components/FileTree/styles.module.css @@ -0,0 +1,103 @@ +.menuLabel { + font-size: 14px; + font-weight: bold; + color: #f1f1f1; + padding: 8px 8px 16px; + display: block; +} + +.menuFolder { +} + +.menuFolderContent { + padding-left: 8px; +} + +.menuFolderName { + cursor: default; + padding: 8px 8px 8px 16px; + border-radius: 8px; +} + +.menuFile { + cursor: default; + padding: 8px 8px 8px 16px; + border-radius: 8px; +} + +.menuFolderName:hover, .menuFile:hover { + background-color: #595959; +} + +.menuFileActive { + background-color: #424242; +} + +.menuInputButton { + white-space: nowrap; +} + +.menuFileButtons { + display: inline-flex; + flex-direction: row; + float: right; +} + +.menuFileButton { + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 12px; +} + +.menuFileButton:hover { + background-color: #fefefe; + color: #1a1a1a; +} + +.menuInput { + background-color: transparent; + color: #f1f1f1; + padding: 8px 50px 8px 16px; + text-align: left; + text-decoration: none; + display: inline-block; + font-size: 14px; + margin-bottom: 8px; + border: 1px solid #ffffff14; + border-radius: 8px; +} + +.menuInstallButton { + transform: translateX(-100%); + background-color: #388E3C; + border: none; + color: #f1f1f1; + font-weight: bold; + padding: 8px 16px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 14px; + cursor: pointer; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + margin-top: 8px; +} + +.menuDependencies { + padding-left: 16px; +} + +.menuDependency { + padding-bottom: 4px; +} + +.menuSpinner { + fill: #f1f1f1; + width: 16px; + height: 16px; + margin-left: 8px; + display: inline-block; + vertical-align: middle; +} diff --git a/src/engines/wasi/editorFS.ts b/src/engines/wasi/editorFS.ts index 181e758..ca04d15 100644 --- a/src/engines/wasi/editorFS.ts +++ b/src/engines/wasi/editorFS.ts @@ -1,4 +1,4 @@ -import { File, PreopenDirectory } from "@bjorn3/browser_wasi_shim"; +import { Directory, File, PreopenDirectory } from "@bjorn3/browser_wasi_shim"; export const initialRubyCode = `# This is a Ruby WASI playground # You can run any Ruby code here and see the result @@ -27,4 +27,7 @@ export const decode = (() => { export const workDir = new PreopenDirectory("/", { "Gemfile": new File(encode(initialGemfile)), "main.rb": new File(encode(initialRubyCode)), + "tmp": new Directory({ + "file.txt": new File(encode("Hello, World!")) + }) }); diff --git a/src/stubs/bundler_stub.rb b/src/stubs/bundler_stub.rb index b6ce127..cffa736 100644 --- a/src/stubs/bundler_stub.rb +++ b/src/stubs/bundler_stub.rb @@ -29,6 +29,7 @@ def deq end) ENV["BUNDLE_SSL_VERIFY_MODE"] = "0" +ENV["BUNDLE_SILENCE_ROOT_WARNING"] = "1" require "js/connection"