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}'],