瀏覽代碼

feat: self-hosting existing google fonts (#8540)

Marcel Mraz 10 月之前
父節點
當前提交
a80cb5896a

+ 0 - 9
excalidraw-app/index.html

@@ -130,15 +130,6 @@
     </script>
     <% } %>
 
-    <!-- For Nunito only preload the latin range, which should be good enough for now -->
-    <link
-      rel="preload"
-      href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
-      as="font"
-      type="font/woff2"
-      crossorigin="anonymous"
-    />
-
     <!-- Register Assistant as the UI font, before the scene inits -->
     <link
       rel="stylesheet"

+ 2 - 0
excalidraw-app/vite.config.mts

@@ -48,6 +48,8 @@ export default defineConfig({
       },
     },
     sourcemap: true,
+    // don't auto-inline small assets (i.e. fonts hosted on CDN)
+    assetsInlineLimit: 0,
   },
   plugins: [
     woff2BrowserPlugin(),

二進制
packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2


二進制
packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2


二進制
packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2


二進制
packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2


二進制
packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2


二進制
packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2


二進制
packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2


+ 8 - 8
packages/excalidraw/fonts/index.ts

@@ -24,14 +24,14 @@ import Cascadia from "./assets/CascadiaCode-Regular.woff2";
 import ComicShanns from "./assets/ComicShanns-Regular.woff2";
 import LiberationSans from "./assets/LiberationSans-Regular.woff2";
 
-import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
-import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
-
-import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
-import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
-import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
-import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
-import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
+import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
+import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
+
+import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
+import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
+import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
+import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
+import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
 
 export class Fonts {
   // it's ok to track fonts across multiple instances only once, so let's use

文件差異過大導致無法顯示
+ 0 - 1
packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap


+ 0 - 1
packages/utils/package.json

@@ -68,7 +68,6 @@
     "css-loader": "6.7.1",
     "file-loader": "6.2.0",
     "fonteditor-core": "2.4.0",
-    "node-fetch": "3.3.2",
     "sass-loader": "13.0.2",
     "ts-loader": "9.3.1",
     "typescript": "4.9.4",

+ 5 - 3
scripts/buildPackage.js

@@ -1,7 +1,6 @@
 const { build } = require("esbuild");
 const { sassPlugin } = require("esbuild-sass-plugin");
 const { externalGlobalPlugin } = require("esbuild-plugin-external-global");
-const { woff2BrowserPlugin } = require("./woff2/woff2-esbuild-plugins");
 
 // Will be used later for treeshaking
 //const fs = require("fs");
@@ -45,13 +44,15 @@ const browserConfig = {
   format: "esm",
   plugins: [
     sassPlugin(),
-    woff2BrowserPlugin(),
     externalGlobalPlugin({
       react: "React",
       "react-dom": "ReactDOM",
     }),
   ],
   splitting: true,
+  loader: {
+    ".woff2": "file",
+  },
 };
 const createESMBrowserBuild = async () => {
   // Development unminified build with source maps
@@ -100,9 +101,10 @@ const rawConfig = {
   entryPoints: ["index.tsx"],
   bundle: true,
   format: "esm",
-  plugins: [sassPlugin(), woff2BrowserPlugin()],
+  plugins: [sassPlugin()],
   loader: {
     ".json": "copy",
+    ".woff2": "file",
   },
   packages: "external",
 };

+ 5 - 5
scripts/buildUtils.js

@@ -1,17 +1,17 @@
 const fs = require("fs");
 const { build } = require("esbuild");
 const { sassPlugin } = require("esbuild-sass-plugin");
-const {
-  woff2BrowserPlugin,
-  woff2ServerPlugin,
-} = require("./woff2/woff2-esbuild-plugins");
+const { woff2ServerPlugin } = require("./woff2/woff2-esbuild-plugins");
 
 const browserConfig = {
   entryPoints: ["index.ts"],
   bundle: true,
   format: "esm",
-  plugins: [sassPlugin(), woff2BrowserPlugin()],
+  plugins: [sassPlugin()],
   assetNames: "assets/[name]",
+  loader: {
+    ".woff2": "file",
+  },
 };
 
 // Will be used later for treeshaking

+ 2 - 63
scripts/woff2/woff2-esbuild-plugins.js

@@ -2,45 +2,9 @@ const fs = require("fs");
 const path = require("path");
 const { execSync } = require("child_process");
 const which = require("which");
-const fetch = require("node-fetch");
 const wawoff = require("wawoff2");
 const { Font } = require("fonteditor-core");
 
-/**
- * Custom esbuild plugin to convert url woff2 imports into a text.
- * Other woff2 imports are handled by a "file" loader.
- *
- * @returns {import("esbuild").Plugin}
- */
-module.exports.woff2BrowserPlugin = () => {
-  return {
-    name: "woff2BrowserPlugin",
-    setup(build) {
-      build.initialOptions.loader = {
-        ".woff2": "file",
-        ...build.initialOptions.loader,
-      };
-
-      build.onResolve({ filter: /^https:\/\/.+?\.woff2$/ }, (args) => {
-        return {
-          path: args.path,
-          namespace: "woff2BrowserPlugin",
-        };
-      });
-
-      build.onLoad(
-        { filter: /.*/, namespace: "woff2BrowserPlugin" },
-        async (args) => {
-          return {
-            contents: args.path,
-            loader: "text",
-          };
-        },
-      );
-    },
-  };
-};
-
 /**
  * Custom esbuild plugin to:
  * 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs)
@@ -53,27 +17,6 @@ module.exports.woff2BrowserPlugin = () => {
  * @returns {import("esbuild").Plugin}
  */
 module.exports.woff2ServerPlugin = (options = {}) => {
-  // google CDN fails time to time, so let's retry
-  async function fetchRetry(url, options = {}, retries = 0, delay = 1000) {
-    try {
-      const response = await fetch(url, options);
-
-      if (!response.ok) {
-        throw new Error(`Status: ${response.status}, ${await response.json()}`);
-      }
-
-      return response;
-    } catch (e) {
-      if (retries > 0) {
-        await new Promise((resolve) => setTimeout(resolve, delay));
-        return fetchRetry(url, options, retries - 1, delay * 2);
-      }
-
-      console.error(`Couldn't fetch: ${url}, error: ${e.message}`);
-      throw e;
-    }
-  }
-
   return {
     name: "woff2ServerPlugin",
     setup(build) {
@@ -82,9 +25,7 @@ module.exports.woff2ServerPlugin = (options = {}) => {
       const fonts = new Map();
 
       build.onResolve({ filter: /\.woff2$/ }, (args) => {
-        const resolvedPath = args.path.startsWith("http")
-          ? args.path // url
-          : path.resolve(args.resolveDir, args.path); // absolute path
+        const resolvedPath = path.resolve(args.resolveDir, args.path);
 
         return {
           path: resolvedPath,
@@ -101,9 +42,7 @@ module.exports.woff2ServerPlugin = (options = {}) => {
             // read local woff2 as a buffer (WARN: `readFileSync` does not work!)
             woff2Buffer = await fs.promises.readFile(args.path);
           } else {
-            // fetch remote woff2 as a buffer (i.e. from a cdn)
-            const response = await fetchRetry(args.path, {}, 3);
-            woff2Buffer = await response.buffer();
+            throw new Error(`Font path has to be absolute! "${args.path}"`);
           }
 
           // google's brotli decompression into snft

+ 13 - 33
scripts/woff2/woff2-vite-plugins.js

@@ -1,15 +1,13 @@
+// `EXCALIDRAW_ASSET_PATH` as a SSOT
 const OSS_FONTS_CDN =
   "https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
 
 /**
- * Custom vite plugin to convert url woff2 imports into a text.
- * Other woff2 imports are automatically served and resolved as a file uri.
+ * Custom vite plugin for auto-prefixing `EXCALIDRAW_ASSET_PATH` woff2 fonts in `excalidraw-app`.
  *
  * @returns {import("vite").PluginOption}
  */
 module.exports.woff2BrowserPlugin = () => {
-  // for now limited to woff2 only, might be extended to any assets in the future
-  const regex = /^https:\/\/.+?\.woff2$/;
   let isDev;
 
   return {
@@ -18,34 +16,9 @@ module.exports.woff2BrowserPlugin = () => {
     config(_, { command }) {
       isDev = command === "serve";
     },
-    resolveId(source) {
-      if (!regex.test(source)) {
-        return null;
-      }
-
-      // getting the url to the dependency tree
-      return source;
-    },
-    load(id) {
-      if (!regex.test(id)) {
-        return null;
-      }
-
-      // loading the url as string
-      return `export default "${id}"`;
-    },
-    // necessary for dev as vite / rollup does skips https imports in serve (~dev) mode
-    // aka dev mode equivalent of "export default x" above (resolveId + load)
     transform(code, id) {
-      // treat https woff2 imports as a text
-      if (isDev && id.endsWith("/excalidraw/fonts/index.ts")) {
-        return code.replaceAll(
-          /import\s+(\w+)\s+from\s+(["']https:\/\/.+?\.woff2["'])/g,
-          `const $1 = $2`,
-        );
-      }
-
-      // use CDN for Assistant
+      // using copy / replace as fonts defined in the `.css` don't have to be manually copied over (vite/rollup does this automatically),
+      // but at the same time can't be easily prefixed with the `EXCALIDRAW_ASSET_PATH` only for the `excalidraw-app`
       if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) {
         return `/* WARN: The following content is generated during excalidraw-app build */
 
@@ -90,7 +63,6 @@ module.exports.woff2BrowserPlugin = () => {
       }`;
       }
 
-      // using EXCALIDRAW_ASSET_PATH as a SSOT
       if (!isDev && id.endsWith("excalidraw-app/index.html")) {
         return code.replace(
           "<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->",
@@ -110,9 +82,10 @@ module.exports.woff2BrowserPlugin = () => {
         type="font/woff2"
         crossorigin="anonymous"
       />
+      <!-- For Nunito only preload the latin range, which should be good enough for now -->
       <link
         rel="preload"
-        href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
+        href="${OSS_FONTS_CDN}Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg-DqUjjPte.woff2"
         as="font"
         type="font/woff2"
         crossorigin="anonymous"
@@ -124,6 +97,13 @@ module.exports.woff2BrowserPlugin = () => {
         type="font/woff2"
         crossorigin="anonymous"
       />
+      <link
+        rel="preload"
+        href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
+        as="font"
+        type="font/woff2"
+        crossorigin="anonymous"
+      />
     `,
         );
       }

+ 0 - 2
vitest.config.mts

@@ -1,9 +1,7 @@
 import { defineConfig } from "vitest/config";
-import { woff2BrowserPlugin } from "./scripts/woff2/woff2-vite-plugins";
 
 export default defineConfig({
   //@ts-ignore
-  plugins: [woff2BrowserPlugin()],
   test: {
     // Since hooks are running in stack in v2, which means all hooks run serially whereas
     // we need to run them in parallel

+ 3 - 67
yarn.lock

@@ -5194,11 +5194,6 @@ damerau-levenshtein@^1.0.8:
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
   integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
 
-data-uri-to-buffer@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
-  integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
-
 data-urls@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4"
@@ -6262,14 +6257,6 @@ fd-slicer@~1.1.0:
   dependencies:
     pend "~1.2.0"
 
-fetch-blob@^3.1.2, fetch-blob@^3.1.4:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
-  integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
-  dependencies:
-    node-domexception "^1.0.0"
-    web-streams-polyfill "^3.0.3"
-
 fflate@^0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
@@ -6408,13 +6395,6 @@ form-data@^4.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
-formdata-polyfill@^4.0.10:
-  version "4.0.10"
-  resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
-  integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
-  dependencies:
-    fetch-blob "^3.1.2"
-
 fraction.js@^4.2.0:
   version "4.3.7"
   resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
@@ -8256,11 +8236,6 @@ no-case@^3.0.4:
     lower-case "^2.0.2"
     tslib "^2.0.3"
 
-node-domexception@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
-  integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
-
 [email protected]:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
@@ -8273,15 +8248,6 @@ [email protected]:
   dependencies:
     whatwg-url "^5.0.0"
 
[email protected]:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
-  integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==
-  dependencies:
-    data-uri-to-buffer "^4.0.0"
-    fetch-blob "^3.1.4"
-    formdata-polyfill "^4.0.10"
-
 node-html-parser@^5.3.3:
   version "5.4.2"
   resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a"
@@ -9667,7 +9633,7 @@ string-natural-compare@^3.0.1:
   resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
   integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
 
-"string-width-cjs@npm:string-width@^4.2.0":
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9685,15 +9651,6 @@ string-width@^4.1.0, string-width@^4.2.0:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string-width@^4.2.3:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
-  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
-
 string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -9765,14 +9722,7 @@ stringify-object@^3.3.0:
     is-obj "^1.0.1"
     is-regexp "^1.0.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
[email protected], strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", [email protected], strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -10562,11 +10512,6 @@ [email protected]:
   dependencies:
     argparse "^2.0.1"
 
-web-streams-polyfill@^3.0.3:
-  version "3.3.3"
-  resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
-  integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
-
 web-worker@^1.2.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.3.0.tgz#e5f2df5c7fe356755a5fb8f8410d4312627e6776"
@@ -11009,7 +10954,7 @@ [email protected], workbox-window@^7.0.0:
     "@types/trusted-types" "^2.0.2"
     workbox-core "7.1.0"
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11027,15 +10972,6 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
-wrap-ansi@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
-  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
-  dependencies:
-    ansi-styles "^4.0.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-
 wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"

部分文件因文件數量過多而無法顯示