浏览代码

build: migrate to Vite 🚀 (#6818)

* init

* add: vite dev build working

* fix: href serving from public

* feat: add ejs plugin

* feat: migrated env files and ejs templating

* chore: add types related to envs

* chore: add vite-env types

* feat: support vite pwa

* chore: upgrade vite pwa

* chore: pin node version to 16.18.1

* chore: preserve use of nodejs 14

* refactor: preserve REACT_APP as env prefix

* chore: support esm environment variables

* fix ts config

* use VITE prefix and remove vite-plugin-env-compatible

* introduce import-meta-loader for building pacakge as webpack isn't compatible with import.meta syntax

* lint

* remove import.meta.env in main.js

* set debug flag to false

* migrate to vitest and use jest-canvas-mock 2.4.0 so its comp
atible with vite

* integrate vitest-ui

* fix most of teh test

* snaps

* Add script for testing with vite ui

* fix all tests related to mocking

* fix more test

* fix more

* fix flip.test.tsx

* fix contentxmenu snaps

* fix regression snaps

* fix excalidraw.test.tsx and this makes all tests finally pass :)

* use node 16

* specify node version

* use node 16 in lint as well

* fix mobile.test.tsx

* use node 16

* add style-loader

* upgrade to node 18

* fix lint package.json

* support eslint with vite

* fix lint

* fix lint

* fix ts

* remove pwa/sw stuff

* use env vars in EJS the vite way

* fix lint

* move remainig jest mock/spy to vite

* don't cache locales

* fix regex

* add fonts cache

* tweak

* add custom service worker

* upgrade vite and create font cache again

* cache fonts.css and locales

* tweak

* use manifestTransforms for filtering locales

* use assets js pattern for locales

* add font.css to globIgnore so its pushed to fonts cache

* create a separate chunk for locales with rollup

* remove manifestTransforms and fix glob pattern for locales to filter from workbox pre-cache

* push sourcemaps in production

* add comments in config

* lint

* use node 18

* disable pwa in dev

* fix

* fix

* increase limit of bundle

* upgrade vite-pwa to latest

* remove public/workbox so workbox assets are not precached

* fon't club en.json and percentages.json with manual locales chunk to fix first load+offline mode

* tweak regex

* remove happy-dom as its not used

* add comment

* use any instead of ts-ignore

* cleanup

* remove jest-canvas-mock resolution as vite-canvas-mock was patched locking deps at 2.4.0

* use same theme color present in entry point

* remove vite-plugin-eslint as it improves DX significantly

* integrate vite-plugin-checker for ts errors

* add nabla/vite-plugin-eslint

* use eslint from checker only

* add env variable VITE_APP_COLLAPSE_OVERLAY for collapsing the checker overlay

* tweak vite checker overlay badge position

* Enable eslint behind flag as its not working well with windows with non WSL

* make port configurable

* open the browser when server ready

* enable eslint by default

---------

Co-authored-by: Weslley Braga <[email protected]>
Co-authored-by: dwelle <[email protected]>
Aakansha Doshi 2 年之前
父节点
当前提交
48924688c7
共有 100 个文件被更改,包括 1677 次插入1657 次删除
  1. 20 11
      .env.development
  2. 8 8
      .env.production
  3. 2 2
      .github/workflows/autorelease-excalidraw.yml
  4. 2 2
      .github/workflows/autorelease-preview.yml
  5. 2 2
      .github/workflows/lint.yml
  6. 2 2
      .github/workflows/locales-coverage.yml
  7. 2 2
      .github/workflows/sentry-production.yml
  8. 2 2
      .github/workflows/test.yml
  9. 2 0
      .gitignore
  10. 9 15
      index.html
  11. 24 41
      package.json
  12. 0 0
      public/workbox/workbox-background-sync.prod.js
  13. 0 2
      public/workbox/workbox-broadcast-update.prod.js
  14. 0 2
      public/workbox/workbox-cacheable-response.prod.js
  15. 0 0
      public/workbox/workbox-core.prod.js
  16. 0 0
      public/workbox/workbox-expiration.prod.js
  17. 0 2
      public/workbox/workbox-navigation-preload.prod.js
  18. 0 2
      public/workbox/workbox-offline-ga.prod.js
  19. 0 0
      public/workbox/workbox-precaching.prod.js
  20. 0 2
      public/workbox/workbox-range-requests.prod.js
  21. 0 0
      public/workbox/workbox-routing.prod.js
  22. 0 0
      public/workbox/workbox-strategies.prod.js
  23. 0 2
      public/workbox/workbox-streams.prod.js
  24. 0 2
      public/workbox/workbox-sw.js
  25. 0 0
      public/workbox/workbox-window.prod.es5.mjs
  26. 0 0
      public/workbox/workbox-window.prod.mjs
  27. 0 0
      public/workbox/workbox-window.prod.umd.js
  28. 1 1
      src/analytics.ts
  29. 2 3
      src/charts.ts
  30. 2 1
      src/components/App.test.tsx
  31. 4 9
      src/components/App.tsx
  32. 1 1
      src/components/LibraryMenuBrowseButton.tsx
  33. 1 1
      src/components/PublishLibrary.tsx
  34. 2 1
      src/components/Sidebar/Sidebar.test.tsx
  35. 1 1
      src/components/Sidebar/Sidebar.tsx
  36. 2 2
      src/components/__snapshots__/App.test.tsx.snap
  37. 1 1
      src/element/newElement.ts
  38. 17 1
      src/element/sizeHelpers.test.ts
  39. 12 12
      src/element/textWysiwyg.test.tsx
  40. 2 8
      src/excalidraw-app/collab/Collab.tsx
  41. 3 1
      src/excalidraw-app/components/AppWelcomeScreen.tsx
  42. 3 1
      src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx
  43. 4 2
      src/excalidraw-app/data/firebase.ts
  44. 5 5
      src/excalidraw-app/data/index.ts
  45. 0 31
      src/excalidraw-app/pwa.ts
  46. 2 2
      src/excalidraw-app/sentry.ts
  47. 0 10
      src/global.d.ts
  48. 2 3
      src/i18n.ts
  49. 1 2
      src/index.tsx
  50. 3 3
      src/packages/excalidraw/env.js
  51. 1 0
      src/packages/excalidraw/package.json
  52. 1 1
      src/packages/excalidraw/publicPath.js
  53. 3 0
      src/packages/excalidraw/webpack.dev.config.js
  54. 3 0
      src/packages/excalidraw/webpack.prod.config.js
  55. 5 0
      src/packages/excalidraw/yarn.lock
  56. 1 1
      src/renderer/renderElement.ts
  57. 4 3
      src/scene/export.ts
  58. 0 147
      src/service-worker.ts
  59. 0 162
      src/serviceWorkerRegistration.ts
  60. 5 8
      src/setupTests.ts
  61. 5 6
      src/tests/MobileMenu.test.tsx
  62. 2 2
      src/tests/__snapshots__/MobileMenu.test.tsx.snap
  63. 6 6
      src/tests/__snapshots__/charts.test.tsx.snap
  64. 183 183
      src/tests/__snapshots__/contextmenu.test.tsx.snap
  65. 31 31
      src/tests/__snapshots__/dragCreate.test.tsx.snap
  66. 2 2
      src/tests/__snapshots__/export.test.tsx.snap
  67. 3 3
      src/tests/__snapshots__/linearElementEditor.test.tsx.snap
  68. 34 34
      src/tests/__snapshots__/move.test.tsx.snap
  69. 19 19
      src/tests/__snapshots__/multiPointCreate.test.tsx.snap
  70. 182 182
      src/tests/__snapshots__/regressionTests.test.tsx.snap
  71. 27 27
      src/tests/__snapshots__/selection.test.tsx.snap
  72. 1 0
      src/tests/appState.test.tsx
  73. 5 4
      src/tests/clipboard.test.tsx
  74. 23 17
      src/tests/collab.test.tsx
  75. 2 1
      src/tests/contextmenu.test.tsx
  76. 57 57
      src/tests/data/__snapshots__/restore.test.ts.snap
  77. 6 5
      src/tests/data/restore.test.ts
  78. 2 1
      src/tests/dragCreate.test.tsx
  79. 3 2
      src/tests/fitToContent.test.tsx
  80. 13 14
      src/tests/flip.test.tsx
  81. 10 5
      src/tests/library.test.tsx
  82. 151 150
      src/tests/linearElementEditor.test.tsx
  83. 2 1
      src/tests/move.test.tsx
  84. 2 1
      src/tests/multiPointCreate.test.tsx
  85. 3 3
      src/tests/packages/__snapshots__/excalidraw.test.tsx.snap
  86. 11 11
      src/tests/packages/__snapshots__/utils.test.ts.snap
  87. 6 4
      src/tests/packages/excalidraw.test.tsx
  88. 13 17
      src/tests/packages/utils.test.ts
  89. 3 1
      src/tests/regressionTests.test.tsx
  90. 2 1
      src/tests/resize.test.tsx
  91. 3 3
      src/tests/scene/__snapshots__/export.test.ts.snap
  92. 1 1
      src/tests/scene/export.test.ts
  93. 2 1
      src/tests/selection.test.tsx
  94. 2 2
      src/utils.ts
  95. 59 0
      src/vite-env.d.ts
  96. 2 1
      tsconfig-types.json
  97. 3 3
      tsconfig.json
  98. 181 0
      vite.config.ts
  99. 9 0
      vitest.config.ts
  100. 437 331
      yarn.lock

+ 20 - 11
.env.development

@@ -1,30 +1,39 @@
-REACT_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
-REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
+VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
+VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
 
-REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
-REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
+VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
+VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
 
 # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
-REACT_APP_WS_SERVER_URL=http://localhost:3002
+VITE_APP_WS_SERVER_URL=http://localhost:3002
 
 # set this only if using the collaboration workflow we use on excalidraw.com
-REACT_APP_PORTAL_URL=
+VITE_APP_PORTAL_URL=
 
-REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
+VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
 
 # put these in your .env.local, or make sure you don't commit!
 # must be lowercase `true` when turned on
 #
 # whether to enable Service Workers in development
-REACT_APP_DEV_ENABLE_SW=
+VITE_APP_DEV_ENABLE_SW=
 # whether to disable live reload / HMR. Usuaully what you want to do when
 # debugging Service Workers.
-REACT_APP_DEV_DISABLE_LIVE_RELOAD=
-REACT_APP_DISABLE_TRACKING=true
+VITE_APP_DEV_DISABLE_LIVE_RELOAD=
+VITE_APP_DISABLE_TRACKING=true
 
 FAST_REFRESH=false
 
+# The port the run the dev server
+VITE_APP_PORT=3000
+
 #Debug flags
 
 # To enable bounding box for text containers
-REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
+VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
+
+# Set this flag to false if you want to open the overlay by default
+VITE_APP_COLLAPSE_OVERLAY=true
+
+# Set this flag to false to disable eslint
+VITE_APP_ENABLE_ESLINT=true

+ 8 - 8
.env.production

@@ -1,15 +1,15 @@
 REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
 REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
 
-REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
-REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
+VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
+VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
 
-REACT_APP_PORTAL_URL=https://portal.excalidraw.com
+VITE_APP_PORTAL_URL=https://portal.excalidraw.com
 # Fill to set socket server URL used for collaboration.
-# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
-REACT_APP_WS_SERVER_URL=
+# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
+VITE_APP_WS_SERVER_URL=
 
-REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
+VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
 
-REACT_APP_PLUS_APP=https://app.excalidraw.com
-REACT_APP_DISABLE_TRACKING=
+VITE_APP_PLUS_APP=https://app.excalidraw.com
+VITE_APP_DISABLE_TRACKING=

+ 2 - 2
.github/workflows/autorelease-excalidraw.yml

@@ -12,10 +12,10 @@ jobs:
       - uses: actions/checkout@v2
         with:
           fetch-depth: 2
-      - name: Setup Node.js 14.x
+      - name: Setup Node.js 18.x
         uses: actions/setup-node@v2
         with:
-          node-version: 14.x
+          node-version: 18.x
       - name: Set up publish access
         run: |
           npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}

+ 2 - 2
.github/workflows/autorelease-preview.yml

@@ -32,10 +32,10 @@ jobs:
         with:
           ref: ${{ steps.sha.outputs.result }}
           fetch-depth: 2
-      - name: Setup Node.js 14.x
+      - name: Setup Node.js 18.x
         uses: actions/setup-node@v2
         with:
-          node-version: 14.x
+          node-version: 18.x
       - name: Set up publish access
         run: |
           npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}

+ 2 - 2
.github/workflows/lint.yml

@@ -9,10 +9,10 @@ jobs:
     steps:
       - uses: actions/checkout@v2
 
-      - name: Setup Node.js 14.x
+      - name: Setup Node.js 18.x
         uses: actions/setup-node@v2
         with:
-          node-version: 14.x
+          node-version: 18.x
 
       - name: Install and lint
         run: |

+ 2 - 2
.github/workflows/locales-coverage.yml

@@ -14,10 +14,10 @@ jobs:
         with:
           token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
 
-      - name: Setup Node.js 14.x
+      - name: Setup Node.js 18.x
         uses: actions/setup-node@v2
         with:
-          node-version: 14.x
+          node-version: 18.x
 
       - name: Create report file
         run: |

+ 2 - 2
.github/workflows/sentry-production.yml

@@ -10,10 +10,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Setup Node.js 14.x
+      - name: Setup Node.js 18.x
         uses: actions/setup-node@v2
         with:
-          node-version: 14.x
+          node-version: 18.x
       - name: Install and build
         run: |
           yarn --frozen-lockfile

+ 2 - 2
.github/workflows/test.yml

@@ -7,10 +7,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - name: Setup Node.js 14.x
+      - name: Setup Node.js 18.x
         uses: actions/setup-node@v2
         with:
-          node-version: 14.x
+          node-version: 18.x
       - name: Install and test
         run: |
           yarn --frozen-lockfile

+ 2 - 0
.gitignore

@@ -26,3 +26,5 @@ src/packages/excalidraw/example/public/bundle.js
 src/packages/excalidraw/example/public/excalidraw-assets-dev
 src/packages/excalidraw/example/public/excalidraw.development.js
 coverage
+dev-dist
+html

+ 9 - 15
public/index.html → index.html

@@ -78,8 +78,7 @@
       }
     </style>
     <!------------------------------------------------------------------------->
