diff --git a/.changeset/chilled-falcons-battle.md b/.changeset/chilled-falcons-battle.md new file mode 100644 index 000000000..1a6f228bb --- /dev/null +++ b/.changeset/chilled-falcons-battle.md @@ -0,0 +1,5 @@ +--- +'@tanstack/virtual-core': patch +--- + +fix(virtual-core): scroll to index doesn't scroll to bottom correctly diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d8e41189f..9b72a44ba 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -28,6 +28,8 @@ jobs: uses: nrwl/nx-set-shas@v4.3.0 with: main-branch-name: main + - name: Install Playwright browsers + run: pnpm exec playwright install chromium - name: Run Checks run: pnpm run test:pr preview: diff --git a/.gitignore b/.gitignore index cc376bd6c..2a91fb574 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,8 @@ stats.html vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Playwright test artifacts +test-results/ +playwright-report/ +*.log diff --git a/knip.json b/knip.json index 639c5df88..f3a69e7a2 100644 --- a/knip.json +++ b/knip.json @@ -1,4 +1,4 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "ignoreWorkspaces": ["examples/**"] + "ignoreWorkspaces": ["examples/**", "packages/react-virtual/e2e/**"] } diff --git a/package.json b/package.json index ee01fe550..370508b6c 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "clean": "pnpm --filter \"./packages/**\" run clean", "preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...'); process.exit(1)}\" || npx -y only-allow pnpm", "test": "pnpm run test:ci", - "test:pr": "nx affected --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build", - "test:ci": "nx run-many --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build", + "test:pr": "nx affected --targets=test:sherif,test:knip,test:eslint,test:lib,test:e2e,test:types,test:build,build", + "test:ci": "nx run-many --targets=test:sherif,test:knip,test:eslint,test:lib,test:e2e,test:types,test:build,build", "test:eslint": "nx affected --target=test:eslint", "test:format": "pnpm run prettier --check", "test:sherif": "sherif", @@ -20,6 +20,7 @@ "test:lib:dev": "pnpm run test:lib && nx watch --all -- pnpm run test:lib", "test:build": "nx affected --target=test:build --exclude=examples/**", "test:types": "nx affected --target=test:types --exclude=examples/**", + "test:e2e": "nx affected --target=test:e2e --exclude=examples/**", "test:knip": "knip", "build": "nx affected --target=build --exclude=examples/**", "build:all": "nx run-many --target=build --exclude=examples/**", @@ -39,6 +40,7 @@ }, "devDependencies": { "@changesets/cli": "^2.29.4", + "@playwright/test": "^1.53.1", "@svitejs/changesets-changelog-github-compact": "^1.2.0", "@tanstack/config": "^0.18.2", "@testing-library/jest-dom": "^6.6.3", diff --git a/packages/react-virtual/e2e/app/index.html b/packages/react-virtual/e2e/app/index.html new file mode 100644 index 000000000..6d5c94aec --- /dev/null +++ b/packages/react-virtual/e2e/app/index.html @@ -0,0 +1,10 @@ + + + + + + +
+ + + diff --git a/packages/react-virtual/e2e/app/main.tsx b/packages/react-virtual/e2e/app/main.tsx new file mode 100644 index 000000000..e5273bca5 --- /dev/null +++ b/packages/react-virtual/e2e/app/main.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { useVirtualizer } from '../../src/index' + +function getRandomInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +const randomHeight = (() => { + const cache = new Map() + return (id: string) => { + const value = cache.get(id) + if (value !== undefined) { + return value + } + const v = getRandomInt(25, 100) + cache.set(id, v) + return v + } +})() + +const App = () => { + const parentRef = React.useRef(null) + const rowVirtualizer = useVirtualizer({ + count: 1002, + getScrollElement: () => parentRef.current, + estimateSize: () => 50, + debug: true, + }) + + return ( +
+ + +
+
+ {rowVirtualizer.getVirtualItems().map((v) => ( +
+
+ Row {v.index} +
+
+ ))} +
+
+
+ ) +} + +ReactDOM.createRoot(document.getElementById('root')!).render() diff --git a/packages/react-virtual/e2e/app/test/scroll.spec.ts b/packages/react-virtual/e2e/app/test/scroll.spec.ts new file mode 100644 index 000000000..65ba73b3a --- /dev/null +++ b/packages/react-virtual/e2e/app/test/scroll.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +const check = () => { + const item = document.querySelector('[data-testid="item-1000"]') + const container = document.querySelector('#scroll-container') + + if (!item || !container) throw new Error('Elements not found') + + const itemRect = item.getBoundingClientRect() + const containerRect = container.getBoundingClientRect() + const scrollTop = container.scrollTop + + const top = itemRect.top + scrollTop - containerRect.top + const botttom = top + itemRect.height + + const containerBottom = scrollTop + container.clientHeight + + return Math.abs(botttom - containerBottom) +} + +test('scrolls to index 1000', async ({ page }) => { + await page.goto('/') + await page.click('#scroll-to-1000') + + // Wait for scroll effect (including retries) + await page.waitForTimeout(1000) + + await expect(page.locator('[data-testid="item-1000"]')).toBeVisible() + + const delta = await page.evaluate(check) + console.log('bootom element detla', delta) + expect(delta).toBeLessThan(1.01) +}) diff --git a/packages/react-virtual/e2e/app/tsconfig.json b/packages/react-virtual/e2e/app/tsconfig.json new file mode 100644 index 000000000..ad08d6c2b --- /dev/null +++ b/packages/react-virtual/e2e/app/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "target": "ESNext", + "moduleResolution": "Bundler", + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/packages/react-virtual/e2e/app/vite.config.ts b/packages/react-virtual/e2e/app/vite.config.ts new file mode 100644 index 000000000..a498bf932 --- /dev/null +++ b/packages/react-virtual/e2e/app/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + root: __dirname, + plugins: [react()], +}) diff --git a/packages/react-virtual/package.json b/packages/react-virtual/package.json index fef3ac781..f311b1cac 100644 --- a/packages/react-virtual/package.json +++ b/packages/react-virtual/package.json @@ -29,7 +29,8 @@ "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", - "build": "vite build" + "build": "vite build", + "test:e2e": "playwright test" }, "type": "module", "types": "dist/esm/index.d.ts", diff --git a/packages/react-virtual/playwright.config.ts b/packages/react-virtual/playwright.config.ts new file mode 100644 index 000000000..6e9f9a5fb --- /dev/null +++ b/packages/react-virtual/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@playwright/test' + +const PORT = 5173 +const baseURL = `http://localhost:${PORT}` + +export default defineConfig({ + testDir: './e2e/app/test', + use: { + baseURL, + }, + webServer: { + command: `VITE_SERVER_PORT=${PORT} vite build --config e2e/app/vite.config.ts && VITE_SERVER_PORT=${PORT} vite preview --config e2e/app/vite.config.ts --port ${PORT}`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, +}) diff --git a/packages/react-virtual/tsconfig.json b/packages/react-virtual/tsconfig.json index 3655b9d05..effe33b1b 100644 --- a/packages/react-virtual/tsconfig.json +++ b/packages/react-virtual/tsconfig.json @@ -3,5 +3,10 @@ "compilerOptions": { "jsx": "react" }, - "include": ["src", "eslint.config.js", "vite.config.ts"] + "include": [ + "src", + "eslint.config.js", + "vite.config.ts", + "playwright.config.ts" + ] } diff --git a/packages/virtual-core/src/index.ts b/packages/virtual-core/src/index.ts index fc6449839..b4794e06e 100644 --- a/packages/virtual-core/src/index.ts +++ b/packages/virtual-core/src/index.ts @@ -359,7 +359,6 @@ export class Virtualizer< scrollElement: TScrollElement | null = null targetWindow: (Window & typeof globalThis) | null = null isScrolling = false - private scrollToIndexTimeoutId: number | null = null measurementsCache: Array = [] private itemSizeCache = new Map() private pendingMeasuredCacheIndexes: Array = [] @@ -904,7 +903,7 @@ export class Virtualizer< toOffset -= size } - const maxOffset = this.getTotalSize() - size + const maxOffset = this.getTotalSize() + this.options.scrollMargin - size return Math.max(Math.min(maxOffset, toOffset), 0) } @@ -943,19 +942,10 @@ export class Virtualizer< private isDynamicMode = () => this.elementsCache.size > 0 - private cancelScrollToIndex = () => { - if (this.scrollToIndexTimeoutId !== null && this.targetWindow) { - this.targetWindow.clearTimeout(this.scrollToIndexTimeoutId) - this.scrollToIndexTimeoutId = null - } - } - scrollToOffset = ( toOffset: number, { align = 'start', behavior }: ScrollToOffsetOptions = {}, ) => { - this.cancelScrollToIndex() - if (behavior === 'smooth' && this.isDynamicMode()) { console.warn( 'The `smooth` scroll behavior is not fully supported with dynamic size.', @@ -972,50 +962,62 @@ export class Virtualizer< index: number, { align: initialAlign = 'auto', behavior }: ScrollToIndexOptions = {}, ) => { - index = Math.max(0, Math.min(index, this.options.count - 1)) - - this.cancelScrollToIndex() - if (behavior === 'smooth' && this.isDynamicMode()) { console.warn( 'The `smooth` scroll behavior is not fully supported with dynamic size.', ) } - const offsetAndAlign = this.getOffsetForIndex(index, initialAlign) - if (!offsetAndAlign) return + index = Math.max(0, Math.min(index, this.options.count - 1)) - const [offset, align] = offsetAndAlign + let attempts = 0 + const maxAttempts = 10 - this._scrollToOffset(offset, { adjustments: undefined, behavior }) + const tryScroll = (currentAlign: ScrollAlignment) => { + if (!this.targetWindow) return - if (behavior !== 'smooth' && this.isDynamicMode() && this.targetWindow) { - this.scrollToIndexTimeoutId = this.targetWindow.setTimeout(() => { - this.scrollToIndexTimeoutId = null + const offsetInfo = this.getOffsetForIndex(index, currentAlign) + if (!offsetInfo) { + console.warn('Failed to get offset for index:', index) + return + } + const [offset, align] = offsetInfo + this._scrollToOffset(offset, { adjustments: undefined, behavior }) + + this.targetWindow.requestAnimationFrame(() => { + const currentOffset = this.getScrollOffset() + const afterInfo = this.getOffsetForIndex(index, align) + if (!afterInfo) { + console.warn('Failed to get offset for index:', index) + return + } - const elementInDOM = this.elementsCache.has( - this.options.getItemKey(index), - ) + if (!approxEqual(afterInfo[0], currentOffset)) { + scheduleRetry(align) + } + }) + } - if (elementInDOM) { - const result = this.getOffsetForIndex(index, align) - if (!result) return - const [latestOffset] = result + const scheduleRetry = (align: ScrollAlignment) => { + if (!this.targetWindow) return - const currentScrollOffset = this.getScrollOffset() - if (!approxEqual(latestOffset, currentScrollOffset)) { - this.scrollToIndex(index, { align, behavior }) - } - } else { - this.scrollToIndex(index, { align, behavior }) + attempts++ + if (attempts < maxAttempts) { + if (process.env.NODE_ENV !== 'production' && this.options.debug) { + console.info('Schedule retry', attempts, maxAttempts) } - }) + this.targetWindow.requestAnimationFrame(() => tryScroll(align)) + } else { + console.warn( + `Failed to scroll to index ${index} after ${maxAttempts} attempts.`, + ) + } } + + tryScroll(initialAlign) } scrollBy = (delta: number, { behavior }: ScrollToOffsetOptions = {}) => { - this.cancelScrollToIndex() - if (behavior === 'smooth' && this.isDynamicMode()) { console.warn( 'The `smooth` scroll behavior is not fully supported with dynamic size.', diff --git a/packages/virtual-core/src/utils.ts b/packages/virtual-core/src/utils.ts index 1bb4615c2..c11b3d38c 100644 --- a/packages/virtual-core/src/utils.ts +++ b/packages/virtual-core/src/utils.ts @@ -83,7 +83,7 @@ export function notUndefined(value: T | undefined, msg?: string): T { } } -export const approxEqual = (a: number, b: number) => Math.abs(a - b) <= 1 +export const approxEqual = (a: number, b: number) => Math.abs(a - b) < 1.01 export const debounce = ( targetWindow: Window & typeof globalThis, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1859b3f87..5a1caa0f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@changesets/cli': specifier: ^2.29.4 version: 2.29.4 + '@playwright/test': + specifier: ^1.53.1 + version: 1.53.1 '@svitejs/changesets-changelog-github-compact': specifier: ^1.2.0 version: 1.2.0(encoding@0.1.13) @@ -3181,6 +3184,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.53.1': + resolution: {integrity: sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==} + engines: {node: '>=18'} + hasBin: true + '@publint/pack@0.1.2': resolution: {integrity: sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==} engines: {node: '>=18'} @@ -5218,6 +5226,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6533,6 +6546,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.53.1: + resolution: {integrity: sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.53.1: + resolution: {integrity: sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==} + engines: {node: '>=18'} + hasBin: true + postcss-loader@8.1.1: resolution: {integrity: sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==} engines: {node: '>= 18.12.0'} @@ -7967,12 +7990,12 @@ snapshots: '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.4.19(@types/node@22.15.29)(less@4.2.0)(sass@1.71.1)(terser@5.29.1)) ansi-colors: 4.1.3 autoprefixer: 10.4.18(postcss@8.4.35) - babel-loader: 9.1.3(@babel/core@7.26.10)(webpack@5.94.0) + babel-loader: 9.1.3(@babel/core@7.26.10)(webpack@5.94.0(esbuild@0.20.1)) babel-plugin-istanbul: 6.1.1 browserslist: 4.25.0 - copy-webpack-plugin: 11.0.0(webpack@5.94.0) + copy-webpack-plugin: 11.0.0(webpack@5.94.0(esbuild@0.20.1)) critters: 0.0.22 - css-loader: 6.10.0(webpack@5.94.0) + css-loader: 6.10.0(webpack@5.94.0(esbuild@0.20.1)) esbuild-wasm: 0.20.1 fast-glob: 3.3.2 http-proxy-middleware: 2.0.8(@types/express@4.17.22) @@ -7981,11 +8004,11 @@ snapshots: jsonc-parser: 3.2.1 karma-source-map-support: 1.4.0 less: 4.2.0 - less-loader: 11.1.0(less@4.2.0)(webpack@5.94.0) - license-webpack-plugin: 4.0.2(webpack@5.94.0) + less-loader: 11.1.0(less@4.2.0)(webpack@5.94.0(esbuild@0.20.1)) + license-webpack-plugin: 4.0.2(webpack@5.94.0(esbuild@0.20.1)) loader-utils: 3.2.1 magic-string: 0.30.8 - mini-css-extract-plugin: 2.8.1(webpack@5.94.0) + mini-css-extract-plugin: 2.8.1(webpack@5.94.0(esbuild@0.20.1)) mrmime: 2.0.0 open: 8.4.2 ora: 5.4.1 @@ -7993,13 +8016,13 @@ snapshots: picomatch: 4.0.1 piscina: 4.4.0 postcss: 8.4.35 - postcss-loader: 8.1.1(postcss@8.4.35)(typescript@5.2.2)(webpack@5.94.0) + postcss-loader: 8.1.1(postcss@8.4.35)(typescript@5.2.2)(webpack@5.94.0(esbuild@0.20.1)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.71.1 - sass-loader: 14.1.1(sass@1.71.1)(webpack@5.94.0) + sass-loader: 14.1.1(sass@1.71.1)(webpack@5.94.0(esbuild@0.20.1)) semver: 7.6.0 - source-map-loader: 5.0.0(webpack@5.94.0) + source-map-loader: 5.0.0(webpack@5.94.0(esbuild@0.20.1)) source-map-support: 0.5.21 terser: 5.29.1 tree-kill: 1.2.2 @@ -8008,10 +8031,10 @@ snapshots: vite: 5.4.19(@types/node@22.15.29)(less@4.2.0)(sass@1.71.1)(terser@5.29.1) watchpack: 2.4.0 webpack: 5.94.0(esbuild@0.20.1) - webpack-dev-middleware: 6.1.2(webpack@5.94.0) + webpack-dev-middleware: 6.1.2(webpack@5.94.0(esbuild@0.20.1)) webpack-dev-server: 4.15.1(webpack@5.94.0(esbuild@0.20.1)) webpack-merge: 5.10.0 - webpack-subresource-integrity: 5.1.0(webpack@5.94.0) + webpack-subresource-integrity: 5.1.0(webpack@5.94.0(esbuild@0.20.1)) optionalDependencies: esbuild: 0.20.1 ng-packagr: 17.3.0(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.2.2))(tslib@2.8.1)(typescript@5.2.2) @@ -9842,6 +9865,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.53.1': + dependencies: + playwright: 1.53.1 + '@publint/pack@0.1.2': {} '@react-hookz/web@25.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -11140,7 +11167,7 @@ snapshots: axobject-query@4.1.0: {} - babel-loader@9.1.3(@babel/core@7.26.10)(webpack@5.94.0): + babel-loader@9.1.3(@babel/core@7.26.10)(webpack@5.94.0(esbuild@0.20.1)): dependencies: '@babel/core': 7.26.10 find-cache-dir: 4.0.0 @@ -11497,7 +11524,7 @@ snapshots: dependencies: is-what: 3.14.1 - copy-webpack-plugin@11.0.0(webpack@5.94.0): + copy-webpack-plugin@11.0.0(webpack@5.94.0(esbuild@0.20.1)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -11538,7 +11565,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-loader@6.10.0(webpack@5.94.0): + css-loader@6.10.0(webpack@5.94.0(esbuild@0.20.1)): dependencies: icss-utils: 5.1.0(postcss@8.5.4) postcss: 8.5.4 @@ -12219,6 +12246,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -12853,7 +12883,7 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.2 - less-loader@11.1.0(less@4.2.0)(webpack@5.94.0): + less-loader@11.1.0(less@4.2.0)(webpack@5.94.0(esbuild@0.20.1)): dependencies: klona: 2.0.6 less: 4.2.0 @@ -12892,7 +12922,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.94.0): + license-webpack-plugin@4.0.2(webpack@5.94.0(esbuild@0.20.1)): dependencies: webpack-sources: 3.3.0 optionalDependencies: @@ -13090,7 +13120,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.8.1(webpack@5.94.0): + mini-css-extract-plugin@2.8.1(webpack@5.94.0(esbuild@0.20.1)): dependencies: schema-utils: 4.3.2 tapable: 2.2.2 @@ -13663,7 +13693,15 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 - postcss-loader@8.1.1(postcss@8.4.35)(typescript@5.2.2)(webpack@5.94.0): + playwright-core@1.53.1: {} + + playwright@1.53.1: + dependencies: + playwright-core: 1.53.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss-loader@8.1.1(postcss@8.4.35)(typescript@5.2.2)(webpack@5.94.0(esbuild@0.20.1)): dependencies: cosmiconfig: 9.0.0(typescript@5.2.2) jiti: 1.21.7 @@ -14013,7 +14051,7 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@14.1.1(sass@1.71.1)(webpack@5.94.0): + sass-loader@14.1.1(sass@1.71.1)(webpack@5.94.0(esbuild@0.20.1)): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -14284,7 +14322,7 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.94.0): + source-map-loader@5.0.0(webpack@5.94.0(esbuild@0.20.1)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 @@ -14923,7 +14961,7 @@ snapshots: schema-utils: 4.3.2 webpack: 5.94.0(esbuild@0.20.1) - webpack-dev-middleware@6.1.2(webpack@5.94.0): + webpack-dev-middleware@6.1.2(webpack@5.94.0(esbuild@0.20.1)): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -14981,7 +15019,7 @@ snapshots: webpack-sources@3.3.0: {} - webpack-subresource-integrity@5.1.0(webpack@5.94.0): + webpack-subresource-integrity@5.1.0(webpack@5.94.0(esbuild@0.20.1)): dependencies: typed-assert: 1.0.9 webpack: 5.94.0(esbuild@0.20.1)