feat(frontend): add mock dev mode

This commit is contained in:
Sergey Krylov 2025-09-04 05:51:25 +03:00
parent 06897a86db
commit 5b91157e6c
21 changed files with 846 additions and 502 deletions

View File

@ -0,0 +1,3 @@
BACKEND_BASE_URL="http://localhost:3000"
PUBLIC_API_BASE_URL="/api"
PUBLIC_ENABLE_MOCKING=false

2
frontend/.env.mock Normal file
View File

@ -0,0 +1,2 @@
PUBLIC_API_BASE_URL="/"
PUBLIC_ENABLE_MOCKING=true

View File

@ -0,0 +1,72 @@
export const items1 = [
{
id: '1',
expenseListId: '1',
title: 'Счет за интернет',
description: 'Ежемесячная плата за интернет',
date: '2024-04-01',
amount: 500,
currency: 'RUB',
count: 1,
createdAt: '2024-04-01T12:00:00Z',
updatedAt: '2024-04-01T12:00:00Z',
},
{
id: '2',
expenseListId: '1',
title: 'Покупка продуктов',
description: 'Продукты на неделю',
date: '2024-04-02',
amount: 3000,
currency: 'RUB',
count: 1,
createdAt: '2024-04-02T12:00:00Z',
updatedAt: '2024-04-02T12:00:00Z',
},
];
export const items2 = [];
export const items3 = null;
export const items4 = [
{
id: '3',
expenseListId: '4',
title: 'Бензин',
description: null,
date: '2024-04-03',
amount: 1500,
currency: 'RUB',
count: null,
createdAt: '2024-04-03T12:00:00Z',
updatedAt: '2024-04-03T12:00:00Z',
},
];
export const items5 = [
{
id: '4',
expenseListId: '5',
title: 'Ноутбук',
description: 'Покупка нового ноутбука',
date: '2024-04-04',
amount: 50000,
currency: 'RUB',
count: 1,
createdAt: '2024-04-04T12:00:00Z',
updatedAt: '2024-04-04T12:00:00Z',
},
{
id: '5',
expenseListId: '5',
title: 'Мышка',
description: 'Механическая мышка',
date: '2024-04-05',
amount: 3000,
currency: 'RUB',
count: 1,
createdAt: '2024-04-05T12:00:00Z',
updatedAt: '2024-04-05T12:00:00Z',
},
];

View File

@ -0,0 +1,47 @@
import {
items1,
items2,
items3,
items4,
items5,
} from './items';
export const list1 = {
id: '1',
title: 'Расходы на быт',
items: items1,
createdAt: '2024-03-28T10:00:00Z',
updatedAt: '2024-04-02T12:00:00Z',
};
export const list2 = {
id: '2',
title: 'Дополнительные расходы',
items: items2,
createdAt: '2024-04-01T10:00:00Z',
updatedAt: '2024-04-01T10:00:00Z',
};
export const list3 = {
id: '3',
title: 'Кредиты',
items: items3,
createdAt: '2024-04-01T11:00:00Z',
updatedAt: '2024-04-01T11:00:00Z',
};
export const list4 = {
id: '4',
title: 'Автомобиль',
items: items4,
createdAt: '2024-04-03T10:00:00Z',
updatedAt: '2024-04-03T10:00:00Z',
};
export const list5 = {
id: '5',
title: 'Техника',
items: items5,
createdAt: '2024-04-04T10:00:00Z',
updatedAt: '2024-04-05T10:00:00Z',
};

View File

@ -0,0 +1,9 @@
import { http, HttpResponse } from 'msw';
import type { paths } from '@/shared/api/schema';
type LoginResponse = paths['/auth/login']['post']['responses']['200']['content']['application/json'];
export const loginHandler = http.post<never, never, LoginResponse>(
'/auth/login',
() => HttpResponse.json({ accessToken: '123' }),
);

View File

@ -0,0 +1,12 @@
import { http, HttpResponse } from 'msw';
import type { paths } from '@/shared/api/schema';
type ExpensesListItemsResponse = paths['/expenses/list/{id}']['get']['responses']['200']['content']['application/json'];
export const getExpenseItemListById = (id: string) => http.get<never, never, ExpensesListItemsResponse>(
`/expenses/list/${id}`,
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
async () => HttpResponse.json((await import('@mocks/browser/data/expenses/lists'))[`list${id}`]),
);