-
-    <% if (process.env.NODE_ENV === "production") { %>
+    <% if ("%PROD%" === "true") { %>
     <script>
       // Redirect Excalidraw+ users which have auto-redirect enabled.
       //
@@ -100,41 +99,35 @@
     </script>
     <% } %>
 
-    <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
+    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
 
     <!-- Excalidraw version -->
     <meta name="version" content="{version}" />
 
     <link
       rel="preload"
-      href="Virgil.woff2"
+      href="/Virgil.woff2"
       as="font"
       type="font/woff2"
       crossorigin="anonymous"
     />
     <link
       rel="preload"
-      href="Cascadia.woff2"
+      href="/Cascadia.woff2"
       as="font"
       type="font/woff2"
       crossorigin="anonymous"
     />
 
-    <link
-      rel="manifest"
-      href="manifest.json"
-      style="--pwacompat-splash-font: 24px Virgil"
-    />
-
-    <link rel="stylesheet" href="fonts.css" type="text/css" />
-    <% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD==="true" ) { %>
+    <link rel="stylesheet" href="/fonts.css" type="text/css" />
+    <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
     <script>
       {
         const _WebSocket = window.WebSocket;
         window.WebSocket = function (url) {
           if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
             console.info(
-              "[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
+              "[!!!] Live reload is disabled via VITE_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
             );
           } else {
             return new _WebSocket(url);
@@ -200,7 +193,8 @@
       <h1 class="visually-hidden">Excalidraw</h1>
     </header>
     <div id="root"></div>
-    <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
+    <script type="module" src="/src/index.tsx"></script>
+    <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
     <!-- 100% privacy friendly analytics -->
     <script>
       // need to load this script dynamically bcs. of iframe embed tracking

+ 24 - 41
package.json

@@ -32,6 +32,7 @@
     "canvas-roundrect-polyfill": "0.0.1",
     "clsx": "1.1.1",
     "cross-env": "7.0.3",
+    "eslint-plugin-react": "7.32.2",
     "fake-indexeddb": "3.1.7",
     "firebase": "8.3.3",
     "i18next-browser-languagedetector": "6.1.4",
@@ -51,26 +52,13 @@
     "pwacompat": "2.0.17",
     "react": "18.2.0",
     "react-dom": "18.2.0",
-    "react-scripts": "5.0.1",
     "roughjs": "4.5.2",
     "sass": "1.51.0",
     "socket.io-client": "2.3.1",
-    "tunnel-rat": "0.1.2",
-    "workbox-background-sync": "^6.5.4",
-    "workbox-broadcast-update": "^6.5.4",
-    "workbox-cacheable-response": "^6.5.4",
-    "workbox-core": "^6.5.4",
-    "workbox-expiration": "^6.5.4",
-    "workbox-google-analytics": "^6.5.4",
-    "workbox-navigation-preload": "^6.5.4",
-    "workbox-precaching": "^6.5.4",
-    "workbox-range-requests": "^6.5.4",
-    "workbox-routing": "^6.5.4",
-    "workbox-strategies": "^6.5.4",
-    "workbox-streams": "^6.5.4"
+    "tunnel-rat": "0.1.2"
   },
   "devDependencies": {
-    "@excalidraw/eslint-config": "1.0.0",
+    "@excalidraw/eslint-config": "1.0.3",
     "@excalidraw/prettier-config": "1.0.2",
     "@types/chai": "4.3.0",
     "@types/jest": "27.4.0",
@@ -81,48 +69,42 @@
     "@types/react-dom": "18.0.6",
     "@types/resize-observer-browser": "0.1.7",
     "@types/socket.io-client": "1.4.36",
+    "@vitejs/plugin-react": "3.1.0",
+    "@vitest/ui": "0.32.2",
     "chai": "4.3.6",
     "dotenv": "16.0.1",
     "eslint-config-prettier": "8.5.0",
+    "eslint-config-react-app": "7.0.1",
     "eslint-plugin-prettier": "3.3.1",
     "http-server": "14.1.1",
     "husky": "7.0.4",
-    "jest-canvas-mock": "2.4.0",
+    "jsdom": "22.1.0",
     "lint-staged": "12.3.7",
     "pepjs": "0.5.3",
     "prettier": "2.6.2",
     "rewire": "6.0.0",
-    "typescript": "4.9.4"
+    "typescript": "4.9.4",
+    "vite": "4.4.2",
+    "vite-plugin-checker": "0.6.1",
+    "vite-plugin-ejs": "1.6.4",
+    "vite-plugin-pwa": "0.16.4",
+    "vite-plugin-svgr": "2.4.0",
+    "vitest": "0.32.2",
+    "vitest-canvas-mock": "0.3.2"
   },
   "engines": {
-    "node": ">=14.0.0"
+    "node": ">=18.0.0"
   },
   "homepage": ".",
-  "jest": {
-    "collectCoverageFrom": [
-      "src/**/*.{js,jsx,ts,tsx}"
-    ],
-    "coveragePathIgnorePatterns": [
-      "<rootDir>/locales",
-      "<rootDir>/src/packages/excalidraw/dist/",
-      "<rootDir>/src/packages/excalidraw/types",
-      "<rootDir>/src/packages/excalidraw/example"
-    ],
-    "transformIgnorePatterns": [
-      "node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access|canvas-roundrect-polyfill)/)"
-    ],
-    "resetMocks": false
-  },
   "name": "excalidraw",
   "prettier": "@excalidraw/prettier-config",
   "private": true,
   "scripts": {
     "build-node": "node ./scripts/build-node.js",
-    "build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true react-scripts build",
-    "build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
+    "build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true vite build",
+    "build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
     "build:version": "node ./scripts/build-version.js",
     "build": "yarn build:app && yarn build:version",
-    "eject": "react-scripts eject",
     "fix:code": "yarn test:code --fix",
     "fix:other": "yarn prettier --write",
     "fix": "yarn fix:other && yarn fix:code",
@@ -130,19 +112,20 @@
     "locales-coverage:description": "node scripts/locales-coverage-description.js",
     "prepare": "husky install",
     "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
-    "start": "react-scripts start",
+    "start": "vite",
     "start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
     "test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
-    "test:app": "react-scripts test --passWithNoTests",
+    "test:app": "vitest --config vitest.config.ts",
     "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
-    "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
     "test:other": "yarn prettier --list-different",
     "test:typecheck": "tsc",
-    "test:update": "yarn test:app --updateSnapshot --watchAll=false",
+    "test:update": "yarn test:app --update --watch=false",
     "test": "yarn test:app",
-    "test:coverage": "react-scripts test --passWithNoTests --coverage --watchAll",
+    "test:coverage": "vitest --coverage --watchAll",
+    "test:ui": "yarn test --ui",
     "autorelease": "node scripts/autorelease.js",
     "prerelease": "node scripts/prerelease.js",
+    "build:preview": "yarn build && vite preview --port 5000",
     "release": "node scripts/release.js"
   }
 }

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-background-sync.prod.js


+ 0 - 2
public/workbox/workbox-broadcast-update.prod.js

@@ -1,2 +0,0 @@
-this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(e,t){"use strict";try{self["workbox:broadcast-update:4.3.1"]&&_()}catch(e){}const s=(e,t,s)=>{return!s.some(s=>e.headers.has(s)&&t.headers.has(s))||s.every(s=>{const n=e.headers.has(s)===t.headers.has(s),a=e.headers.get(s)===t.headers.get(s);return n&&a})},n="workbox",a=1e4,i=["content-length","etag","last-modified"],o=async({channel:e,cacheName:t,url:s})=>{const n={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:{cacheName:t,updatedURL:s}};if(e)e.postMessage(n);else{const e=await clients.matchAll({type:"window"});for(const t of e)t.postMessage(n)}};class c{constructor({headersToCheck:e,channelName:t,deferNoticationTimeout:s}={}){this.t=e||i,this.s=t||n,this.i=s||a,this.o()}notifyIfUpdated({oldResponse:e,newResponse:t,url:n,cacheName:a,event:i}){if(!s(e,t,this.t)){const e=(async()=>{i&&i.request&&"navigate"===i.request.mode&&await this.h(i),await this.l({channel:this.u(),cacheName:a,url:n})})();if(i)try{i.waitUntil(e)}catch(e){}return e}}async l(e){await o(e)}u(){return"BroadcastChannel"in self&&!this.p&&(this.p=new BroadcastChannel(this.s)),this.p}h(e){if(!this.m.has(e)){const s=new t.Deferred;this.m.set(e,s);const n=setTimeout(()=>{s.resolve()},this.i);s.promise.then(()=>clearTimeout(n))}return this.m.get(e).promise}o(){this.m=new Map,self.addEventListener("message",e=>{if("WINDOW_READY"===e.data.type&&"workbox-window"===e.data.meta&&this.m.size>0){for(const e of this.m.values())e.resolve();this.m.clear()}})}}return e.BroadcastCacheUpdate=c,e.Plugin=class{constructor(e){this.l=new c(e)}cacheDidUpdate({cacheName:e,oldResponse:t,newResponse:s,request:n,event:a}){t&&this.l.notifyIfUpdated({cacheName:e,oldResponse:t,newResponse:s,event:a,url:n.url})}},e.broadcastUpdate=o,e.responsesAreSame=s,e}({},workbox.core._private);
-//# sourceMappingURL=workbox-broadcast-update.prod.js.map

+ 0 - 2
public/workbox/workbox-cacheable-response.prod.js

@@ -1,2 +0,0 @@
-this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(t){"use strict";try{self["workbox:cacheable-response:4.3.1"]&&_()}catch(t){}class s{constructor(t={}){this.t=t.statuses,this.s=t.headers}isResponseCacheable(t){let s=!0;return this.t&&(s=this.t.includes(t.status)),this.s&&s&&(s=Object.keys(this.s).some(s=>t.headers.get(s)===this.s[s])),s}}return t.CacheableResponse=s,t.Plugin=class{constructor(t){this.i=new s(t)}cacheWillUpdate({response:t}){return this.i.isResponseCacheable(t)?t:null}},t}({});
-//# sourceMappingURL=workbox-cacheable-response.prod.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-core.prod.js


文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-expiration.prod.js


+ 0 - 2
public/workbox/workbox-navigation-preload.prod.js

@@ -1,2 +0,0 @@
-this.workbox=this.workbox||{},this.workbox.navigationPreload=function(t){"use strict";try{self["workbox:navigation-preload:4.3.1"]&&_()}catch(t){}function e(){return Boolean(self.registration&&self.registration.navigationPreload)}return t.disable=function(){e()&&self.addEventListener("activate",t=>{t.waitUntil(self.registration.navigationPreload.disable().then(()=>{}))})},t.enable=function(t){e()&&self.addEventListener("activate",e=>{e.waitUntil(self.registration.navigationPreload.enable().then(()=>{t&&self.registration.navigationPreload.setHeaderValue(t)}))})},t.isSupported=e,t}({});
-//# sourceMappingURL=workbox-navigation-preload.prod.js.map

+ 0 - 2
public/workbox/workbox-offline-ga.prod.js

@@ -1,2 +0,0 @@
-this.workbox=this.workbox||{},this.workbox.googleAnalytics=function(e,t,o,n,a,c,w){"use strict";try{self["workbox:google-analytics:4.3.1"]&&_()}catch(e){}const r=/^\/(\w+\/)?collect/,s=e=>async({queue:t})=>{let o;for(;o=await t.shiftRequest();){const{request:n,timestamp:a}=o,c=new URL(n.url);try{const w="POST"===n.method?new URLSearchParams(await n.clone().text()):c.searchParams,r=a-(Number(w.get("qt"))||0),s=Date.now()-r;if(w.set("qt",s),e.parameterOverrides)for(const t of Object.keys(e.parameterOverrides)){const o=e.parameterOverrides[t];w.set(t,o)}"function"==typeof e.hitFilter&&e.hitFilter.call(null,w),await fetch(new Request(c.origin+c.pathname,{body:w.toString(),method:"POST",mode:"cors",credentials:"omit",headers:{"Content-Type":"text/plain"}}))}catch(e){throw await t.unshiftRequest(o),e}}},i=e=>{const t=({url:e})=>"www.google-analytics.com"===e.hostname&&r.test(e.pathname),o=new w.NetworkOnly({plugins:[e]});return[new n.Route(t,o,"GET"),new n.Route(t,o,"POST")]},l=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.google-analytics.com"===e.hostname&&"/analytics.js"===e.pathname,t,"GET")},m=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtag/js"===e.pathname,t,"GET")},u=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtm.js"===e.pathname,t,"GET")};return e.initialize=((e={})=>{const n=o.cacheNames.getGoogleAnalyticsName(e.cacheName),c=new t.Plugin("workbox-google-analytics",{maxRetentionTime:2880,onSync:s(e)}),w=[u(n),l(n),m(n),...i(c)],r=new a.Router;for(const e of w)r.registerRoute(e);r.addFetchListener()}),e}({},workbox.backgroundSync,workbox.core._private,workbox.routing,workbox.routing,workbox.strategies,workbox.strategies);
-//# sourceMappingURL=workbox-offline-ga.prod.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-precaching.prod.js


+ 0 - 2
public/workbox/workbox-range-requests.prod.js

@@ -1,2 +0,0 @@
-this.workbox=this.workbox||{},this.workbox.rangeRequests=function(e,n){"use strict";try{self["workbox:range-requests:4.3.1"]&&_()}catch(e){}async function t(e,t){try{if(206===t.status)return t;const s=e.headers.get("range");if(!s)throw new n.WorkboxError("no-range-header");const a=function(e){const t=e.trim().toLowerCase();if(!t.startsWith("bytes="))throw new n.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:t});if(t.includes(","))throw new n.WorkboxError("single-range-only",{normalizedRangeHeader:t});const s=/(\d*)-(\d*)/.exec(t);if(null===s||!s[1]&&!s[2])throw new n.WorkboxError("invalid-range-values",{normalizedRangeHeader:t});return{start:""===s[1]?null:Number(s[1]),end:""===s[2]?null:Number(s[2])}}(s),r=await t.blob(),i=function(e,t,s){const a=e.size;if(s>a||t<0)throw new n.WorkboxError("range-not-satisfiable",{size:a,end:s,start:t});let r,i;return null===t?(r=a-s,i=a):null===s?(r=t,i=a):(r=t,i=s+1),{start:r,end:i}}(r,a.start,a.end),o=r.slice(i.start,i.end),u=o.size,l=new Response(o,{status:206,statusText:"Partial Content",headers:t.headers});return l.headers.set("Content-Length",u),l.headers.set("Content-Range",`bytes ${i.start}-${i.end-1}/`+r.size),l}catch(e){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return e.createPartialResponse=t,e.Plugin=class{async cachedResponseWillBeUsed({request:e,cachedResponse:n}){return n&&e.headers.has("range")?await t(e,n):n}},e}({},workbox.core._private);
-//# sourceMappingURL=workbox-range-requests.prod.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-routing.prod.js


