diff --git a/package-lock.json b/package-lock.json index 2f5d474..092c5a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "eslint-plugin-react-refresh": "^0.4.24", "jsdom": "^27.2.0", "msw": "^2.12.3", + "rollup-plugin-visualizer": "^6.0.5", "typescript": "^5.9.3", "typescript-eslint": "^8.47.0", "vite": "^7.2.4", @@ -3425,6 +3426,16 @@ "dev": true, "license": "MIT" }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4256,6 +4267,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4313,6 +4340,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -4806,6 +4846,24 @@ "node": ">=0.10.0" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5393,6 +5451,60 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.5.tgz", + "integrity": "sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.0.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "rolldown": "1.x || ^1.0.0-beta", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index cba30c4..2e9660d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "eslint-plugin-react-refresh": "^0.4.24", "jsdom": "^27.2.0", "msw": "^2.12.3", + "rollup-plugin-visualizer": "^6.0.5", "typescript": "^5.9.3", "typescript-eslint": "^8.47.0", "vite": "^7.2.4", diff --git a/src/App.tsx b/src/App.tsx index d89b4b2..cb5fd21 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,16 +2,18 @@ import { CssBaseline, ThemeProvider, createTheme } from '@mui/material'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { DBProvider } from './context/DBProvider'; import { AppLayout } from './components/AppLayout'; -import { HomeScreen } from './screens/HomeScreen'; -import { StartPickListScreen } from './screens/StartPickListScreen'; -import { PickListsScreen } from './screens/PickListsScreen'; -import { ActivePickListScreen } from './screens/ActivePickListScreen'; -import { ManageProductsScreen } from './screens/ManageProductsScreen'; -import { ManageAreasScreen } from './screens/ManageAreasScreen'; -import { ManageCategoriesScreen } from './screens/ManageCategoriesScreen'; -import { BarcodeScannerScreen } from './screens/BarcodeScannerScreen'; +import React, { Suspense, lazy } from 'react'; import { useServiceWorker } from './hooks/useServiceWorker'; -import { ImportExportScreen } from './screens/ImportExportScreen'; + +const HomeScreen = lazy(() => import('./screens/HomeScreen')); +const StartPickListScreen = lazy(() => import('./screens/StartPickListScreen')); +const PickListsScreen = lazy(() => import('./screens/PickListsScreen')); +const ActivePickListScreen = lazy(() => import('./screens/ActivePickListScreen')); +const ManageProductsScreen = lazy(() => import('./screens/ManageProductsScreen')); +const ManageAreasScreen = lazy(() => import('./screens/ManageAreasScreen')); +const ManageCategoriesScreen = lazy(() => import('./screens/ManageCategoriesScreen')); +const BarcodeScannerScreen = lazy(() => import('./screens/BarcodeScannerScreen')); +const ImportExportScreen = lazy(() => import('./screens/ImportExportScreen')); const theme = createTheme({ palette: { @@ -25,19 +27,21 @@ const theme = createTheme({ const AppRoutes = () => { useServiceWorker(); return ( - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + Loading…}> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); }; diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index 02d012b..2da45cb 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -2,10 +2,11 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi, beforeEach } from 'vitest'; -import { ActivePickListScreen } from './ActivePickListScreen'; +import ActivePickListScreen from './ActivePickListScreen'; import { PickItem } from '../models/PickItem'; import { Product } from '../models/Product'; +//const ActivePickListScreen = lazy(() => import('./screens/ActivePickListScreen')); const addMock = vi.fn(); const updateMock = vi.fn(); const pickItemsMock = vi.fn<() => PickItem[]>(); diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index 481917e..1a06d36 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -34,7 +34,7 @@ import { ProductAutocomplete } from '../components/ProductAutocomplete'; const normalizeName = (name: string) => name.trim().toLowerCase(); -export const ActivePickListScreen = () => { +const ActivePickListScreen = () => { const { id } = useParams(); const pickList = usePickList(id); const items = usePickItems(id); @@ -558,3 +558,5 @@ export const ActivePickListScreen = () => { ); }; + +export default ActivePickListScreen \ No newline at end of file diff --git a/src/screens/BarcodeScannerScreen.tsx b/src/screens/BarcodeScannerScreen.tsx index e29231a..91d7a48 100644 --- a/src/screens/BarcodeScannerScreen.tsx +++ b/src/screens/BarcodeScannerScreen.tsx @@ -26,7 +26,7 @@ interface ProductLookup { pickLists: { list: PickList; area?: Area }[]; } -export const BarcodeScannerScreen = () => { +const BarcodeScannerScreen = () => { const db = useDatabase(); const navigate = useNavigate(); const [lastCode, setLastCode] = useState(''); @@ -193,3 +193,5 @@ export const BarcodeScannerScreen = () => { ); }; + +export default BarcodeScannerScreen \ No newline at end of file diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index a4a455a..dd261ca 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -1,7 +1,7 @@ import { Button, Container, Stack, Typography } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; -export const HomeScreen = () => ( +const HomeScreen = () => ( StockFill @@ -28,3 +28,5 @@ export const HomeScreen = () => ( ); + +export default HomeScreen; \ No newline at end of file diff --git a/src/screens/ImportExportScreen.tsx b/src/screens/ImportExportScreen.tsx index 33e5d68..cd84a50 100644 --- a/src/screens/ImportExportScreen.tsx +++ b/src/screens/ImportExportScreen.tsx @@ -19,7 +19,7 @@ import { ImportExportLog } from '../models/ImportExportLog'; const templateFiles: { label: string; file: string }[] = [{ label: 'Products', file: 'products_template.csv' }]; -export const ImportExportScreen = () => { +const ImportExportScreen = () => { const db = useDatabase(); const [logLines, setLogLines] = useState([]); const [history, setHistory] = useState([]); @@ -176,3 +176,5 @@ export const ImportExportScreen = () => { ); }; + +export default ImportExportScreen \ No newline at end of file diff --git a/src/screens/ManageAreasScreen.test.tsx b/src/screens/ManageAreasScreen.test.tsx index e1ed354..2c69b35 100644 --- a/src/screens/ManageAreasScreen.test.tsx +++ b/src/screens/ManageAreasScreen.test.tsx @@ -2,7 +2,7 @@ import { MemoryRouter } from 'react-router-dom'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; -import { ManageAreasScreen } from './ManageAreasScreen'; +import ManageAreasScreen from './ManageAreasScreen'; const areasMock = [{ id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }]; diff --git a/src/screens/ManageAreasScreen.tsx b/src/screens/ManageAreasScreen.tsx index d84e4d8..f031bf8 100644 --- a/src/screens/ManageAreasScreen.tsx +++ b/src/screens/ManageAreasScreen.tsx @@ -20,7 +20,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useAreas } from '../hooks/dataHooks'; import { useDatabase } from '../context/DBProvider'; -export const ManageAreasScreen = () => { +const ManageAreasScreen = () => { const db = useDatabase(); const areas = useAreas(); const [name, setName] = useState(''); @@ -128,3 +128,5 @@ export const ManageAreasScreen = () => { ); }; + +export default ManageAreasScreen \ No newline at end of file diff --git a/src/screens/ManageCategoriesScreen.test.tsx b/src/screens/ManageCategoriesScreen.test.tsx index 89a6f07..e53cc1f 100644 --- a/src/screens/ManageCategoriesScreen.test.tsx +++ b/src/screens/ManageCategoriesScreen.test.tsx @@ -2,7 +2,7 @@ import { MemoryRouter } from 'react-router-dom'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ManageCategoriesScreen } from './ManageCategoriesScreen'; +import ManageCategoriesScreen from './ManageCategoriesScreen'; const categoriesMock = [{ id: 'cat-1', name: 'Snacks', created_at: 0, updated_at: 0 }]; const productsMock = [ diff --git a/src/screens/ManageCategoriesScreen.tsx b/src/screens/ManageCategoriesScreen.tsx index 5ea1422..3e33199 100644 --- a/src/screens/ManageCategoriesScreen.tsx +++ b/src/screens/ManageCategoriesScreen.tsx @@ -20,7 +20,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useDatabase } from '../context/DBProvider'; import { useCategories, useProducts } from '../hooks/dataHooks'; -export const ManageCategoriesScreen = () => { +const ManageCategoriesScreen = () => { const db = useDatabase(); const categories = useCategories(); const products = useProducts(); @@ -198,3 +198,5 @@ const deleteCategory = async (categoryId: string, categoryName: string) => { ); }; + +export default ManageCategoriesScreen \ No newline at end of file diff --git a/src/screens/ManageProductsScreen.test.tsx b/src/screens/ManageProductsScreen.test.tsx index 8b545fd..d35ea03 100644 --- a/src/screens/ManageProductsScreen.test.tsx +++ b/src/screens/ManageProductsScreen.test.tsx @@ -4,7 +4,7 @@ import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; -import { ManageProductsScreen } from './ManageProductsScreen'; +import ManageProductsScreen from './ManageProductsScreen'; let mockScannedBarcode = '123456'; const mockUseProducts = vi.fn(); diff --git a/src/screens/ManageProductsScreen.tsx b/src/screens/ManageProductsScreen.tsx index f6d3d58..5776fda 100644 --- a/src/screens/ManageProductsScreen.tsx +++ b/src/screens/ManageProductsScreen.tsx @@ -24,7 +24,7 @@ import { BarcodeScannerView } from '../components/BarcodeScannerView'; import { ExternalProductInfo, fetchProductFromOFF } from '../modules/openFoodFacts'; import { DEFAULT_BULK_NAME, DEFAULT_UNIT_TYPE, Product } from '../models/Product'; -export const ManageProductsScreen = () => { +const ManageProductsScreen = () => { const db = useDatabase(); const products = useProducts(); const categories = useCategories(); @@ -505,3 +505,5 @@ export const ManageProductsScreen = () => { ); }; + +export default ManageProductsScreen \ No newline at end of file diff --git a/src/screens/PickListsScreen.test.tsx b/src/screens/PickListsScreen.test.tsx index 890b6e7..d1178ac 100644 --- a/src/screens/PickListsScreen.test.tsx +++ b/src/screens/PickListsScreen.test.tsx @@ -2,7 +2,7 @@ import { MemoryRouter } from 'react-router-dom'; import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { PickListsScreen } from './PickListsScreen'; +import PickListsScreen from './PickListsScreen'; let areasMock = [ { id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }, diff --git a/src/screens/PickListsScreen.tsx b/src/screens/PickListsScreen.tsx index 9972103..1598714 100644 --- a/src/screens/PickListsScreen.tsx +++ b/src/screens/PickListsScreen.tsx @@ -22,7 +22,7 @@ import { usePickLists, useAreas } from '../hooks/dataHooks'; import { useDatabase } from '../context/DBProvider'; import { PickList } from '../models/PickList'; -export const PickListsScreen = () => { +const PickListsScreen = () => { const lists = usePickLists(); const areas = useAreas(); const db = useDatabase(); @@ -176,3 +176,5 @@ export const PickListsScreen = () => { ); }; + +export default PickListsScreen \ No newline at end of file diff --git a/src/screens/StartPickListScreen.test.tsx b/src/screens/StartPickListScreen.test.tsx index 4b42f7d..eb027dd 100644 --- a/src/screens/StartPickListScreen.test.tsx +++ b/src/screens/StartPickListScreen.test.tsx @@ -2,7 +2,7 @@ import { MemoryRouter } from 'react-router-dom'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { StartPickListScreen } from './StartPickListScreen'; +import StartPickListScreen from './StartPickListScreen'; const navigateMock = vi.fn(); diff --git a/src/screens/StartPickListScreen.tsx b/src/screens/StartPickListScreen.tsx index 4f3fe32..861dade 100644 --- a/src/screens/StartPickListScreen.tsx +++ b/src/screens/StartPickListScreen.tsx @@ -15,7 +15,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useAreas, useCategories } from '../hooks/dataHooks'; import { useDatabase } from '../context/DBProvider'; -export const StartPickListScreen = () => { +const StartPickListScreen = () => { const areas = useAreas(); const categories = useCategories(); const db = useDatabase(); @@ -175,3 +175,5 @@ export const StartPickListScreen = () => { ); }; + +export default StartPickListScreen \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 836658d..fe27e20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "noEmit": true, "jsx": "react-jsx", "strict": true, - "types": ["vite/client", "vitest/globals"] + "types": ["vite/client", "vitest/globals", "vitest"] }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/vite.config.ts b/vite.config.ts index 98a0605..913cd8f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,57 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import { visualizer } from 'rollup-plugin-visualizer'; + +function packageNameFromId(id: string) { + // Extract the package name from a node_modules path, including scoped packages. + const match = id.match(/node_modules\/((?:@[^/]+\/)?[^/]+)/); + return match ? match[1] : null; +} export default defineConfig({ - plugins: [react()], + plugins: [react(), visualizer({ filename: 'dist/stats.html', open: true })], server: { port: 5173, }, build: { outDir: 'dist', + minify: 'esbuild', // fast minifier + rollupOptions: { + output: { + manualChunks(id: string) { + if (!id) return; + if (id.includes('node_modules')) { + // Always group react + router into one chunk + if (id.includes('react') || id.includes('react-dom') || id.includes('react-router-dom')) { + return 'react-vendor'; + } + + // MUI splits: specific first, generic later + if (id.includes('@mui/icons-material')) return 'mui-icons'; + if (id.includes('@mui/material/esm/styles') || id.includes('@mui/material/styles')) return 'mui-styles'; + // remaining @mui material code -> mui-core (covers components, utils, etc.) + if (id.includes('@mui/material') || id.includes('@mui/base')) return 'mui-core'; + // emotion / styled engine split + if (id.includes('@emotion') || id.includes('@mui/styled-engine')) return 'mui-styled'; + + + // Keep large libs in their own chunk to avoid a huge 'vendor' + if (id.includes('jszip')) return 'jszip'; + if (id.includes('papaparse')) return 'papaparse'; + if (id.includes('dexie')) return 'dexie'; + if (id.includes('@zxing')) return 'zxing'; + + // Default: create a per-package chunk name so vendor is split up. + const pkg = packageNameFromId(id); + if (pkg) { + // normalize '@' from scoped packages (can't use '/' in filenames) + return `npm.${pkg.replace('/', '__').replace('@', '')}`; + } + return 'vendor'; + } + }, + }, + }, }, test: { include: ['src/**/*.{test,spec}.{ts,tsx}'],