View File

@ -0,0 +1,25 @@
import { http, HttpResponse } from 'msw';
import {
list1,
list2,
list3,
list4,
list5,
} from '@mocks/browser/data/expenses/lists';
import type { paths } from '@/shared/api/schema';
const expenseLists = [
list1,
list2,
list3,
list4,
list5,
];
type ExpensesListResponse = paths['/expenses/list']['get']['responses']['200']['content']['application/json'];
export const getExpenseList = http.get<never, never, ExpensesListResponse>(
'/expenses/list',
() => HttpResponse.json(expenseLists),
);

View File

@ -0,0 +1,3 @@
export { getExpenseList } from './expense/list/get';
export { getExpenseItemListById } from './expense/list/:id/get';
export { loginHandler } from './auth/login/post';

View File

@ -0,0 +1,17 @@
import { setupWorker } from 'msw/browser';
import {
getExpenseList,
getExpenseItemListById,
loginHandler,
} from './handlers';
export const worker = setupWorker(
loginHandler,
getExpenseList,
getExpenseItemListById('1'),
getExpenseItemListById('2'),
getExpenseItemListById('3'),
getExpenseItemListById('4'),
getExpenseItemListById('5'),
);

View File

@ -15,9 +15,9 @@
"react-router": "7.8.1"
},
"devDependencies": {
"@rsbuild/core": "1.4.15",
"@rsbuild/core": "1.5.3",
"@rsbuild/plugin-eslint": "1.1.2",
"@rsbuild/plugin-react": "1.3.5",
"@rsbuild/plugin-react": "1.4.0",
"@rsbuild/plugin-type-check": "1.2.4",
"@swc/core": "1.13.5",
"@swc/jest": "0.2.39",
@ -29,7 +29,7 @@
"@types/jest": "30.0.0",
"@types/react": "19.1.9",
"@types/react-dom": "19.1.7",
"@typescript-eslint/parser": "8.39.0",
"@typescript-eslint/parser": "8.42.0",
"eslint": "9.34.0",
"eslint-config-ksv741": "0.2.0",
"identity-obj-proxy": "3.0.0",
@ -2518,62 +2518,62 @@
}
},
"node_modules/@module-federation/error-codes": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.17.1.tgz",
"integrity": "sha512-n6Elm4qKSjwAPxLUGtwnl7qt4y1dxB8OpSgVvXBIzqI9p27a3ZXshLPLnumlpPg1Qudaj8sLnSnFtt9yGpt5yQ==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.18.0.tgz",
"integrity": "sha512-Woonm8ehyVIUPXChmbu80Zj6uJkC0dD9SJUZ/wOPtO8iiz/m+dkrOugAuKgoiR6qH4F+yorWila954tBz4uKsQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@module-federation/runtime": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.17.1.tgz",
"integrity": "sha512-vKEN32MvUbpeuB/s6UXfkHDZ9N5jFyDDJnj83UTJ8n4N1jHIJu9VZ6Yi4/Ac8cfdvU8UIK9bIbfVXWbUYZUDsw==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.18.0.tgz",
"integrity": "sha512-+C4YtoSztM7nHwNyZl6dQKGUVJdsPrUdaf3HIKReg/GQbrt9uvOlUWo2NXMZ8vDAnf/QRrpSYAwXHmWDn9Obaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@module-federation/error-codes": "0.17.1",
"@module-federation/runtime-core": "0.17.1",
"@module-federation/sdk": "0.17.1"
"@module-federation/error-codes": "0.18.0",
"@module-federation/runtime-core": "0.18.0",
"@module-federation/sdk": "0.18.0"
}
},
"node_modules/@module-federation/runtime-core": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.17.1.tgz",
"integrity": "sha512-LCtIFuKgWPQ3E+13OyrVpuTPOWBMI/Ggwsq1Q874YeT8Px28b8tJRCj09DjyRFyhpSPyV/uG80T6iXPAUoLIfQ==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.18.0.tgz",
"integrity": "sha512-ZyYhrDyVAhUzriOsVfgL6vwd+5ebYm595Y13KeMf6TKDRoUHBMTLGQ8WM4TDj8JNsy7LigncK8C03fn97of0QQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@module-federation/error-codes": "0.17.1",
"@module-federation/sdk": "0.17.1"
"@module-federation/error-codes": "0.18.0",
"@module-federation/sdk": "0.18.0"
}
},
"node_modules/@module-federation/runtime-tools": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.17.1.tgz",
"integrity": "sha512-4kr6zTFFwGywJx6whBtxsc84V+COAuuBpEdEbPZN//YLXhNB0iz2IGsy9r9wDl+06h84bD+3dQ05l9euRLgXzQ==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.18.0.tgz",
"integrity": "sha512-fSga9o4t1UfXNV/Kh6qFvRyZpPp3EHSPRISNeyT8ZoTpzDNiYzhtw0BPUSSD8m6C6XQh2s/11rI4g80UY+d+hA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@module-federation/runtime": "0.17.1",
"@module-federation/webpack-bundler-runtime": "0.17.1"
"@module-federation/runtime": "0.18.0",
"@module-federation/webpack-bundler-runtime": "0.18.0"
}
},
"node_modules/@module-federation/sdk": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.17.1.tgz",
"integrity": "sha512-nlUcN6UTEi+3HWF+k8wPy7gH0yUOmCT+xNatihkIVR9REAnr7BUvHFGlPJmx7WEbLPL46+zJUbtQHvLzXwFhng==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.18.0.tgz",
"integrity": "sha512-Lo/Feq73tO2unjmpRfyyoUkTVoejhItXOk/h5C+4cistnHbTV8XHrW/13fD5e1Iu60heVdAhhelJd6F898Ve9A==",
"dev": true,
"license": "MIT"
},
"node_modules/@module-federation/webpack-bundler-runtime": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.17.1.tgz",
"integrity": "sha512-Swspdgf4PzcbvS9SNKFlBzfq8h/Qxwqjq/xRSqw1pqAZWondZQzwTTqPXhgrg0bFlz7qWjBS/6a8KuH/gRvGaQ==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.18.0.tgz",
"integrity": "sha512-TEvErbF+YQ+6IFimhUYKK3a5wapD90d90sLsNpcu2kB3QGT7t4nIluE25duXuZDVUKLz86tEPrza/oaaCWTpvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@module-federation/runtime": "0.17.1",
"@module-federation/sdk": "0.17.1"
"@module-federation/runtime": "0.18.0",
"@module-federation/sdk": "0.18.0"
}
},
"node_modules/@mswjs/interceptors": {
@ -2771,23 +2771,23 @@
}
},
"node_modules/@rsbuild/core": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.4.15.tgz",
"integrity": "sha512-KoSTtKjzQUQwamcbeCp63Ne9kL7io1WI4+skTJe2chfLz6wsp/Gfg8aKkfs1DuyG1p+zxFDcYpwTWMsNtxqqiw==",
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.5.3.tgz",
"integrity": "sha512-asJYYmpMmYVEPgqR2hoPlW5pFtMYf7+dq9/u8vP0uY+Xbi9aUoguskc783qSi1yu9KvZwDbwpbfCuoBAt8anIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/core": "1.4.11",
"@rspack/core": "1.5.2",
"@rspack/lite-tapable": "~1.0.1",
"@swc/helpers": "^0.5.17",
"core-js": "~3.45.0",
"core-js": "~3.45.1",
"jiti": "^2.5.1"
},
"bin": {
"rsbuild": "bin/rsbuild.js"
},
"engines": {
"node": ">=16.10.0"
"node": ">=18.12.0"
}
},
"node_modules/@rsbuild/plugin-eslint": {
@ -2810,13 +2810,13 @@
}
},
"node_modules/@rsbuild/plugin-react": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-1.3.5.tgz",
"integrity": "sha512-L/GoHgJV4j+EQbI4KOhe5EscM0OHgnSat1eR0Nt5P3JZxpJV2ryO5Yfx5jElPWOkYZZCuk+EWhHWDQ4CkeC5BQ==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-1.4.0.tgz",
"integrity": "sha512-YhhOUOonJBjnKpUf7E4iXKidldPWAGmYBRtDjQgcSmW4tbW0DasFpNCqLn5870Q2Ly6oCU06sLv+8G597I36+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/plugin-react-refresh": "~1.4.3",
"@rspack/plugin-react-refresh": "^1.5.0",
"react-refresh": "^0.17.0"
},
"peerDependencies": {
@ -2858,28 +2858,28 @@
}
},
"node_modules/@rspack/binding": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.4.11.tgz",
"integrity": "sha512-maGl/zRwnl0QVwkBCkgjn5PH20L9HdlRIdkYhEsfTepy5x2QZ0ti/0T49djjTJQrqb+S1i6wWQymMMMMMsxx6Q==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.5.2.tgz",
"integrity": "sha512-NKiBcsxmAzFDYRnK2ZHWbTtDFVT5/704eK4OfpgsDXPMkaMnBKijMKNgP5pbe18X4rUlz+8HnGm4+Xllo9EESw==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "1.4.11",
"@rspack/binding-darwin-x64": "1.4.11",
"@rspack/binding-linux-arm64-gnu": "1.4.11",
"@rspack/binding-linux-arm64-musl": "1.4.11",
"@rspack/binding-linux-x64-gnu": "1.4.11",
"@rspack/binding-linux-x64-musl": "1.4.11",
"@rspack/binding-wasm32-wasi": "1.4.11",
"@rspack/binding-win32-arm64-msvc": "1.4.11",
"@rspack/binding-win32-ia32-msvc": "1.4.11",
"@rspack/binding-win32-x64-msvc": "1.4.11"
"@rspack/binding-darwin-arm64": "1.5.2",
"@rspack/binding-darwin-x64": "1.5.2",
"@rspack/binding-linux-arm64-gnu": "1.5.2",
"@rspack/binding-linux-arm64-musl": "1.5.2",
"@rspack/binding-linux-x64-gnu": "1.5.2",
"@rspack/binding-linux-x64-musl": "1.5.2",
"@rspack/binding-wasm32-wasi": "1.5.2",
"@rspack/binding-win32-arm64-msvc": "1.5.2",
"@rspack/binding-win32-ia32-msvc": "1.5.2",
"@rspack/binding-win32-x64-msvc": "1.5.2"
}
},
"node_modules/@rspack/binding-darwin-arm64": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.11.tgz",
"integrity": "sha512-PrmBVhR8MC269jo6uQ+BMy1uwIDx0HAJYLQRQur8gXiehWabUBCRg/d4U9KR7rLzdaSScRyc5JWXR52T7/4MfA==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.5.2.tgz",
"integrity": "sha512-aO76T6VQvAFt1LJNRA5aPOJ+szeTLlzC5wubsnxgWWjG53goP+Te35kFjDIDe+9VhKE/XqRId6iNAymaEsN+Uw==",
"cpu": [
"arm64"
],
@ -2891,9 +2891,9 @@
]
},
"node_modules/@rspack/binding-darwin-x64": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.4.11.tgz",
"integrity": "sha512-YIV8Wzy+JY0SoSsVtN4wxFXOjzxxVPnVXNswrrfqVUTPr9jqGOFYUWCGpbt8lcCgfuBFm6zN8HpOsKm1xUNsVA==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.5.2.tgz",
"integrity": "sha512-XNSmUOwdGs2PEdCKTFCC0/vu/7U9nMhAlbHJKlmdt0V4iPvFyaNWxkNdFqzLc05jlJOfgDdwbwRb91y9IcIIFQ==",
"cpu": [
"x64"
],
@ -2905,9 +2905,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.11.tgz",
"integrity": "sha512-ms6uwECUIcu+6e82C5HJhRMHnfsI+l33v7XQezntzRPN0+sG3EpikEoT7SGbgt4vDwaWLR7wS20suN4qd5r3GA==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.5.2.tgz",
"integrity": "sha512-rNxRfgC5khlrhyEP6y93+45uQ4TI7CdtWqh5PKsaR6lPepG1rH4L8VE+etejSdhzXH6wQ76Rw4wzb96Hx+5vuQ==",
"cpu": [
"arm64"
],
@ -2919,9 +2919,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-musl": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.4.11.tgz",
"integrity": "sha512-9evq0DOdxMN/H8VM8ZmyY9NSuBgILNVV6ydBfVPMHPx4r1E7JZGpWeKDegZcS5Erw3sS9kVSIxyX78L5PDzzKw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.5.2.tgz",
"integrity": "sha512-kTFX+KsGgArWC5q+jJWz0K/8rfVqZOn1ojv1xpCCcz/ogWRC/qhDGSOva6Wandh157BiR93Vfoe1gMvgjpLe5g==",
"cpu": [
"arm64"
],
@ -2933,9 +2933,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-gnu": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.11.tgz",
"integrity": "sha512-bHYFLxPPYBOSaHdQbEoCYGMQ1gOrEWj7Mro/DLfSHZi1a0okcQ2Q1y0i1DczReim3ZhLGNrK7k1IpFXCRbAobQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.5.2.tgz",
"integrity": "sha512-Lh/6WZGq30lDV6RteQQu7Phw0RH2Z1f4kGR+MsplJ6X4JpnziDow+9oxKdu6FvFHWxHByncpveVeInusQPmL7Q==",
"cpu": [
"x64"
],
@ -2947,9 +2947,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-musl": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.4.11.tgz",
"integrity": "sha512-wrm4E7q2k4+cwT6Uhp6hIQ3eUe/YoaUttj6j5TqHYZX6YeLrNPtD9+ne6lQQ17BV8wmm6NZsmoFIJ5xIptpRhQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.5.2.tgz",
"integrity": "sha512-CsLC/SIOIFs6CBmusSAF0FECB62+J36alMdwl7j6TgN6nX3UQQapnL1aVWuQaxU6un/1Vpim0V/EZbUYIdJQ4g==",
"cpu": [
"x64"
],
@ -2961,9 +2961,9 @@
]
},
"node_modules/@rspack/binding-wasm32-wasi": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.4.11.tgz",
"integrity": "sha512-hiYxHZjaZ17wQtXyLCK0IdtOvMWreGVTiGsaHCxyeT+SldDG+r16bXNjmlqfZsjlfl1mkAqKz1dg+mMX28OTqw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.5.2.tgz",
"integrity": "sha512-cuVbGr1b4q0Z6AtEraI3becZraPMMgZtZPRaIsVLeDXCmxup/maSAR3T6UaGf4Q2SNcFfjw4neGz5UJxPK8uvA==",
"cpu": [
"wasm32"
],
@ -2975,9 +2975,9 @@
}
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.4.11.tgz",
"integrity": "sha512-+HF/mnjmTr8PC1dccRt1bkrD2tPDGeqvXC1BBLYd/Klq1VbtIcnrhfmvQM6KaXbiLcY9VWKzcZPOTmnyZ8TaHQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.5.2.tgz",
"integrity": "sha512-4vJQdzRTSuvmvL3vrOPuiA7f9v9frNc2RFWDxqg+GYt0YAjDStssp+lkVbRYyXnTYVJkARSuO6N+BOiI+kLdsQ==",
"cpu": [
"arm64"
],
@ -2989,9 +2989,9 @@
]
},
"node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.4.11.tgz",
"integrity": "sha512-EU2fQGwrRfwFd/tcOInlD0jy6gNQE4Q3Ayj0Is+cX77sbhPPyyOz0kZDEaQ4qaN2VU8w4Hu/rrD7c0GAKLFvCw==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.5.2.tgz",
"integrity": "sha512-zPbu3lx/NrNxdjZzTIjwD0mILUOpfhuPdUdXIFiOAO8RiWSeQpYOvyI061s/+bNOmr4A+Z0uM0dEoOClfkhUFg==",
"cpu": [
"ia32"
],
@ -3003,9 +3003,9 @@
]
},
"node_modules/@rspack/binding-win32-x64-msvc": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.4.11.tgz",
"integrity": "sha512-1Nc5ZzWqfvE+iJc47qtHFzYYnHsC3awavXrCo74GdGip1vxtksM3G30BlvAQHHVtEmULotWqPbjZpflw/Xk9Ag==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.5.2.tgz",
"integrity": "sha512-duLNUTshX38xhC10/W9tpkPca7rOifP2begZjdb1ikw7C4AI0I7VnBnYt8qPSxGISoclmhOBxU/LuAhS8jMMlg==",
"cpu": [
"x64"
],
@ -3017,18 +3017,18 @@
]
},
"node_modules/@rspack/core": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.4.11.tgz",
"integrity": "sha512-JtKnL6p7Kc/YgWQJF3Woo4OccbgKGyT/4187W4dyex8BMkdQcbqCNIdi6dFk02hwQzxpOOxRSBI4hlGRbz7oYQ==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.5.2.tgz",
"integrity": "sha512-ifjHqLczC81d1xjXPXCzxTFKNOFsEzuuLN44cMnyzQ/GWi4B48fyX7JHndWE7Lxd54cW1O9Ik7AdBN3Gq891EA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@module-federation/runtime-tools": "0.17.1",
"@rspack/binding": "1.4.11",
"@module-federation/runtime-tools": "0.18.0",
"@rspack/binding": "1.5.2",
"@rspack/lite-tapable": "1.0.1"
},
"engines": {
"node": ">=16.0.0"
"node": ">=18.12.0"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.1"
@ -3050,9 +3050,9 @@
}
},
"node_modules/@rspack/plugin-react-refresh": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.4.3.tgz",
"integrity": "sha512-wZx4vWgy5oMEvgyNGd/oUKcdnKaccYWHCRkOqTdAPJC3WcytxhTX+Kady8ERurSBiLyQpoMiU3Iyd+F1Y2Arbw==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.5.0.tgz",
"integrity": "sha512-pYOmc1mrK8Ui/7VWUgjKt9YqrxFn4woURTgGpFYWwsFvJxmWm05zog4fUbChvErbaBHkx1aA+KHxIvM/6tFODg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3104,14 +3104,14 @@
}
},
"node_modules/@stylistic/eslint-plugin": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.0.tgz",
"integrity": "sha512-RCEdbREv9EBiToUBQTlRhVYKG093I6ZnnQ990j08eJ6uRZh71DXkOnoxtTLfDQ6utVCVQzrhZFHZP0zfrfOIjA==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.3.1.tgz",
"integrity": "sha512-Ykums1VYonM0TgkD0VteVq9mrlO2FhF48MDJnPyv3MktIB2ydtuhlO0AfWm7xnW1kyf5bjOqA6xc7JjviuVTxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/types": "^8.37.0",
"@typescript-eslint/types": "^8.41.0",
"eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"estraverse": "^5.3.0",
@ -3813,17 +3813,57 @@
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/parser": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz",
"integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==",
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
"integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/type-utils": "8.42.0",
"@typescript-eslint/utils": "8.42.0",
"@typescript-eslint/visitor-keys": "8.42.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.42.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz",
"integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.42.0",
"@typescript-eslint/visitor-keys": "8.42.0",
"debug": "^4.3.4"
},
"engines": {
@ -3839,14 +3879,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz",
"integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==",
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz",
"integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.39.0",
"@typescript-eslint/types": "^8.39.0",
"@typescript-eslint/tsconfig-utils": "^8.42.0",
"@typescript-eslint/types": "^8.42.0",
"debug": "^4.3.4"
},
"engines": {
@ -3861,14 +3901,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz",
"integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==",
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz",
"integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0"
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/visitor-keys": "8.42.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3879,9 +3919,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz",
"integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==",
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz",
"integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -3895,10 +3935,35 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz",
"integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.42.0",
"@typescript-eslint/utils": "8.42.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz",
"integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==",
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz",
"integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==",
"dev": true,
"license": "MIT",
"engines": {
@ -3910,16 +3975,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz",
"integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==",
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz",
"integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.39.0",
"@typescript-eslint/tsconfig-utils": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"@typescript-eslint/project-service": "8.42.0",
"@typescript-eslint/tsconfig-utils": "8.42.0",
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/visitor-keys": "8.42.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -3978,16 +4043,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz",
"integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==",
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz",
"integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0"
"@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/types": "8.42.0",
"@typescript-eslint/typescript-estree": "8.42.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -4002,13 +4067,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz",
"integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==",
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz",
"integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/types": "8.42.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@ -5308,9 +5373,9 @@
}
},
"node_modules/core-js": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -5968,66 +6033,6 @@
}
}
},
"node_modules/eslint-config-ksv741/node_modules/@typescript-eslint/scope-manager": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz",
"integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/eslint-config-ksv741/node_modules/@typescript-eslint/types": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz",
"integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/eslint-config-ksv741/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz",
"integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/eslint-config-ksv741/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/eslint-config-ksv741/node_modules/globals": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
@ -6041,241 +6046,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-config-ksv741/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/eslint-config-ksv741/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/eslint-config-ksv741/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-config-ksv741/node_modules/typescript-eslint": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz",
"integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/utils": "8.37.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/eslint-config-ksv741/node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
"integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/type-utils": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.37.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/eslint-config-ksv741/node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz",
"integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/eslint-config-ksv741/node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/eslint-config-ksv741/node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz",
"integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.37.0",
"@typescript-eslint/tsconfig-utils": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/eslint-config-ksv741/node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
"integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.37.0",
"@typescript-eslint/types": "^8.37.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/eslint-config-ksv741/node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz",
"integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/eslint-config-ksv741/node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz",
"integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/eslint-import-context": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz",
@ -12872,6 +12642,30 @@
"node": ">=14.17"
}
},
"node_modules/typescript-eslint": {
"version": "8.42.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.42.0.tgz",
"integrity": "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@typescript-eslint/typescript-estree": "8.42.0",
"@typescript-eslint/utils": "8.42.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View File

@ -6,11 +6,12 @@
"scripts": {
"build": "rsbuild build",
"dev": "rsbuild dev",
"dev:mock": "rsbuild dev --env-mode mock",
"preview": "rsbuild preview",
"lint": "eslint",
"test:unit": "jest",
"typecheck": "tsc --noEmit",
"openapi:gen": "npx openapi-typescript http://localhost:3000/docs/swagger.json -o src/shared/api/schema.ts"
"openapi:gen": "openapi-typescript http://localhost:3000/docs/swagger.json -o src/shared/api/schema.ts"
},
"dependencies": {
"@tanstack/react-query": "5.85.5",
@ -20,9 +21,9 @@
"react-router": "7.8.1"
},
"devDependencies": {
"@rsbuild/core": "1.4.15",
"@rsbuild/core": "1.5.3",
"@rsbuild/plugin-eslint": "1.1.2",
"@rsbuild/plugin-react": "1.3.5",
"@rsbuild/plugin-react": "1.4.0",
"@rsbuild/plugin-type-check": "1.2.4",
"@swc/core": "1.13.5",
"@swc/jest": "0.2.39",

View File

@ -0,0 +1,344 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.10.5'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
*/
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}

View File

@ -20,10 +20,12 @@ export default defineConfig({
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
[`${process.env.PUBLIC_API_BASE_URL}`]: {
target: process.env.BACKEND_BASE_URL,
changeOrigin: true,
pathRewrite: { '^/api': '' },
pathRewrite: {
[`^${process.env.PUBLIC_API_BASE_URL}`]: '',
},
},
},
},

View File

@ -45,25 +45,25 @@ const submitHandler = (requestSpy: jest.Mock) => http.post<
AddExpenseItemBody,
AddExpenseItemSuccessResponse
>(
'http://localhost:3000/expenses/list/123/items',
async ({ request }) => {
const body = await request.json();
requestSpy(body);
'/expenses/list/123/items',
async ({ request }) => {
const body = await request.json();
requestSpy(body);
return HttpResponse.json({
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
title: 'Mock item',
amount: 12,
count: 12,
updatedAt: '2025-08-29T07:57:40.503Z',
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
createdAt: '2025-08-29T07:57:40.503Z',
currency: 'RUB',
date: '2025-08-22T00:00:03.000Z',
description: 'Mock description',
});
},
);
return HttpResponse.json({
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
title: 'Mock item',
amount: 12,
count: 12,
updatedAt: '2025-08-29T07:57:40.503Z',
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
createdAt: '2025-08-29T07:57:40.503Z',
currency: 'RUB',
date: '2025-08-22T00:00:03.000Z',
description: 'Mock description',
});
},
);
describe('Test AddExpenseItemButton', () => {
test('should correct render button', () => {

View File

@ -45,7 +45,7 @@ const openModal = async () => {
};
};
const submitHandler = http.delete<DeleteExpenseItemPath>(`http://localhost:3000/expenses/list/${deletedItem.expenseListId}/items/${deletedItem.id}`, () => HttpResponse.json(deletedItem));
const submitHandler = http.delete<DeleteExpenseItemPath>(`/expenses/list/${deletedItem.expenseListId}/items/${deletedItem.id}`, () => HttpResponse.json(deletedItem));
describe('Test DeleteExpenseItemButton', () => {
test('should correct render button', () => {

View File

@ -50,18 +50,18 @@ const openModal = async () => {
};
const submitHandler = (requestSpy: jest.Mock) => http.patch<
EditExpenseItemPath,
EditExpenseItemBody,
EditExpenseItemSuccessResponse
>(
`http://localhost:3000/expenses/list/${editingItem.expenseListId}/items/${editingItem.id}`,
async ({ request }) => {
const body = await request.json();
requestSpy(body);
EditExpenseItemPath,
EditExpenseItemBody,
EditExpenseItemSuccessResponse
>(
`/expenses/list/${editingItem.expenseListId}/items/${editingItem.id}`,
async ({ request }) => {
const body = await request.json();
requestSpy(body);
return HttpResponse.json(editingItem);
},
);
return HttpResponse.json(editingItem);
},
);
const fillForm = async (data: { title: string; description: string; date: string; amount: string; count: string }) => {
const titleInputElem = screen.getByLabelText('Название');

View File

@ -37,20 +37,20 @@ const fillForm = async (data: { title: string }) => {
};
const submitHandler = (requestSpy: jest.Mock) => http.post<never, AddExpenseListBody, AddExpenseListSuccessResponse>(
'http://localhost:3000/expenses/list',
async ({ request }) => {
const body = await request.json();
requestSpy(body);
'/expenses/list',
async ({ request }) => {
const body = await request.json();
requestSpy(body);
return HttpResponse.json({
id: '10cc093c-3875-4d33-a40a-df526266e262',
title: 'Mock list',
items: [],
updatedAt: '2025-08-29T07:57:40.503Z',
createdAt: '2025-08-29T07:57:40.503Z',
});
},
);
return HttpResponse.json({
id: '10cc093c-3875-4d33-a40a-df526266e262',
title: 'Mock list',
items: [],
updatedAt: '2025-08-29T07:57:40.503Z',
createdAt: '2025-08-29T07:57:40.503Z',
});
},
);
describe('Test AddExpensesListButton', () => {
test('should correct render button', () => {

View File

@ -40,7 +40,7 @@ const openModal = async () => {
};
};
const submitHandler = http.delete<DeleteExpenseListPath>(`http://localhost:3000/expenses/list/${deletingList.id}`, () => HttpResponse.json(deletingList));
const submitHandler = http.delete<DeleteExpenseListPath>(`/expenses/list/${deletingList.id}`, () => HttpResponse.json(deletingList));
describe('Test DeleteExpensesListButton', () => {
test('should correct render button', () => {

View File

@ -45,18 +45,18 @@ const openModal = async () => {
};
const submitHandler = (requestSpy: jest.Mock) => http.patch<
EditExpenseListPath,
EditExpenseListBody,
EditExpenseListSuccessResponse
>(
`http://localhost:3000/expenses/list/${editingList.id}`,
async ({ request }) => {
const body = await request.json();
requestSpy(body);
EditExpenseListPath,
EditExpenseListBody,
EditExpenseListSuccessResponse
>(
`/expenses/list/${editingList.id}`,
async ({ request }) => {
const body = await request.json();
requestSpy(body);
return HttpResponse.json(editingList);
},
);
return HttpResponse.json(editingList);
},
);
const fillForm = async (data: { title: string }) => {
const titleInputElem = screen.getByLabelText('Название');

View File

@ -4,11 +4,24 @@ import ReactDOM from 'react-dom/client';
import { App } from '@/app/App';
const rootEl = document.getElementById('root');
async function enableMocking() {
if (process.env.PUBLIC_ENABLE_MOCKING !== 'true') {
return;
}
const { worker } = await import('@mocks/browser/msw');
return worker.start();
}
if (rootEl) {
const root = ReactDOM.createRoot(rootEl);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
enableMocking().then(() => {
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
});
}

View File

@ -4,7 +4,7 @@ import { PAGES } from '@/shared/router';
import type { paths } from './schema';
const BASE_URL = 'http://localhost:3000';
const BASE_URL = process.env.PUBLIC_API_BASE_URL;
export const api = axios.create({
baseURL: BASE_URL,