文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-strategies.prod.js


+ 0 - 2
public/workbox/workbox-streams.prod.js

@@ -1,2 +0,0 @@
-this.workbox=this.workbox||{},this.workbox.streams=function(e){"use strict";try{self["workbox:streams:4.3.1"]&&_()}catch(e){}function n(e){const n=e.map(e=>Promise.resolve(e).then(e=>(function(e){return e.body&&e.body.getReader?e.body.getReader():e.getReader?e.getReader():new Response(e).body.getReader()})(e)));let t,r;const s=new Promise((e,n)=>{t=e,r=n});let o=0;return{done:s,stream:new ReadableStream({pull(e){return n[o].then(e=>e.read()).then(r=>{if(r.done)return++o>=n.length?(e.close(),void t()):this.pull(e);e.enqueue(r.value)}).catch(e=>{throw r(e),e})},cancel(){t()}})}}function t(e={}){const n=new Headers(e);return n.has("content-type")||n.set("content-type","text/html"),n}function r(e,r){const{done:s,stream:o}=n(e),a=t(r);return{done:s,response:new Response(o,{headers:a})}}let s=void 0;function o(){if(void 0===s)try{new ReadableStream({start(){}}),s=!0}catch(e){s=!1}return s}return e.concatenate=n,e.concatenateToResponse=r,e.isSupported=o,e.strategy=function(e,n){return async({event:s,url:a,params:c})=>{if(o()){const{done:t,response:o}=r(e.map(e=>e({event:s,url:a,params:c})),n);return s.waitUntil(t),o}const i=await Promise.all(e.map(e=>e({event:s,url:a,params:c})).map(async e=>{const n=await e;return n instanceof Response?n.blob():n})),u=t(n);return new Response(new Blob(i),{headers:u})}},e}({});
-//# sourceMappingURL=workbox-streams.prod.js.map

+ 0 - 2
public/workbox/workbox-sw.js

@@ -1,2 +0,0 @@
-!function(){"use strict";try{self["workbox:sw:4.3.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.3.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}();
-//# sourceMappingURL=workbox-sw.js.map

文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-window.prod.es5.mjs


文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-window.prod.mjs


文件差异内容过多而无法显示
+ 0 - 0
public/workbox/workbox-window.prod.umd.js


+ 1 - 1
src/analytics.ts

@@ -11,7 +11,7 @@ export const trackEvent = (
     // Uncomment the next line to track locally
     // console.log("Track Event", { category, action, label, value });
 
-    if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
+    if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
       return;
     }
 

+ 2 - 3
src/charts.ts

@@ -6,7 +6,6 @@ import {
 import {
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_SIZE,
-  ENV,
   VERTICAL_ALIGN,
 } from "./constants";
 import { newElement, newLinearElement, newTextElement } from "./element";
@@ -384,7 +383,7 @@ const chartTypeBar = (
       y,
       groupId,
       backgroundColor,
-      process.env.NODE_ENV === ENV.DEVELOPMENT,
+      import.meta.env.DEV,
     ),
   ];
 };
@@ -473,7 +472,7 @@ const chartTypeLine = (
       y,
       groupId,
       backgroundColor,
-      process.env.NODE_ENV === ENV.DEVELOPMENT,
+      import.meta.env.DEV,
     ),
     line,
     ...lines,

+ 2 - 1
src/components/App.test.tsx

@@ -4,8 +4,9 @@ import { reseed } from "../random";
 import { render, queryByTestId } from "../tests/test-utils";
 
 import ExcalidrawApp from "../excalidraw-app";
+import { vi } from "vitest";
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 
 describe("Test <App/>", () => {
   beforeEach(async () => {

+ 4 - 9
src/components/App.tsx

@@ -255,6 +255,7 @@ import {
   isTransparent,
   easeToValuesRAF,
   muteFSAbortError,
+  isTestEnv,
   easeOut,
 } from "../utils";
 import {
@@ -1595,10 +1596,7 @@ class App extends React.Component<AppProps, AppState> {
     this.excalidrawContainerValue.container =
       this.excalidrawContainerRef.current;
 
-    if (
-      process.env.NODE_ENV === ENV.TEST ||
-      process.env.NODE_ENV === ENV.DEVELOPMENT
-    ) {
+    if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
       const setState = this.setState.bind(this);
       Object.defineProperties(window.h, {
         state: {
@@ -1636,7 +1634,7 @@ class App extends React.Component<AppProps, AppState> {
       // bounding rects don't work in tests so updating
       // the state on init would result in making the test enviro run
       // in mobile breakpoint (0 width/height), making everything fail
-      process.env.NODE_ENV !== "test"
+      !isTestEnv()
     ) {
       this.refreshDeviceState(this.excalidrawContainerRef.current);
     }
@@ -8173,10 +8171,7 @@ declare global {
   }
 }
 
-if (
-  process.env.NODE_ENV === ENV.TEST ||
-  process.env.NODE_ENV === ENV.DEVELOPMENT
-) {
+if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
   window.h = window.h || ({} as Window["h"]);
 
   Object.defineProperties(window.h, {

+ 1 - 1
src/components/LibraryMenuBrowseButton.tsx

@@ -16,7 +16,7 @@ const LibraryMenuBrowseButton = ({
   return (
     <a
       className="library-menu-browse-button"
-      href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
+      href={`${import.meta.env.VITE_APP_LIBRARY_URL}?target=${
         window.name || "_blank"
       }&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
         VERSIONS.excalidrawLibrary

+ 1 - 1
src/components/PublishLibrary.tsx

@@ -319,7 +319,7 @@ const PublishLibrary = ({
     formData.append("twitterHandle", libraryData.twitterHandle);
     formData.append("website", libraryData.website);
 
-    fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
+    fetch(`${import.meta.env.VITE_APP_LIBRARY_BACKEND}/submit`, {
       method: "post",
       body: formData,
     })

+ 2 - 1
src/components/Sidebar/Sidebar.test.tsx

@@ -10,6 +10,7 @@ import {
   waitFor,
   withExcalidrawDimensions,
 } from "../../tests/test-utils";
+import { vi } from "vitest";
 
 export const assertSidebarDockButton = async <T extends boolean>(
   hasDockButton: T,
@@ -205,7 +206,7 @@ describe("Sidebar", () => {
     });
 
     it("<Sidebar.Header> should render close button", async () => {
-      const onStateChange = jest.fn();
+      const onStateChange = vi.fn();
       const CustomExcalidraw = () => {
         return (
           <Excalidraw

+ 1 - 1
src/components/Sidebar/Sidebar.tsx

@@ -53,7 +53,7 @@ export const SidebarInner = forwardRef(
     }: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
     ref: React.ForwardedRef<HTMLDivElement>,
   ) => {
-    if (process.env.NODE_ENV === "development" && onDock && docked == null) {
+    if (import.meta.env.DEV && onDock && docked == null) {
       console.warn(
         "Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
       );

+ 2 - 2
src/components/__snapshots__/App.test.tsx.snap

@@ -1,6 +1,6 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`Test <App/> should show error modal when using brave and measureText API is not working 1`] = `
+exports[`Test <App/> > should show error modal when using brave and measureText API is not working 1`] = `
 <div
   data-testid="brave-measure-text-error"
 >

+ 1 - 1
src/element/newElement.ts

@@ -443,7 +443,7 @@ const _deepCopyElement = (val: any, depth: number = 0) => {
   // we're not cloning non-array & non-plain-object objects because we
   // don't support them on excalidraw elements yet. If we do, we need to make
   // sure we start cloning them, so let's warn about it.
-  if (process.env.NODE_ENV === "development") {
+  if (import.meta.env.DEV) {
     if (
       objectType !== "[object Object]" &&
       objectType !== "[object Array]" &&

+ 17 - 1
src/element/sizeHelpers.test.ts

@@ -1,19 +1,32 @@
+import { vi } from "vitest";
 import { getPerfectElementSize } from "./sizeHelpers";
 import * as constants from "../constants";
 
 const EPSILON_DIGITS = 3;
-
+// Needed so that we can mock the value of constants which is done in
+// below tests. In Jest this wasn't needed as global override was possible
+// but vite doesn't allow that hence we need to mock
+vi.mock(
+  "../constants.ts",
+  //@ts-ignore
+  async (importOriginal) => {
+    const module: any = await importOriginal();
+    return { ...module };
+  },
+);
 describe("getPerfectElementSize", () => {
   it("should return height:0 if `elementType` is line and locked angle is 0", () => {
     const { height, width } = getPerfectElementSize("line", 149, 10);
     expect(width).toBeCloseTo(149, EPSILON_DIGITS);
     expect(height).toBeCloseTo(0, EPSILON_DIGITS);
   });
+
   it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
     const { height, width } = getPerfectElementSize("line", 10, 140);
     expect(width).toBeCloseTo(0, EPSILON_DIGITS);
     expect(height).toBeCloseTo(140, EPSILON_DIGITS);
   });
+
   it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
     const { height, width } = getPerfectElementSize("arrow", 200, 20);
     expect(width).toBeCloseTo(200, EPSILON_DIGITS);
@@ -24,16 +37,19 @@ describe("getPerfectElementSize", () => {
     expect(width).toBeCloseTo(0, EPSILON_DIGITS);
     expect(height).toBeCloseTo(100, EPSILON_DIGITS);
   });
+
   it("should return adjust height to be width * tan(locked angle)", () => {
     const { height, width } = getPerfectElementSize("arrow", 120, 185);
     expect(width).toBeCloseTo(120, EPSILON_DIGITS);
     expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
   });
+
   it("should return height equals to width if locked angle is 45 deg", () => {
     const { height, width } = getPerfectElementSize("arrow", 135, 145);
     expect(width).toBeCloseTo(135, EPSILON_DIGITS);
     expect(height).toBeCloseTo(135, EPSILON_DIGITS);
   });
+
   it("should return height:0 and width:0 when width and height are 0", () => {
     const { height, width } = getPerfectElementSize("arrow", 0, 0);
     expect(width).toBeCloseTo(0, EPSILON_DIGITS);

+ 12 - 12
src/element/textWysiwyg.test.tsx

@@ -955,7 +955,7 @@ describe("textWysiwyg", () => {
       // should center align horizontally and vertically by default
       resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-        Array [
+        [
           85,
           4.5,
         ]
@@ -979,7 +979,7 @@ describe("textWysiwyg", () => {
       // should left align horizontally and bottom vertically after resize
       resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-        Array [
+        [
           15,
           65,
         ]
@@ -1001,7 +1001,7 @@ describe("textWysiwyg", () => {
       // should right align horizontally and top vertically after resize
       resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
       expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-        Array [
+        [
           375,
           -539,
         ]
@@ -1279,7 +1279,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Left"));
         fireEvent.click(screen.getByTitle("Align top"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             15,
             25,
           ]
@@ -1290,7 +1290,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center"));
         fireEvent.click(screen.getByTitle("Align top"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             30,
             25,
           ]
@@ -1302,7 +1302,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Align top"));
 
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             45,
             25,
           ]
@@ -1313,7 +1313,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center vertically"));
         fireEvent.click(screen.getByTitle("Left"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             15,
             45,
           ]
@@ -1325,7 +1325,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center vertically"));
 
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             30,
             45,
           ]
@@ -1337,7 +1337,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center vertically"));
 
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             45,
             45,
           ]
@@ -1349,7 +1349,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Align bottom"));
 
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             15,
             65,
           ]
@@ -1360,7 +1360,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Center"));
         fireEvent.click(screen.getByTitle("Align bottom"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             30,
             65,
           ]
@@ -1371,7 +1371,7 @@ describe("textWysiwyg", () => {
         fireEvent.click(screen.getByTitle("Right"));
         fireEvent.click(screen.getByTitle("Align bottom"));
         expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
-          Array [
+          [
             45,
             65,
           ]

+ 2 - 8
src/excalidraw-app/collab/Collab.tsx

@@ -171,10 +171,7 @@ class Collab extends PureComponent<Props, CollabState> {
 
     appJotaiStore.set(collabAPIAtom, collabAPI);
 
-    if (
-      process.env.NODE_ENV === ENV.TEST ||
-      process.env.NODE_ENV === ENV.DEVELOPMENT
-    ) {
+    if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
       window.collab = window.collab || ({} as Window["collab"]);
       Object.defineProperties(window, {
         collab: {
@@ -860,10 +857,7 @@ declare global {
   }
 }
 
-if (
-  process.env.NODE_ENV === ENV.TEST ||
-  process.env.NODE_ENV === ENV.DEVELOPMENT
-) {
+if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
   window.collab = window.collab || ({} as Window["collab"]);
 }
 

+ 3 - 1
src/excalidraw-app/components/AppWelcomeScreen.tsx

@@ -19,7 +19,9 @@ export const AppWelcomeScreen: React.FC<{
           return (
             <a
               style={{ pointerEvents: "all" }}
-              href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
+              href={`${
+                import.meta.env.VITE_APP_PLUS_APP
+              }?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
               key={idx}
             >
               Excalidraw+

+ 3 - 1
src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx

@@ -6,7 +6,9 @@ export const ExcalidrawPlusAppLink = () => {
   }
   return (
     <a
-      href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
+      href={`${
+        import.meta.env.VITE_APP_PLUS_APP
+      }?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
       target="_blank"
       rel="noreferrer"
       className="plus-button"

+ 4 - 2
src/excalidraw-app/data/firebase.ts

@@ -21,10 +21,12 @@ import { ResolutionType } from "../../utility-types";
 
 let FIREBASE_CONFIG: Record<string, any>;
 try {
-  FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
+  FIREBASE_CONFIG = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
 } catch (error: any) {
   console.warn(
-    `Error JSON parsing firebase config. Supplied value: ${process.env.REACT_APP_FIREBASE_CONFIG}`,
+    `Error JSON parsing firebase config. Supplied value: ${
+      import.meta.env.VITE_APP_FIREBASE_CONFIG
+    }`,
   );
   FIREBASE_CONFIG = {};
 }

+ 5 - 5
src/excalidraw-app/data/index.ts

@@ -47,8 +47,8 @@ export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
     isSyncableElement(element),
   ) as SyncableExcalidrawElement[];
 
-const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
-const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
+const BACKEND_V2_GET = import.meta.env.VITE_APP_BACKEND_V2_GET_URL;
+const BACKEND_V2_POST = import.meta.env.VITE_APP_BACKEND_V2_POST_URL;
 
 const generateRoomId = async () => {
   const buffer = new Uint8Array(ROOM_ID_BYTES);
@@ -67,16 +67,16 @@ export const getCollabServer = async (): Promise<{
   url: string;
   polling: boolean;
 }> => {
-  if (process.env.REACT_APP_WS_SERVER_URL) {
+  if (import.meta.env.VITE_APP_WS_SERVER_URL) {
     return {
-      url: process.env.REACT_APP_WS_SERVER_URL,
+      url: import.meta.env.VITE_APP_WS_SERVER_URL,
       polling: true,
     };
   }
 
   try {
     const resp = await fetch(
-      `${process.env.REACT_APP_PORTAL_URL}/collab-server`,
+      `${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
     );
     return await resp.json();
   } catch (error) {

+ 0 - 31
src/excalidraw-app/pwa.ts

@@ -1,31 +0,0 @@
-import { register as registerServiceWorker } from "../serviceWorkerRegistration";
-import { EVENT } from "../constants";
-
-// On Apple mobile devices add the proprietary app icon and splashscreen markup.
-// No one should have to do this manually, and eventually this annoyance will
-// go away once https://bugs.webkit.org/show_bug.cgi?id=183937 is fixed.
-if (
-  /\b(iPad|iPhone|iPod|Safari)\b/.test(navigator.userAgent) &&
-  !matchMedia("(display-mode: standalone)").matches
-) {
-  import(/* webpackChunkName: "pwacompat" */ "pwacompat");
-}
-
-registerServiceWorker({
-  onUpdate: (registration) => {
-    const waitingServiceWorker = registration.waiting;
-    if (waitingServiceWorker) {
-      waitingServiceWorker.addEventListener(
-        EVENT.STATE_CHANGE,
-        (event: Event) => {
-          const target = event.target as ServiceWorker;
-          const state = target.state as ServiceWorkerState;
-          if (state === "activated") {
-            window.location.reload();
-          }
-        },
-      );
-      waitingServiceWorker.postMessage({ type: "SKIP_WAITING" });
-    }
-  },
-});

+ 2 - 2
src/excalidraw-app/sentry.ts

@@ -7,7 +7,7 @@ const SentryEnvHostnameMap: { [key: string]: string } = {
 };
 
 const REACT_APP_DISABLE_SENTRY =
-  process.env.REACT_APP_DISABLE_SENTRY === "true";
+  import.meta.env.VITE_APP_DISABLE_SENTRY === "true";
 
 // Disable Sentry locally or inside the Docker to avoid noise/respect privacy
 const onlineEnv =
@@ -21,7 +21,7 @@ Sentry.init({
     ? "https://[email protected]/5179260"
     : undefined,
   environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
-  release: process.env.REACT_APP_GIT_SHA,
+  release: import.meta.env.VITE_APP_GIT_SHA,
   ignoreErrors: [
     "undefined is not an object (evaluating 'window.__pad.performLoop')", // Only happens on Safari, but spams our servers. Doesn't break anything
   ],

+ 0 - 10
src/global.d.ts

@@ -38,16 +38,6 @@ interface CanvasRenderingContext2D {
   ) => void;
 }
 
-// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
-declare namespace NodeJS {
-  interface ProcessEnv {
-    readonly REACT_APP_BACKEND_V2_GET_URL: string;
-    readonly REACT_APP_BACKEND_V2_POST_URL: string;
-    readonly REACT_APP_PORTAL_URL: string;
-    readonly REACT_APP_FIREBASE_CONFIG: string;
-  }
-}
-
 interface Clipboard extends EventTarget {
   write(data: any[]): Promise<void>;
 }

+ 2 - 3
src/i18n.ts

@@ -1,6 +1,5 @@
 import fallbackLangData from "./locales/en.json";
 import percentages from "./locales/percentages.json";
-import { ENV } from "./constants";
 import { jotaiScope, jotaiStore } from "./jotai";
 import { atom, useAtomValue } from "jotai";
 import { NestedKeyOf } from "./utility-types";
@@ -74,7 +73,7 @@ export const languages: Language[] = [
 ];
 
 const TEST_LANG_CODE = "__test__";
-if (process.env.NODE_ENV === ENV.DEVELOPMENT) {
+if (import.meta.env.DEV) {
   languages.unshift(
     { code: TEST_LANG_CODE, label: "test language" },
     {
@@ -145,7 +144,7 @@ export const t = (
   if (translation === undefined) {
     const errorMessage = `Can't find translation for ${path}`;
     // in production, don't blow up the app on a missing translation key
-    if (process.env.NODE_ENV === "production") {
+    if (import.meta.env.PROD) {
       console.warn(errorMessage);
       return "";
     }

+ 1 - 2
src/index.tsx

@@ -2,9 +2,8 @@ import { StrictMode } from "react";
 import { createRoot } from "react-dom/client";
 import ExcalidrawApp from "./excalidraw-app";
 
-import "./excalidraw-app/pwa";
 import "./excalidraw-app/sentry";
-window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
+window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
 const rootElement = document.getElementById("root")!;
 const root = createRoot(rootElement);
 root.render(

+ 3 - 3
src/packages/excalidraw/env.js

@@ -9,9 +9,9 @@ const parseEnvVariables = (filepath) => {
     },
     {},
   );
-  envVars.PKG_NAME = JSON.stringify(pkg.name);
-  envVars.PKG_VERSION = JSON.stringify(pkg.version);
-  envVars.IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
+  envVars.VITE_PKG_NAME = JSON.stringify(pkg.name);
+  envVars.VITE_PKG_VERSION = JSON.stringify(pkg.version);
+  envVars.VITE_IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
   return envVars;
 };
 

+ 1 - 0
src/packages/excalidraw/package.json

@@ -59,6 +59,7 @@
     "cross-env": "7.0.3",
     "css-loader": "6.7.1",
     "dotenv": "16.0.1",
+    "import-meta-loader": "1.1.0",
     "mini-css-extract-plugin": "2.6.1",
     "postcss-loader": "7.0.1",
     "sass-loader": "13.0.2",

+ 1 - 1
src/packages/excalidraw/publicPath.js

@@ -4,5 +4,5 @@ if (process.env.NODE_ENV !== ENV.TEST) {
   /* global __webpack_public_path__:writable */
   __webpack_public_path__ =
     window.EXCALIDRAW_ASSET_PATH ||
-    `https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}/dist/`;
+    `https://unpkg.com/${process.env.VITE_PKG_NAME}@${process.env.VITE_PKG_VERSION}/dist/`;
 }

+ 3 - 0
src/packages/excalidraw/webpack.dev.config.js

@@ -47,6 +47,9 @@ module.exports = {
         exclude:
           /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
         use: [
+          {
+            loader: "import-meta-loader",
+          },
           {
             loader: "ts-loader",
             options: {

+ 3 - 0
src/packages/excalidraw/webpack.prod.config.js

@@ -50,6 +50,9 @@ module.exports = {
           /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
 
         use: [
+          {
+            loader: "import-meta-loader",
+          },
           {
             loader: "ts-loader",
             options: {

+ 5 - 0
src/packages/excalidraw/yarn.lock

@@ -2916,6 +2916,11 @@ import-local@^3.0.2:
     pkg-dir "^4.2.0"
     resolve-cwd "^3.0.0"
 
[email protected]:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/import-meta-loader/-/import-meta-loader-1.1.0.tgz#927060305f2d0f88b495f2754aa33387ca6579d7"
+  integrity sha512-f96r2o8xT+b2KVlOY4x+1KTJmJiapZlf77j1WebR8NQgMG1dpdqijjGl4i/2jMoXch2CVqcQoTMfh5BR7bR8wA==
+
 indexes-of@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"

+ 1 - 1
src/renderer/renderElement.ts

@@ -915,7 +915,7 @@ const drawElementFromCanvas = (
     );
 
     if (
-      process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
+      import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
         "true" &&
       hasBoundTextElement(element)
     ) {

+ 4 - 3
src/scene/export.ts

@@ -133,12 +133,13 @@ export const exportToSvg = async (
   }
 
   let assetPath = "https://excalidraw.com/";
-
   // Asset path needs to be determined only when using package
-  if (process.env.IS_EXCALIDRAW_NPM_PACKAGE) {
+  if (import.meta.env.VITE_IS_EXCALIDRAW_NPM_PACKAGE) {
     assetPath =
       window.EXCALIDRAW_ASSET_PATH ||
-      `https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}`;
+      `https://unpkg.com/${import.meta.env.VITE_PKG_NAME}@${
+        import.meta.env.PKG_VERSION
+      }`;
 
     if (assetPath?.startsWith("/")) {
       assetPath = assetPath.replace("/", `${window.location.origin}/`);

+ 0 - 147
src/service-worker.ts

@@ -1,147 +0,0 @@
-/// <reference lib="webworker" />
-/* eslint-disable no-restricted-globals */
-
-// This service worker can be customized!
-// See https://developers.google.com/web/tools/workbox/modules
-// for the list of available Workbox modules, or add any other
-// code you'd like.
-// You can also remove this file if you'd prefer not to use a
-// service worker, and the Workbox build step will be skipped.
-
-import { clientsClaim } from "workbox-core";
-import { ExpirationPlugin } from "workbox-expiration";
-import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
-import { registerRoute } from "workbox-routing";
-import { CacheFirst, StaleWhileRevalidate } from "workbox-strategies";
-
-declare const self: ServiceWorkerGlobalScope;
-
-clientsClaim();
-
-// Precache assets generated by your build process.
-//
-// Their URLs are injected into the __WB_MANIFEST during build (by workbox).
-//
-// This variable must be present somewhere in your service worker file,
-// even if you decide not to use precaching. See https://cra.link/PWA.
-//
-// We don't want to precache i18n files so we filter them out
-// (normally this should be configured in a webpack workbox plugin, but we don't
-// have access to it in CRA) — this is because all users will use at most
-// one or two languages, so there's no point fetching all of them. (They'll
-// be cached as you load them.)
-const manifest = self.__WB_MANIFEST.filter((entry) => {
-  return !/locales\/[\w-]+json/.test(
-    typeof entry === "string" ? entry : entry.url,
-  );
-});
-
-precacheAndRoute(manifest);
-
-// Set up App Shell-style routing, so that all navigation requests
-// are fulfilled with your index.html shell. Learn more at
-// https://developer.chrome.com/docs/workbox/app-shell-model/
-//
-// below is copied verbatim from CRA@5
-const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
-registerRoute(
-  // Return false to exempt requests from being fulfilled by index.html.
-  ({ request, url }: { request: Request; url: URL }) => {
-    // If this isn't a navigation, skip.
-    if (request.mode !== "navigate") {
-      return false;
-    }
-
-    // If this is a URL that starts with /_, skip.
-    if (url.pathname.startsWith("/_")) {
-      return false;
-    }
-
-    // If this looks like a URL for a resource, because it contains
-    // a file extension, skip.
-    if (url.pathname.match(fileExtensionRegexp)) {
-      return false;
-    }
-
-    // Return true to signal that we want to use the handler.
-    return true;
-  },
-  createHandlerBoundToURL(`${process.env.PUBLIC_URL}/index.html`),
-);
-
-// Cache resources that aren't being precached
-// -----------------------------------------------------------------------------
-
-registerRoute(
-  new RegExp("/fonts.css"),
-  new StaleWhileRevalidate({
-    cacheName: "fonts",
-    plugins: [
-      // Ensure that once this runtime cache reaches a maximum size the
-      // least-recently used images are removed.
-      new ExpirationPlugin({ maxEntries: 50 }),
-    ],
-  }),
-);
-
-// since we serve fonts from, don't forget to append new ?v= param when
-// updating fonts (glyphs) without changing the filename
-registerRoute(
-  new RegExp("/.+.(ttf|woff2|otf)"),
-  new CacheFirst({
-    cacheName: "fonts",
-    plugins: [
-      // Ensure that once this runtime cache reaches a maximum size the
-      // least-recently used images are removed.
-      new ExpirationPlugin({
-        maxEntries: 50,
-        // 90 days
-        maxAgeSeconds: 7776000000,
-      }),
-    ],
-  }),
-);
-
-registerRoute(
-  new RegExp("/locales\\/[\\w-]+json"),
-  // Customize this strategy as needed, e.g., by changing to CacheFirst.
-  new CacheFirst({
-    cacheName: "locales",
-    plugins: [
-      // Ensure that once this runtime cache reaches a maximum size the
-      // least-recently used images are removed.
-      new ExpirationPlugin({
-        maxEntries: 50,
-        // 30 days
-        maxAgeSeconds: 2592000000,
-      }),
-    ],
-  }),
-);
-
-// -----------------------------------------------------------------------------
-
-self.addEventListener("fetch", (event) => {
-  if (
-    event.request.method === "POST" &&
-    event.request.url.endsWith("/web-share-target")
-  ) {
-    return event.respondWith(
-      (async () => {
-        const formData = await event.request.formData();
-        const file = formData.get("file");
-        const webShareTargetCache = await caches.open("web-share-target");
-        await webShareTargetCache.put("shared-file", new Response(file));
-        return Response.redirect("/?web-share-target", 303);
-      })(),
-    );
-  }
-});
-
-// This allows the web app to trigger skipWaiting via
-// registration.waiting.postMessage({type: 'SKIP_WAITING'})
-self.addEventListener("message", (event) => {
-  if (event.data && event.data.type === "SKIP_WAITING") {
-    self.skipWaiting();
-  }
-});

+ 0 - 162
src/serviceWorkerRegistration.ts

@@ -1,162 +0,0 @@
-// This optional code is used to register a service worker.
-// register() is not called by default.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on subsequent visits to a page, after all the
-// existing tabs open on the page have been closed, since previously cached
-// resources are updated in the background.
-
-// To learn more about the benefits of this model and instructions on how to
-// opt-in, read https://bit.ly/CRA-PWA
-
-const isLocalhost = Boolean(
-  window.location.hostname === "localhost" ||
-    // [::1] is the IPv6 localhost address.
-    window.location.hostname === "[::1]" ||
-    // 127.0.0.0/8 are considered localhost for IPv4.
-    window.location.hostname.match(
-      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
-    ),
-);
-
-type Config = {
-  onSuccess?: (registration: ServiceWorkerRegistration) => void;
-  onUpdate?: (registration: ServiceWorkerRegistration) => void;
-};
-
-export const register = (config?: Config) => {
-  if (
-    (process.env.NODE_ENV === "production" ||
-      process.env.REACT_APP_DEV_ENABLE_SW?.toLowerCase() === "true") &&
-    "serviceWorker" in navigator
-  ) {
-    // The URL constructor is available in all browsers that support SW.
-    const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
-    if (publicUrl.origin !== window.location.origin) {
-      // Our service worker won't work if PUBLIC_URL is on a different origin
-      // from what our page is served on. This might happen if a CDN is used to
-      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
-      return;
-    }
-
-    window.addEventListener("load", () => {
-      const isWebexLP = window.location.pathname.startsWith("/webex");
-      if (isWebexLP) {
-        unregister(() => {
-          window.location.reload();
-        });
-        return false;
-      }
-      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
-      if (isLocalhost) {
-        // This is running on localhost. Let's check if a service worker still exists or not.
-        checkValidServiceWorker(swUrl, config);
-
-        // Add some additional logging to localhost, pointing developers to the
-        // service worker/PWA documentation.
-        navigator.serviceWorker.ready.then(() => {
-          console.info(
-            "This web app is being served cache-first by a service " +
-              "worker. To learn more, visit https://bit.ly/CRA-PWA",
-          );
-        });
-      } else {
-        // Is not localhost. Just register service worker
-        registerValidSW(swUrl, config);
-      }
-    });
-  }
-};
-
-const registerValidSW = (swUrl: string, config?: Config) => {
-  navigator.serviceWorker
-    .register(swUrl)
-    .then((registration) => {
-      registration.onupdatefound = () => {
-        const installingWorker = registration.installing;
-        if (installingWorker == null) {
-          return;
-        }
-        installingWorker.onstatechange = () => {
-          if (installingWorker.state === "installed") {
-            if (navigator.serviceWorker.controller) {
-              // At this point, the updated precached content has been fetched,
-              // but the previous service worker will still serve the older
-              // content until all client tabs are closed.
-
-              console.info(
-                "New content is available and will be used when all tabs for this page are closed.",
-              );
-
-              // Execute callback
-              if (config && config.onUpdate) {
-                config.onUpdate(registration);
-              }
-            } else {
-              // At this point, everything has been precached.
-              // It's the perfect time to display a
-              // "Content is cached for offline use." message.
-
-              console.info("Content is cached for offline use.");
-
-              // Execute callback
-              if (config && config.onSuccess) {
-                config.onSuccess(registration);
-              }
-            }
-          }
-        };
-      };
-    })
-    .catch((error) => {
-      console.error("Error during service worker registration:", error);
-    });
-};
-
-const checkValidServiceWorker = (swUrl: string, config?: Config) => {
-  // Check if the service worker can be found. If it can't reload the page.
-  fetch(swUrl, {
-    headers: { "Service-Worker": "script" },
-  })
-    .then((response) => {
-      // Ensure service worker exists, and that we really are getting a JS file.
-      const contentType = response.headers.get("content-type");
-      if (
-        response.status === 404 ||
-        (contentType != null && contentType.indexOf("javascript") === -1)
-      ) {
-        // No service worker found. Probably a different app. Reload the page.
-        navigator.serviceWorker.ready.then((registration) => {
-          registration.unregister().then(() => {
-            window.location.reload();
-          });
-        });
-      } else {
-        // Service worker found. Proceed as normal.
-        registerValidSW(swUrl, config);
-      }
-    })
-    .catch((error) => {
-      console.info(
-        "No internet connection found. App is running in offline mode.",
-        error.message,
-      );
-    });
-};
-
-export const unregister = (callback?: () => void) => {
-  if ("serviceWorker" in navigator) {
-    navigator.serviceWorker.ready
-      .then((registration) => {
-        return registration.unregister();
-      })
-      .then(() => {
-        callback?.();
-      })
-      .catch((error) => {
-        console.error(error.message);
-      });
-  }
-};

+ 5 - 8
src/setupTests.ts

@@ -1,19 +1,16 @@
+// vitest.setup.ts
+import "vitest-canvas-mock";
 import "@testing-library/jest-dom";
-import "jest-canvas-mock";
-import dotenv from "dotenv";
+import { vi } from "vitest";
 import polyfill from "./polyfill";
 
 require("fake-indexeddb/auto");
 
 polyfill();
-// jest doesn't know of .env.development so we need to init it ourselves
-dotenv.config({
-  path: require("path").resolve(__dirname, "../.env.development"),
-});
 
-jest.mock("nanoid", () => {
+vi.mock("nanoid", () => {
   return {
-    nanoid: jest.fn(() => "test-id"),
+    nanoid: vi.fn(() => "test-id"),
   };
 });
 // ReactDOM is located inside index.tsx file

+ 5 - 6
src/tests/MobileMenu.test.tsx

@@ -11,23 +11,23 @@ describe("Test MobileMenu", () => {
   const { h } = window;
   const dimensions = { height: 400, width: 800 };
 
+  beforeAll(() => {
+    mockBoundingClientRect(dimensions);
+  });
+
   beforeEach(async () => {
     await render(<ExcalidrawApp />);
     //@ts-ignore
     h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
   });
 
-  beforeAll(() => {
-    mockBoundingClientRect(dimensions);
-  });
-
   afterAll(() => {
     restoreOriginalGetBoundingClientRect();
   });
 
   it("should set device correctly", () => {
     expect(h.app.device).toMatchInlineSnapshot(`
-      Object {
+      {
         "canDeviceFitSidebar": false,
         "isLandscape": true,
         "isMobile": true,
@@ -39,7 +39,6 @@ describe("Test MobileMenu", () => {
 
   it("should initialize with welcome screen and hide once user interacts", async () => {
     expect(document.querySelector(".welcome-screen-center")).toMatchSnapshot();
-
     UI.clickTool("rectangle");
     expect(document.querySelector(".welcome-screen-center")).toBeNull();
   });

+ 2 - 2
src/tests/__snapshots__/MobileMenu.test.tsx.snap

@@ -1,6 +1,6 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`Test MobileMenu should initialize with welcome screen and hide once user interacts 1`] = `
+exports[`Test MobileMenu > should initialize with welcome screen and hide once user interacts 1`] = `
 <div
   class="welcome-screen-center"
 >

+ 6 - 6
src/tests/__snapshots__/charts.test.tsx.snap

@@ -1,15 +1,15 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`tryParseSpreadsheet works for numbers with comma in them 1`] = `
-Object {
-  "spreadsheet": Object {
-    "labels": Array [
+exports[`tryParseSpreadsheet > works for numbers with comma in them 1`] = `
+{
+  "spreadsheet": {
+    "labels": [
       "Week 1",
       "Week 2",
       "Week 3",
     ],
     "title": "Users",
-    "values": Array [
+    "values": [
       814,
       10301,
       4264,

文件差异内容过多而无法显示
+ 183 - 183
src/tests/__snapshots__/contextmenu.test.tsx.snap


+ 31 - 31
src/tests/__snapshots__/dragCreate.test.tsx.snap

@@ -1,9 +1,9 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough arrow 1`] = `1`;
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 1`] = `1`;
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough arrow 2`] = `
-Object {
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 2`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -11,7 +11,7 @@ Object {
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -19,18 +19,18 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       30,
       50,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -49,16 +49,16 @@ Object {
 }
 `;
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough diamond 1`] = `1`;
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 1`] = `1`;
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough diamond 2`] = `
-Object {
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 2`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -66,7 +66,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -83,16 +83,16 @@ Object {
 }
 `;
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough ellipse 1`] = `1`;
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 1`] = `1`;
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough ellipse 2`] = `
-Object {
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 2`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -100,7 +100,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -117,8 +117,8 @@ Object {
 }
 `;
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough line 1`] = `
-Object {
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > line 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -126,7 +126,7 @@ Object {
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -134,18 +134,18 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       30,
       50,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -164,16 +164,16 @@ Object {
 }
 `;
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough rectangle 1`] = `1`;
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 1`] = `1`;
 
-exports[`Test dragCreate add element to the scene when pointer dragging long enough rectangle 2`] = `
-Object {
+exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 2`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -181,7 +181,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": 337897,

文件差异内容过多而无法显示
+ 2 - 2
src/tests/__snapshots__/export.test.tsx.snap


+ 3 - 3
src/tests/__snapshots__/linearElementEditor.test.tsx.snap

@@ -1,11 +1,11 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`Test Linear Elements Test bound text element should match styles for text editor 1`] = `
+exports[`Test Linear Elements > Test bound text element > should match styles for text editor 1`] = `
 <textarea
   class="excalidraw-wysiwyg"
   data-type="wysiwyg"
   dir="auto"
-  style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
+  style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
   tabindex="0"
   wrap="off"
 />

+ 34 - 34
src/tests/__snapshots__/move.test.tsx.snap

@@ -1,13 +1,13 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`duplicate element on move when ALT is clicked rectangle 1`] = `
-Object {
+exports[`duplicate element on move when ALT is clicked > rectangle 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0_copy",
   "isDeleted": false,
@@ -15,7 +15,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": 401146281,
@@ -32,14 +32,14 @@ Object {
 }
 `;
 
-exports[`duplicate element on move when ALT is clicked rectangle 2`] = `
-Object {
+exports[`duplicate element on move when ALT is clicked > rectangle 2`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -47,7 +47,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": 337897,
@@ -64,14 +64,14 @@ Object {
 }
 `;
 
-exports[`move element rectangle 1`] = `
-Object {
+exports[`move element > rectangle 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -79,7 +79,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": 337897,
@@ -96,19 +96,19 @@ Object {
 }
 `;
 
-exports[`move element rectangles with binding arrow 1`] = `
-Object {
+exports[`move element > rectangles with binding arrow 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": Array [
-    Object {
+  "boundElements": [
+    {
       "id": "id2",
       "type": "arrow",
     },
   ],
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 100,
   "id": "id0",
   "isDeleted": false,
@@ -116,7 +116,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": 337897,
@@ -133,19 +133,19 @@ Object {
 }
 `;
 
-exports[`move element rectangles with binding arrow 2`] = `
-Object {
+exports[`move element > rectangles with binding arrow 2`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": Array [
-    Object {
+  "boundElements": [
+    {
       "id": "id2",
       "type": "arrow",
     },
   ],
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 300,
   "id": "id1",
   "isDeleted": false,
@@ -153,7 +153,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": 449462985,
@@ -170,20 +170,20 @@ Object {
 }
 `;
 
-exports[`move element rectangles with binding arrow 3`] = `
-Object {
+exports[`move element > rectangles with binding arrow 3`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "endArrowhead": null,
-  "endBinding": Object {
+  "endBinding": {
     "elementId": "id1",
     "focus": -0.46666666666666673,
     "gap": 10,
   },
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 81.48231043525051,
   "id": "id2",
   "isDeleted": false,
@@ -191,23 +191,23 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       81,
       81.48231043525051,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 401146281,
   "startArrowhead": null,
-  "startBinding": Object {
+  "startBinding": {
     "elementId": "id0",
     "focus": -0.6000000000000001,
     "gap": 10,

+ 19 - 19
src/tests/__snapshots__/multiPointCreate.test.tsx.snap

@@ -1,7 +1,7 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`multi point mode in linear elements arrow 1`] = `
-Object {
+exports[`multi point mode in linear elements > arrow 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -9,33 +9,33 @@ Object {
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 110,
   "id": "id0",
   "isDeleted": false,
-  "lastCommittedPoint": Array [
+  "lastCommittedPoint": [
     70,
     110,
   ],
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       20,
       30,
     ],
-    Array [
+    [
       70,
       110,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -54,8 +54,8 @@ Object {
 }
 `;
 
-exports[`multi point mode in linear elements line 1`] = `
-Object {
+exports[`multi point mode in linear elements > line 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -63,33 +63,33 @@ Object {
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 110,
   "id": "id0",
   "isDeleted": false,
-  "lastCommittedPoint": Array [
+  "lastCommittedPoint": [
     70,
     110,
   ],
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       20,
       30,
     ],
-    Array [
+    [
       70,
       110,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,

文件差异内容过多而无法显示
+ 182 - 182
src/tests/__snapshots__/regressionTests.test.tsx.snap


+ 27 - 27
src/tests/__snapshots__/selection.test.tsx.snap

@@ -1,7 +1,7 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`select single element on the scene arrow 1`] = `
-Object {
+exports[`select single element on the scene > arrow 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -9,7 +9,7 @@ Object {
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -17,18 +17,18 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       30,
       50,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -47,8 +47,8 @@ Object {
 }
 `;
 
-exports[`select single element on the scene arrow escape 1`] = `
-Object {
+exports[`select single element on the scene > arrow escape 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
@@ -56,7 +56,7 @@ Object {
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -64,18 +64,18 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       30,
       50,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -94,14 +94,14 @@ Object {
 }
 `;
 
-exports[`select single element on the scene diamond 1`] = `
-Object {
+exports[`select single element on the scene > diamond 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -109,7 +109,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -126,14 +126,14 @@ Object {
 }
 `;
 
-exports[`select single element on the scene ellipse 1`] = `
-Object {
+exports[`select single element on the scene > ellipse 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -141,7 +141,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": 337897,
@@ -158,14 +158,14 @@ Object {
 }
 `;
 
-exports[`select single element on the scene rectangle 1`] = `
-Object {
+exports[`select single element on the scene > rectangle 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "boundElements": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 50,
   "id": "id0",
   "isDeleted": false,
@@ -173,7 +173,7 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": 337897,

+ 1 - 0
src/tests/appState.test.tsx

@@ -1,4 +1,5 @@
 import { queryByTestId, render, waitFor } from "./test-utils";
+
 import ExcalidrawApp from "../excalidraw-app";
 import { API } from "./helpers/api";
 import { getDefaultAppState } from "../appState";

+ 5 - 4
src/tests/clipboard.test.tsx

@@ -1,3 +1,4 @@
+import { vi } from "vitest";
 import ReactDOM from "react-dom";
 import {
   render,
@@ -21,14 +22,14 @@ const { h } = window;
 
 const mouse = new Pointer("mouse");
 
-jest.mock("../keys.ts", () => {
-  const actual = jest.requireActual("../keys.ts");
+vi.mock("../keys.ts", async (importOriginal) => {
+  const module: any = await importOriginal();
   return {
     __esmodule: true,
-    ...actual,
+    ...module,
     isDarwin: false,
     KEYS: {
-      ...actual.KEYS,
+      ...module.KEYS,
       CTRL_OR_CMD: "ctrlKey",
     },
   };

+ 23 - 17
src/tests/collab.test.tsx

@@ -1,3 +1,4 @@
+import { vi } from "vitest";
 import { render, updateSceneData, waitFor } from "./test-utils";
 import ExcalidrawApp from "../excalidraw-app";
 import { API } from "./helpers/api";
@@ -15,15 +16,18 @@ Object.defineProperty(window, "crypto", {
   },
 });
 
-jest.mock("../excalidraw-app/data/index.ts", () => ({
-  __esmodule: true,
-  ...jest.requireActual("../excalidraw-app/data/index.ts"),
-  getCollabServer: jest.fn(() => ({
-    url: /* doesn't really matter */ "http://localhost:3002",
-  })),
-}));
+vi.mock("../excalidraw-app/data/index.ts", async (importActual) => {
+  const module = (await importActual()) as any;
+  return {
+    __esmodule: true,
+    ...module,
+    getCollabServer: vi.fn(() => ({
+      url: /* doesn't really matter */ "http://localhost:3002",
+    })),
+  };
+});
 
-jest.mock("../excalidraw-app/data/firebase.ts", () => {
+vi.mock("../excalidraw-app/data/firebase.ts", () => {
   const loadFromFirebase = async () => null;
   const saveToFirebase = () => {};
   const isSavedToFirebase = () => true;
@@ -45,15 +49,17 @@ jest.mock("../excalidraw-app/data/firebase.ts", () => {
   };
 });
 
-jest.mock("socket.io-client", () => {
-  return () => {
-    return {
-      close: () => {},
-      on: () => {},
-      once: () => {},
-      off: () => {},
-      emit: () => {},
-    };
+vi.mock("socket.io-client", () => {
+  return {
+    default: () => {
+      return {
+        close: () => {},
+        on: () => {},
+        once: () => {},
+        off: () => {},
+        emit: () => {},
+      };
+    },
   };
 });
 

+ 2 - 1
src/tests/contextmenu.test.tsx

@@ -21,6 +21,7 @@ import { copiedStyles } from "../actions/actionStyles";
 import { API } from "./helpers/api";
 import { setDateTimeForTests } from "../utils";
 import { LibraryItem } from "../types";
+import { vi } from "vitest";
 
 const checkpoint = (name: string) => {
   expect(renderScene.mock.calls.length).toMatchSnapshot(
@@ -39,7 +40,7 @@ const mouse = new Pointer("mouse");
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 57 - 57
src/tests/data/__snapshots__/restore.test.ts.snap

@@ -1,15 +1,15 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`restoreElements should restore arrow element correctly 1`] = `
-Object {
+exports[`restoreElements > should restore arrow element correctly 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": Array [],
+  "boundElements": [],
   "endArrowhead": null,
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 100,
   "id": "id-arrow01",
   "isDeleted": false,
@@ -17,18 +17,18 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       100,
       100,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": Any<Number>,
@@ -47,14 +47,14 @@ Object {
 }
 `;
 
-exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 1`] = `
-Object {
+exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 1`] = `
+{
   "angle": 0,
   "backgroundColor": "blue",
-  "boundElements": Array [],
+  "boundElements": [],
   "fillStyle": "cross-hatch",
   "frameId": null,
-  "groupIds": Array [
+  "groupIds": [
     "1",
     "2",
     "3",
@@ -66,7 +66,7 @@ Object {
   "locked": false,
   "opacity": 10,
   "roughness": 2,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -83,14 +83,14 @@ Object {
 }
 `;
 
-exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 2`] = `
-Object {
+exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 2`] = `
+{
   "angle": 0,
   "backgroundColor": "blue",
-  "boundElements": Array [],
+  "boundElements": [],
   "fillStyle": "cross-hatch",
   "frameId": null,
-  "groupIds": Array [
+  "groupIds": [
     "1",
     "2",
     "3",
@@ -102,7 +102,7 @@ Object {
   "locked": false,
   "opacity": 10,
   "roughness": 2,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -119,14 +119,14 @@ Object {
 }
 `;
 
-exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 3`] = `
-Object {
+exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 3`] = `
+{
   "angle": 0,
   "backgroundColor": "blue",
-  "boundElements": Array [],
+  "boundElements": [],
   "fillStyle": "cross-hatch",
   "frameId": null,
-  "groupIds": Array [
+  "groupIds": [
     "1",
     "2",
     "3",
@@ -138,7 +138,7 @@ Object {
   "locked": false,
   "opacity": 10,
   "roughness": 2,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -155,14 +155,14 @@ Object {
 }
 `;
 
-exports[`restoreElements should restore freedraw element correctly 1`] = `
-Object {
+exports[`restoreElements > should restore freedraw element correctly 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": Array [],
+  "boundElements": [],
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 0,
   "id": "id-freedraw01",
   "isDeleted": false,
@@ -170,10 +170,10 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [],
-  "pressures": Array [],
+  "points": [],
+  "pressures": [],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -191,16 +191,16 @@ Object {
 }
 `;
 
-exports[`restoreElements should restore line and draw elements correctly 1`] = `
-Object {
+exports[`restoreElements > should restore line and draw elements correctly 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": Array [],
+  "boundElements": [],
   "endArrowhead": null,
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 100,
   "id": "id-line01",
   "isDeleted": false,
@@ -208,18 +208,18 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       100,
       100,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": Any<Number>,
@@ -238,16 +238,16 @@ Object {
 }
 `;
 
-exports[`restoreElements should restore line and draw elements correctly 2`] = `
-Object {
+exports[`restoreElements > should restore line and draw elements correctly 2`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
-  "boundElements": Array [],
+  "boundElements": [],
   "endArrowhead": null,
   "endBinding": null,
   "fillStyle": "hachure",
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 100,
   "id": "id-draw01",
   "isDeleted": false,
@@ -255,18 +255,18 @@ Object {
   "link": null,
   "locked": false,
   "opacity": 100,
-  "points": Array [
-    Array [
+  "points": [
+    [
       0,
       0,
     ],
-    Array [
+    [
       100,
       100,
     ],
   ],
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 2,
   },
   "seed": Any<Number>,
@@ -285,18 +285,18 @@ Object {
 }
 `;
 
-exports[`restoreElements should restore text element correctly passing value for each attribute 1`] = `
-Object {
+exports[`restoreElements > should restore text element correctly passing value for each attribute 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "baseline": 0,
-  "boundElements": Array [],
+  "boundElements": [],
   "containerId": null,
   "fillStyle": "hachure",
   "fontFamily": 1,
   "fontSize": 14,
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 100,
   "id": "id-text01",
   "isDeleted": false,
@@ -306,7 +306,7 @@ Object {
   "opacity": 100,
   "originalText": "text",
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": Any<Number>,
@@ -326,18 +326,18 @@ Object {
 }
 `;
 
-exports[`restoreElements should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
-Object {
+exports[`restoreElements > should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
+{
   "angle": 0,
   "backgroundColor": "transparent",
   "baseline": 0,
-  "boundElements": Array [],
+  "boundElements": [],
   "containerId": null,
   "fillStyle": "hachure",
   "fontFamily": 1,
   "fontSize": 10,
   "frameId": null,
-  "groupIds": Array [],
+  "groupIds": [],
   "height": 100,
   "id": "id-text01",
   "isDeleted": false,
@@ -347,7 +347,7 @@ Object {
   "opacity": 100,
   "originalText": "test",
   "roughness": 1,
-  "roundness": Object {
+  "roundness": {
     "type": 3,
   },
   "seed": Any<Number>,

+ 6 - 5
src/tests/data/restore.test.ts

@@ -12,9 +12,10 @@ import { ImportedDataState } from "../../data/types";
 import { NormalizedZoomValue } from "../../types";
 import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants";
 import { newElementWith } from "../../element/mutateElement";
+import { vi } from "vitest";
 
 describe("restoreElements", () => {
-  const mockSizeHelper = jest.spyOn(sizeHelpers, "isInvisiblySmallElement");
+  const mockSizeHelper = vi.spyOn(sizeHelpers, "isInvisiblySmallElement");
 
   beforeEach(() => {
     mockSizeHelper.mockReset();
@@ -152,7 +153,7 @@ describe("restoreElements", () => {
   it("when arrow element has undefined endArrowHead", () => {
     const arrowElement = API.createElement({ type: "arrow" });
     Object.defineProperty(arrowElement, "endArrowhead", {
-      get: jest.fn(() => undefined),
+      get: vi.fn(() => undefined),
     });
 
     const restoredElements = restore.restoreElements([arrowElement], null);
@@ -205,7 +206,7 @@ describe("restoreElements", () => {
       [1, 1],
     ];
     Object.defineProperty(lineElement_0, "points", {
-      get: jest.fn(() => pointsEl_0),
+      get: vi.fn(() => pointsEl_0),
     });
 
     const pointsEl_1 = [
@@ -213,7 +214,7 @@ describe("restoreElements", () => {
       [5, 6],
     ];
     Object.defineProperty(lineElement_1, "points", {
-      get: jest.fn(() => pointsEl_1),
+      get: vi.fn(() => pointsEl_1),
     });
 
     const restoredElements = restore.restoreElements(
@@ -440,7 +441,7 @@ describe("restoreAppState", () => {
       const stubImportedAppState = getDefaultAppState();
 
       Object.defineProperty(stubImportedAppState, "zoom", {
-        get: jest.fn(() => null),
+        get: vi.fn(() => null),
       });
 
       const stubLocalAppState = getDefaultAppState();

+ 2 - 1
src/tests/dragCreate.test.tsx

@@ -10,11 +10,12 @@ import {
 } from "./test-utils";
 import { ExcalidrawLinearElement } from "../element/types";
 import { reseed } from "../random";
+import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 3 - 2
src/tests/fitToContent.test.tsx

@@ -2,6 +2,7 @@ import { render } from "./test-utils";
 import { API } from "./helpers/api";
 
 import ExcalidrawApp from "../excalidraw-app";
+import { vi } from "vitest";
 
 const { h } = window;
 
@@ -97,11 +98,11 @@ const waitForNextAnimationFrame = () => {
 
 describe("fitToContent animated", () => {
   beforeEach(() => {
-    jest.spyOn(window, "requestAnimationFrame");
+    vi.spyOn(window, "requestAnimationFrame");
   });
 
   afterEach(() => {
-    jest.restoreAllMocks();
+    vi.restoreAllMocks();
   });
 
   it("should ease scroll the viewport to the selected element", async () => {

+ 13 - 14
src/tests/flip.test.tsx

@@ -20,21 +20,21 @@ import ExcalidrawApp from "../excalidraw-app";
 import { mutateElement } from "../element/mutateElement";
 import { NormalizedZoomValue } from "../types";
 import { ROUNDNESS } from "../constants";
+import { vi } from "vitest";
+import * as blob from "../data/blob";
 
 const { h } = window;
-
 const mouse = new Pointer("mouse");
-jest.mock("../data/blob", () => {
-  const originalModule = jest.requireActual("../data/blob");
-
-  //Prevent Node.js modules errors (document is not defined etc...)
-  return {
-    __esModule: true,
-    ...originalModule,
-    resizeImageFile: (imageFile: File) => imageFile,
-    generateIdFromFile: () => "fileId" as FileId,
-  };
-});
+// This needs to fixed in vitest mock, as when importActual used with mock
+// the tests hangs - https://github.com/vitest-dev/vitest/issues/546.
+// But fortunately spying and mocking the return value of spy works :p
+
+const resizeImageFileSpy = vi.spyOn(blob, "resizeImageFile");
+const generateIdFromFileSpy = vi.spyOn(blob, "generateIdFromFile");
+
+resizeImageFileSpy.mockImplementation(async (imageFile: File) => imageFile);
+generateIdFromFileSpy.mockImplementation(async () => "fileId" as FileId);
+
 beforeEach(async () => {
   // Unmount ReactDOM from root
   ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -42,7 +42,7 @@ beforeEach(async () => {
   mouse.reset();
   localStorage.clear();
   sessionStorage.clear();
-  jest.clearAllMocks();
+  vi.clearAllMocks();
 
   Object.assign(document, {
     elementFromPoint: () => GlobalTestState.canvas,
@@ -732,7 +732,6 @@ describe("image", () => {
   it("flips an unrotated image horizontally correctly", async () => {
     //paste image
     await createImage();
-
     await waitFor(() => {
       expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
       expect(API.getSelectedElements().length).toBeGreaterThan(0);

+ 10 - 5
src/tests/library.test.tsx

@@ -1,3 +1,4 @@
+import { vi } from "vitest";
 import { fireEvent, render, waitFor } from "./test-utils";
 import { queryByTestId } from "@testing-library/react";
 
@@ -29,11 +30,15 @@ const mockLibraryFilePromise = new Promise<Blob>(async (resolve, reject) => {
   }
 });
 
-jest.mock("../data/filesystem.ts", () => ({
-  __esmodule: true,
-  ...jest.requireActual("../data/filesystem.ts"),
-  fileOpen: jest.fn(() => mockLibraryFilePromise),
-}));
+vi.mock("../data/filesystem.ts", async (importOriginal) => {
+  const module = await importOriginal();
+  return {
+    __esmodule: true,
+    //@ts-ignore
+    ...module,
+    fileOpen: vi.fn(() => mockLibraryFilePromise),
+  };
+});
 
 describe("library", () => {
   beforeEach(async () => {

+ 151 - 150
src/tests/linearElementEditor.test.tsx

@@ -24,8 +24,9 @@ import {
 } from "../element/textElement";
 import * as textElementUtils from "../element/textElement";
 import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
+import { vi } from "vitest";
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 
 const { h } = window;
 const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
@@ -179,16 +180,16 @@ describe("Test Linear Elements", () => {
     expect(renderScene).toHaveBeenCalledTimes(11);
     expect(line.points.length).toEqual(3);
     expect(line.points).toMatchInlineSnapshot(`
-      Array [
-        Array [
+      [
+        [
           0,
           0,
         ],
-        Array [
+        [
           70,
           50,
         ],
-        Array [
+        [
           40,
           0,
         ],
@@ -273,16 +274,16 @@ describe("Test Linear Elements", () => {
 
       expect(line.points.length).toEqual(3);
       expect(line.points).toMatchInlineSnapshot(`
-        Array [
-          Array [
+        [
+          [
             0,
             0,
           ],
-          Array [
+          [
             70,
             50,
           ],
-          Array [
+          [
             40,
             0,
           ],
@@ -315,12 +316,12 @@ describe("Test Linear Elements", () => {
       expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
 
       expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
-        Array [
-          Array [
+        [
+          [
             55.9697848965255,
             47.442326230998205,
           ],
-          Array [
+          [
             76.08587175006699,
             43.294165939653226,
           ],
@@ -363,12 +364,12 @@ describe("Test Linear Elements", () => {
       expect(midPoints[0]).not.toEqual(newMidPoints[0]);
       expect(midPoints[1]).not.toEqual(newMidPoints[1]);
       expect(newMidPoints).toMatchInlineSnapshot(`
-        Array [
-          Array [
+        [
+          [
             105.96978489652551,
             67.4423262309982,
           ],
-          Array [
+          [
             126.08587175006699,
             63.294165939653226,
           ],
@@ -412,29 +413,29 @@ describe("Test Linear Elements", () => {
 
         expect((h.elements[0] as ExcalidrawLinearElement).points)
           .toMatchInlineSnapshot(`
-          Array [
-            Array [
-              0,
-              0,
-            ],
-            Array [
-              85,
-              75,
-            ],
-            Array [
-              70,
-              50,
-            ],
-            Array [
-              105,
-              70,
-            ],
-            Array [
-              40,
-              0,
-            ],
-          ]
-        `);
+            [
+              [
+                0,
+                0,
+              ],
+              [
+                85,
+                75,
+              ],
+              [
+                70,
+                50,
+              ],
+              [
+                105,
+                70,
+              ],
+              [
+                40,
+                0,
+              ],
+            ]
+          `);
       });
 
       it("should update only the first segment midpoint when its point is dragged", async () => {
@@ -558,29 +559,29 @@ describe("Test Linear Elements", () => {
 
         expect((h.elements[0] as ExcalidrawLinearElement).points)
           .toMatchInlineSnapshot(`
-          Array [
-            Array [
-              0,
-              0,
-            ],
-            Array [
-              85.96978489652551,
-              77.4423262309982,
-            ],
-            Array [
-              70,
-              50,
-            ],
-            Array [
-              106.08587175006699,
-              73.29416593965323,
-            ],
-            Array [
-              40,
-              0,
-            ],
-          ]
-        `);
+            [
+              [
+                0,
+                0,
+              ],
+              [
+                85.96978489652551,
+                77.4423262309982,
+              ],
+              [
+                70,
+                50,
+              ],
+              [
+                106.08587175006699,
+                73.29416593965323,
+              ],
+              [
+                40,
+                0,
+              ],
+            ]
+          `);
       });
 
       it("should update all the midpoints when its point is dragged", async () => {
@@ -606,12 +607,12 @@ describe("Test Linear Elements", () => {
         expect(midPoints[0]).not.toEqual(newMidPoints[0]);
         expect(midPoints[1]).not.toEqual(newMidPoints[1]);
         expect(newMidPoints).toMatchInlineSnapshot(`
-          Array [
-            Array [
+          [
+            [
               31.884084517616053,
               23.13275505472383,
             ],
-            Array [
+            [
               77.74792546875662,
               44.57840982272327,
             ],
@@ -667,12 +668,12 @@ describe("Test Linear Elements", () => {
         expect(midPoints[0]).not.toEqual(newMidPoints[0]);
         expect(midPoints[1]).not.toEqual(newMidPoints[1]);
         expect(newMidPoints).toMatchInlineSnapshot(`
-          Array [
-            Array [
+          [
+            [
               55.9697848965255,
               47.442326230998205,
             ],
-            Array [
+            [
               76.08587175006699,
               43.294165939653226,
             ],
@@ -704,12 +705,12 @@ describe("Test Linear Elements", () => {
         [dragEndPositionOffset[0] + line.x, dragEndPositionOffset[1] + line.y],
       );
       expect(line.points).toMatchInlineSnapshot(`
-        Array [
-          Array [
+        [
+          [
             0,
             0,
           ],
-          Array [
+          [
             -60,
             -100,
           ],
@@ -768,7 +769,7 @@ describe("Test Linear Elements", () => {
           textElement,
         );
         expect(position).toMatchInlineSnapshot(`
-          Object {
+          {
             "x": 25,
             "y": 10,
           }
@@ -790,7 +791,7 @@ describe("Test Linear Elements", () => {
           textElement,
         );
         expect(position).toMatchInlineSnapshot(`
-          Object {
+          {
             "x": 75,
             "y": 60,
           }
@@ -824,7 +825,7 @@ describe("Test Linear Elements", () => {
           textElement,
         );
         expect(position).toMatchInlineSnapshot(`
-          Object {
+          {
             "x": 85.82201843191861,
             "y": 75.63461309860818,
           }
@@ -939,11 +940,11 @@ describe("Test Linear Elements", () => {
       expect(textElement.angle).toBe(0);
       expect(getBoundTextElementPosition(arrow, textElement))
         .toMatchInlineSnapshot(`
-        Object {
-          "x": 75,
-          "y": 60,
-        }
-      `);
+          {
+            "x": 75,
+            "y": 60,
+          }
+        `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made 
@@ -951,26 +952,26 @@ describe("Test Linear Elements", () => {
       `);
       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
         .toMatchInlineSnapshot(`
-        Array [
-          20,
-          20,
-          105,
-          80,
-          55.45893770831013,
-          45,
-        ]
-      `);
+          [
+            20,
+            20,
+            105,
+            80,
+            55.45893770831013,
+            45,
+          ]
+        `);
 
       rotate(container, -35, 55);
       expect(container.angle).toMatchInlineSnapshot(`1.3988061968364685`);
       expect(textElement.angle).toBe(0);
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-        Object {
-          "x": 21.73926141863671,
-          "y": 73.31003398390868,
-        }
-      `);
+          {
+            "x": 21.73926141863671,
+            "y": 73.31003398390868,
+          }
+        `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made 
@@ -978,15 +979,15 @@ describe("Test Linear Elements", () => {
       `);
       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
         .toMatchInlineSnapshot(`
-        Array [
-          20,
-          20,
-          102.41961302274555,
-          86.49012635273976,
-          55.45893770831013,
-          45,
-        ]
-      `);
+          [
+            20,
+            20,
+            102.41961302274555,
+            86.49012635273976,
+            55.45893770831013,
+            45,
+          ]
+        `);
     });
 
     it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
@@ -1004,11 +1005,11 @@ describe("Test Linear Elements", () => {
       expect(container.height).toBe(50);
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-        Object {
-          "x": 75,
-          "y": 60,
-        }
-      `);
+          {
+            "x": 75,
+            "y": 60,
+          }
+        `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made 
@@ -1016,33 +1017,33 @@ describe("Test Linear Elements", () => {
       `);
       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
         .toMatchInlineSnapshot(`
-        Array [
-          20,
-          20,
-          105,
-          80,
-          55.45893770831013,
-          45,
-        ]
-      `);
+          [
+            20,
+            20,
+            105,
+            80,
+            55.45893770831013,
+            45,
+          ]
+        `);
 
       resize(container, "ne", [300, 200]);
 
       expect({ width: container.width, height: container.height })
         .toMatchInlineSnapshot(`
-        Object {
-          "height": 130,
-          "width": 367,
-        }
-      `);
+          {
+            "height": 130,
+            "width": 367,
+          }
+        `);
 
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-        Object {
-          "x": 272,
-          "y": 45,
-        }
-      `);
+          {
+            "x": 272,
+            "y": 45,
+          }
+        `);
       expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
         .toMatchInlineSnapshot(`
         "Online whiteboard 
@@ -1050,15 +1051,15 @@ describe("Test Linear Elements", () => {
       `);
       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
         .toMatchInlineSnapshot(`
-        Array [
-          20,
-          35,
-          502,
-          95,
-          205.9061448421403,
-          52.5,
-        ]
-      `);
+          [
+            20,
+            35,
+            502,
+            95,
+            205.9061448421403,
+            52.5,
+          ]
+        `);
     });
 
     it("should resize and position the bound text correctly when 2 pointer linear element resized", () => {
@@ -1072,11 +1073,11 @@ describe("Test Linear Elements", () => {
       expect(container.width).toBe(40);
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-        Object {
-          "x": 25,
-          "y": 10,
-        }
-      `);
+          {
+            "x": 25,
+            "y": 10,
+          }
+        `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made 
@@ -1089,19 +1090,19 @@ describe("Test Linear Elements", () => {
 
       expect({ width: container.width, height: container.height })
         .toMatchInlineSnapshot(`
-        Object {
-          "height": 130,
-          "width": 340,
-        }
-      `);
+          {
+            "height": 130,
+            "width": 340,
+          }
+        `);
 
       expect(getBoundTextElementPosition(container, textElement))
         .toMatchInlineSnapshot(`
-        Object {
-          "x": 75,
-          "y": -5,
-        }
-      `);
+          {
+            "x": 75,
+            "y": -5,
+          }
+        `);
       expect(textElement.text).toMatchInlineSnapshot(`
         "Online whiteboard 
         collaboration made easy"
@@ -1154,7 +1155,7 @@ describe("Test Linear Elements", () => {
         "Online whiteboard collaboration
         made easy"
       `);
-      const handleBindTextResizeSpy = jest.spyOn(
+      const handleBindTextResizeSpy = vi.spyOn(
         textElementUtils,
         "handleBindTextResize",
       );

+ 2 - 1
src/tests/move.test.tsx

@@ -12,11 +12,12 @@ import {
 } from "../element/types";
 import { UI, Pointer, Keyboard } from "./helpers/ui";
 import { KEYS } from "../keys";
+import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 2 - 1
src/tests/multiPointCreate.test.tsx

@@ -10,11 +10,12 @@ import * as Renderer from "../renderer/renderScene";
 import { KEYS } from "../keys";
 import { ExcalidrawLinearElement } from "../element/types";
 import { reseed } from "../random";
+import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 3 - 3
src/tests/packages/__snapshots__/excalidraw.test.tsx.snap

@@ -1,6 +1,6 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`<Excalidraw/> <MainMenu/> should render main menu with host menu items if passed from host 1`] = `
+exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = `
 <div
   class="dropdown-menu"
   data-testid="dropdown-menu"
@@ -108,7 +108,7 @@ exports[`<Excalidraw/> <MainMenu/> should render main menu with host menu items
 </div>
 `;
 
-exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should render menu with default items when "UIOPtions" is "undefined" 1`] = `
+exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should render menu with default items when "UIOPtions" is "undefined" 1`] = `
 <div
   class="dropdown-menu"
   data-testid="dropdown-menu"

+ 11 - 11
src/tests/packages/__snapshots__/utils.test.ts.snap

@@ -1,9 +1,9 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`exportToSvg with default arguments 1`] = `
-Object {
+exports[`exportToSvg > with default arguments 1`] = `
+{
   "activeEmbeddable": null,
-  "activeTool": Object {
+  "activeTool": {
     "customType": null,
     "lastActiveTool": null,
     "locked": false,
@@ -40,7 +40,7 @@ Object {
   "exportScale": 1,
   "exportWithDarkMode": false,
   "fileHandle": null,
-  "frameRendering": Object {
+  "frameRendering": {
     "clip": true,
     "enabled": true,
     "name": true,
@@ -59,21 +59,21 @@ Object {
   "openMenu": null,
   "openPopup": null,
   "openSidebar": null,
-  "pasteDialog": Object {
+  "pasteDialog": {
     "data": null,
     "shown": false,
   },
   "penDetected": false,
   "penMode": false,
   "pendingImageElementId": null,
-  "previousSelectedElementIds": Object {},
+  "previousSelectedElementIds": {},
   "resizingElement": null,
   "scrollX": 0,
   "scrollY": 0,
   "scrolledOutside": false,
-  "selectedElementIds": Object {},
+  "selectedElementIds": {},
   "selectedElementsAreBeingDragged": false,
-  "selectedGroupIds": Object {},
+  "selectedGroupIds": {},
   "selectedLinearElement": null,
   "selectionElement": null,
   "shouldCacheIgnoreZoom": false,
@@ -81,13 +81,13 @@ Object {
   "showStats": false,
   "showWelcomeScreen": false,
   "startBoundElement": null,
-  "suggestedBindings": Array [],
+  "suggestedBindings": [],
   "theme": "light",
   "toast": null,
   "viewBackgroundColor": "#ffffff",
   "viewModeEnabled": false,
   "zenModeEnabled": false,
-  "zoom": Object {
+  "zoom": {
     "value": 1,
   },
 }

+ 6 - 4
src/tests/packages/excalidraw.test.tsx

@@ -1,6 +1,6 @@
 import { fireEvent, GlobalTestState, toggleMenu, render } from "../test-utils";
 import { Excalidraw, Footer, MainMenu } from "../../packages/excalidraw/index";
-import { queryByText, queryByTestId } from "@testing-library/react";
+import { queryByText, queryByTestId, screen } from "@testing-library/react";
 import { GRID_SIZE, THEME } from "../../constants";
 import { t } from "../../i18n";
 import { useMemo } from "react";
@@ -42,7 +42,7 @@ describe("<Excalidraw/>", () => {
         container.getElementsByClassName("disable-zen-mode--visible").length,
       ).toBe(0);
       expect(h.state.zenModeEnabled).toBe(true);
-
+      screen.debug();
       fireEvent.contextMenu(GlobalTestState.canvas, {
         button: 2,
         clientX: 1,
@@ -74,7 +74,8 @@ describe("<Excalidraw/>", () => {
         </Footer>
       </Excalidraw>,
     ));
-    expect(container.querySelector(".footer-center")).toMatchInlineSnapshot(`
+    expect(container.querySelector(".footer-center")).toMatchInlineSnapshot(
+      `
       <div
         class="footer-center zen-mode-transition"
       >
@@ -82,7 +83,8 @@ describe("<Excalidraw/>", () => {
           This is a custom footer
         </div>
       </div>
-    `);
+    `,
+    );
   });
 
   describe("Test gridModeEnabled prop", () => {

+ 13 - 17
src/tests/packages/utils.test.ts

@@ -1,15 +1,13 @@
 import * as utils from "../../packages/utils";
 import { diagramFactory } from "../fixtures/diagramFixture";
+import { vi } from "vitest";
 import * as mockedSceneExportUtils from "../../scene/export";
+
 import { MIME_TYPES } from "../../constants";
 
-jest.mock("../../scene/export", () => ({
-  __esmodule: true,
-  ...jest.requireActual("../../scene/export"),
-  exportToSvg: jest.fn(),
-}));
+const exportToSvgSpy = vi.spyOn(mockedSceneExportUtils, "exportToSvg");
 
-describe("exportToCanvas", () => {
+describe("exportToCanvas", async () => {
   const EXPORT_PADDING = 10;
 
   it("with default arguments", async () => {
@@ -32,10 +30,9 @@ describe("exportToCanvas", () => {
   });
 });
 
-describe("exportToBlob", () => {
+describe("exportToBlob", async () => {
   describe("mime type", () => {
-    afterEach(jest.restoreAllMocks);
-
+    // afterEach(vi.restoreAllMocks);
     it("should change image/jpg to image/jpeg", async () => {
       const blob = await utils.exportToBlob({
         ...diagramFactory(),
@@ -48,7 +45,6 @@ describe("exportToBlob", () => {
       });
       expect(blob?.type).toBe(MIME_TYPES.jpg);
     });
-
     it("should default to image/png", async () => {
       const blob = await utils.exportToBlob({
         ...diagramFactory(),
@@ -57,16 +53,14 @@ describe("exportToBlob", () => {
     });
 
     it("should warn when using quality with image/png", async () => {
-      const consoleSpy = jest
+      const consoleSpy = vi
         .spyOn(console, "warn")
         .mockImplementationOnce(() => void 0);
-
       await utils.exportToBlob({
         ...diagramFactory(),
         mimeType: MIME_TYPES.png,
         quality: 1,
       });
-
       expect(consoleSpy).toHaveBeenCalledWith(
         `"quality" will be ignored for "${MIME_TYPES.png}" mimeType`,
       );
@@ -75,10 +69,12 @@ describe("exportToBlob", () => {
 });
 
 describe("exportToSvg", () => {
-  const mockedExportUtil = mockedSceneExportUtils.exportToSvg as jest.Mock;
-  const passedElements = () => mockedExportUtil.mock.calls[0][0];
-  const passedOptions = () => mockedExportUtil.mock.calls[0][1];
-  afterEach(jest.resetAllMocks);
+  const passedElements = () => exportToSvgSpy.mock.calls[0][0];
+  const passedOptions = () => exportToSvgSpy.mock.calls[0][1];
+
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
 
   it("with default arguments", async () => {
     await utils.exportToSvg({

+ 3 - 1
src/tests/regressionTests.test.tsx

@@ -17,10 +17,11 @@ import {
 } from "./test-utils";
 import { defaultLang } from "../i18n";
 import { FONT_FAMILY } from "../constants";
+import { vi } from "vitest";
 
 const { h } = window;
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 
 const mouse = new Pointer("mouse");
 const finger1 = new Pointer("touch", 1);
@@ -156,6 +157,7 @@ describe("regression tests", () => {
   }
   it("change the properties of a shape", () => {
     UI.clickTool("rectangle");
+
     mouse.down(10, 10);
     mouse.up(10, 10);
     togglePopover("Background");

+ 2 - 1
src/tests/resize.test.tsx

@@ -9,11 +9,12 @@ import { ExcalidrawTextElement } from "../element/types";
 import ExcalidrawApp from "../excalidraw-app";
 import { API } from "./helpers/api";
 import { KEYS } from "../keys";
+import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

文件差异内容过多而无法显示
+ 3 - 3
src/tests/scene/__snapshots__/export.test.ts.snap


+ 1 - 1
src/tests/scene/export.test.ts

@@ -61,7 +61,7 @@ describe("exportToSvg", () => {
     );
 
     expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
-      `"themeFilter"`,
+      '"_themeFilter_f32792"',
     );
   });
 

+ 2 - 1
src/tests/selection.test.tsx

@@ -13,11 +13,12 @@ import { reseed } from "../random";
 import { API } from "./helpers/api";
 import { Keyboard, Pointer, UI } from "./helpers/ui";
 import { SHAPES } from "../shapes";
+import { vi } from "vitest";
 
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
-const renderScene = jest.spyOn(Renderer, "renderScene");
+const renderScene = vi.spyOn(Renderer, "renderScene");
 beforeEach(() => {
   localStorage.clear();
   renderScene.mockClear();

+ 2 - 2
src/utils.ts

@@ -160,7 +160,7 @@ export const throttleRAF = <T extends any[]>(
   };
 
   const ret = (...args: T) => {
-    if (process.env.NODE_ENV === "test") {
+    if (import.meta.env.MODE === "test") {
       fn(...args);
       return;
     }
@@ -772,7 +772,7 @@ export const arrayToMapWithIndex = <T extends { id: string }>(
     return acc;
   }, new Map<string, [element: T, index: number]>());
 
-export const isTestEnv = () => process.env.NODE_ENV === "test";
+export const isTestEnv = () => import.meta.env.MODE === "test";
 
 export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
   return new CustomEvent(name, {

+ 59 - 0
src/vite-env.d.ts

@@ -0,0 +1,59 @@
+/// <reference types="vite/client" />
+/// <reference types="vite-plugin-pwa/react" />
+/// <reference types="vite-plugin-pwa/info" />
+/// <reference types="vite-plugin-svgr/client" />
+
+interface ImportMetaEnv {
+  // The port to run the dev server
+  VITE_APP_PORT: string;
+
+  VITE_APP_BACKEND_V2_GET_URL: string;
+  VITE_APP_BACKEND_V2_POST_URL: string;
+
+  VITE_APP_LIBRARY_URL: string;
+  VITE_APP_LIBRARY_BACKEND: string;
+
+  // collaboration WebSocket server (https: string
+  VITE_APP_WS_SERVER_URL: string;
+
+  // set this only if using the collaboration workflow we use on excalidraw.com
+  VITE_APP_PORTAL_URL: string;
+
+  VITE_APP_FIREBASE_CONFIG: string;
+
+  // whether to enable Service Workers in development
+  VITE_APP_DEV_ENABLE_SW: string;
+  // whether to disable live reload / HMR. Usuaully what you want to do when
+  // debugging Service Workers.
+  VITE_APP_DEV_DISABLE_LIVE_RELOAD: string;
+
+  FAST_REFRESH: string;
+
+  // MATOMO
+  VITE_APP_MATOMO_URL: string;
+  VITE_APP_CDN_MATOMO_TRACKER_URL: string;
+  VITE_APP_MATOMO_SITE_ID: string;
+
+  //Debug flags
+
+  // To enable bounding box for text containers
+  VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX: string;
+  VITE_APP_DISABLE_SENTRY: string;
+  // Set this flag to false if you want to open the overlay by default
+  VITE_APP_COLLAPSE_OVERLAY: string;
+  // Enable eslint in dev server
+  VITE_APP_ENABLE_ESLINT: string;
+
+  VITE_PKG_NAME: string;
+  VITE_PKG_VERSION: string;
+  VITE_IS_EXCALIDRAW_NPM_PACKAGE: string;
+
+  VITE_WORKER_ID: string;
+  MODE: string;
+  DEV: string;
+  PROD: string;
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv;
+}

+ 2 - 1
tsconfig-types.json

@@ -1,6 +1,7 @@
 {
   "include": ["src/packages/excalidraw", "src/global.d.ts", "src/css.d.ts"],
   "compilerOptions": {
+    "types": ["vite/client", "vite-plugin-svgr/client"],
     "allowJs": true,
     "declaration": true,
     "emitDeclarationOnly": true,
@@ -8,7 +9,7 @@
     "jsx": "react-jsx",
     "target": "es6",
     "lib": ["dom", "dom.iterable", "esnext"],
-    "module": "esnext",
+    "module": "ESNext",
     "moduleResolution": "node",
     "resolveJsonModule": true,
     "skipLibCheck": true,

+ 3 - 3
tsconfig.json

@@ -1,6 +1,6 @@
 {
   "compilerOptions": {
-    "target": "es6",
+    "target": "ESNext",
     "lib": ["dom", "dom.iterable", "esnext"],
     "allowJs": true,
     "skipLibCheck": true,
@@ -8,9 +8,9 @@
     "allowSyntheticDefaultImports": true,
     "strict": true,
     "forceConsistentCasingInFileNames": true,
-    "module": "esnext",
-    "moduleResolution": "node",
     "noFallthroughCasesInSwitch": true,
+    "module": "ESNext",
+    "moduleResolution": "node",
     "resolveJsonModule": true,
     "isolatedModules": true,
     "noEmit": true,

+ 181 - 0
vite.config.ts

@@ -0,0 +1,181 @@
+import { defineConfig, loadEnv } from "vite";
+import react from "@vitejs/plugin-react";
+import svgrPlugin from "vite-plugin-svgr";
+import { ViteEjsPlugin } from "vite-plugin-ejs";
+import { VitePWA } from "vite-plugin-pwa";
+import checker from "vite-plugin-checker";
+
+// To load .env.local variables
+const envVars = loadEnv("", process.cwd());
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  server: {
+    port: Number(envVars.VITE_APP_PORT || 3000),
+    // open the browser
+    open: true,
+  },
+  build: {
+    outDir: "build",
+    rollupOptions: {
+      output: {
+        // Creating separate chunk for locales except for en and percentages.json so they
+        // can be cached at runtime and not merged with
+        // app precache. en.json and percentages.json are needed for first load
+        // or fallback hence not clubbing with locales so first load followed by offline mode works fine. This is how CRA used to work too.
+        manualChunks(id) {
+          if (
+            id.includes("src/locales") &&
+            id.match(/en.json|percentages.json/) === null
+          ) {
+            const index = id.indexOf("locales/");
+            // Taking the substring after "locales/"
+            return `locales/${id.substring(index + 8)}`;
+          }
+        },
+      },
+    },
+    sourcemap: true,
+  },
+  plugins: [
+    react(),
+    checker({
+      typescript: true,
+      eslint:
+        envVars.VITE_APP_ENABLE_ESLINT === "false"
+          ? undefined
+          : { lintCommand: 'eslint "./src/**/*.{js,ts,tsx}"' },
+      overlay: {
+        initialIsOpen: envVars.VITE_APP_COLLAPSE_OVERLAY === "false",
+        badgeStyle: "margin-bottom: 4rem; margin-left: 1rem",
+      },
+    }),
+    svgrPlugin(),
+    ViteEjsPlugin(),
+    VitePWA({
+      devOptions: {
+        /* set this flag to true to enable in Development mode */
+        enabled: false,
+      },
+
+      workbox: {
+        // Don't push fonts and locales to app precache
+        globIgnores: ["fonts.css", "**/locales/**"],
+        runtimeCaching: [
+          {
+            urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
+            handler: "CacheFirst",
+            options: {
+              cacheName: "fonts",
+              expiration: {
+                maxEntries: 50,
+                maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
+              },
+            },
+          },
+          {
+            urlPattern: new RegExp("fonts.css"),
+            handler: "StaleWhileRevalidate",
+            options: {
+              cacheName: "fonts",
+              expiration: {
+                maxEntries: 50,
+              },
+            },
+          },
+          {
+            urlPattern: new RegExp("locales/[^/]+.js"),
+            handler: "CacheFirst",
+            options: {
+              cacheName: "locales",
+              expiration: {
+                maxEntries: 50,
+                maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
+              },
+            },
+          },
+        ],
+      },
+      manifest: {
+        short_name: "Excalidraw",
+        name: "Excalidraw",
+        description:
+          "Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them.",
+        icons: [
+          {
+            src: "logo-180x180.png",
+            sizes: "180x180",
+            type: "image/png",
+          },
+          {
+            src: "apple-touch-icon.png",
+            type: "image/png",
+            sizes: "256x256",
+          },
+        ],
+        start_url: "/",
+        display: "standalone",
+        theme_color: "#121212",
+        background_color: "#ffffff",
+        file_handlers: [
+          {
+            action: "/",
+            accept: {
+              "application/vnd.excalidraw+json": [".excalidraw"],
+            },
+          },
+        ],
+        share_target: {
+          action: "/web-share-target",
+          method: "POST",
+          enctype: "multipart/form-data",
+          params: {
+            files: [
+              {
+                name: "file",
+                accept: [
+                  "application/vnd.excalidraw+json",
+                  "application/json",
+                  ".excalidraw",
+                ],
+              },
+            ],
+          },
+        },
+        screenshots: [
+          {
+            src: "/screenshots/virtual-whiteboard.png",
+            type: "image/png",
+            sizes: "462x945",
+          },
+          {
+            src: "/screenshots/wireframe.png",
+            type: "image/png",
+            sizes: "462x945",
+          },
+          {
+            src: "/screenshots/illustration.png",
+            type: "image/png",
+            sizes: "462x945",
+          },
+          {
+            src: "/screenshots/shapes.png",
+            type: "image/png",
+            sizes: "462x945",
+          },
+          {
+            src: "/screenshots/collaboration.png",
+            type: "image/png",
+            sizes: "462x945",
+          },
+          {
+            src: "/screenshots/export.png",
+            type: "image/png",
+            sizes: "462x945",
+          },
+        ],
+      },
+    }),
+  ],
+  publicDir: "./public",
+});

+ 9 - 0
vitest.config.ts

@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+  test: {
+    setupFiles: ["./src/setupTests.ts"],
+    globals: true,
+    environment: "jsdom",
+  },
+});

文件差异内容过多而无法显示
+ 437 - 331
yarn.lock


部分文件因为文件数量过多而无法显示