Compare commits
No commits in common. "a3d9fcac53b13212ee06239a7c63bdddee33e82f" and "fd35640fd84749338a94652133f91a11a71d1386" have entirely different histories.
a3d9fcac53
...
fd35640fd8
22 changed files with 353 additions and 747 deletions
|
|
@ -1,236 +0,0 @@
|
|||
# Code Removal and Consolidation (LOC Reduction)
|
||||
|
||||
Owner: Engineering
|
||||
Document: code-removal-and-consolodation-spec.md
|
||||
Revision: 2.0
|
||||
Date: 2025-10-20
|
||||
Revision: 2.1
|
||||
Date: 2025-10-25
|
||||
|
||||
Purpose
|
||||
- Reduce lines of code (LOC) in this TS/Vue project (and any Python utilities) without changing behavior.
|
||||
- Remove dead/duplicate code, collapse pass-through layers, and slim public APIs.
|
||||
|
||||
Success metrics
|
||||
- [ ] LOC reduced by 10–30% (cloc baseline vs. final).
|
||||
- [ ] Test coverage ≥ baseline; typecheck/lint clean.
|
||||
- [ ] Bundle size stable or lower.
|
||||
- [ ] No regressions in main user flows.
|
||||
|
||||
Current status (2025-10-20)
|
||||
- Interim verification: typecheck and tests passing locally.
|
||||
- Consolidations completed in this pass (see checklist 4):
|
||||
- Shopping request types centralized in sdk.
|
||||
- UIShoppingListItem removed; grouping works on domain items with refs.
|
||||
- Meals composable removed; toMealInput moved to decoders; components now import sdk directly.
|
||||
- decodeLookup moved to decoders and reused.
|
||||
- Baseline metrics captured (cloc, coverage, build size):
|
||||
- cloc saved: cloc.baseline.txt
|
||||
- coverage saved (v8): coverage/ (see summary in terminal output)
|
||||
- build size: dist size ~1.6M
|
||||
- ts-prune saved: ts-prune.baseline.txt
|
||||
- depcheck saved: depcheck.baseline.json
|
||||
|
||||
Status update (2025-10-21)
|
||||
- Ran ts-prune and depcheck again to guide safe removals.
|
||||
- Notes:
|
||||
- ts-prune doesn’t analyze .vue SFC imports, so exports used only by SFCs (e.g., dateformats.ago) appear unused; avoid removing those.
|
||||
- Completed a safe API shrink: removed unused exports in domain (Maybe, NonNull) and decoders (decodeRecipes) without changing behavior.
|
||||
- Removed an unused ref in MyShoppingPage.vue.
|
||||
- depcheck flags core-js as unused; given Vue CLI/babel preset may rely on it for polyfills, defer removal for now.
|
||||
- depcheck shows some devDependencies as unused but they are required by the Vue CLI toolchain; defer removal.
|
||||
- Resolved typecheck errors after domain ref opt-in changes: updated ShoppingListItem.vue to remove person ref usage and guard optional ingredient access; TS + vue-tsc + tests pass.
|
||||
- Verified end-to-end: lint PASS, typecheck PASS, vue-tsc PASS, tests PASS (no regressions).
|
||||
- Reduced nullability in public SDK APIs where safe: getRecipe/getMeal/markMealConsumed/getCurrentShoppingList now return non-null and throw on errors; updated call sites accordingly.
|
||||
- Domain decoders now return non-null and throw on invalid input (decodeRecipe/decodeMeal/decodeMealRecipe/decodeIngredient/decodeShoppingListItem/decodeShoppingList); list decoders return arrays.
|
||||
- Lint/typecheck/vue-tsc/tests: all PASS after decoder contract tightening.
|
||||
- Aggressive dependency cleanup: removed node-fetch and undici (unused in Node 18+), removed core-js (build and tests still PASS under current browserslist targets), and dropped Vite types from tsconfig. All checks and build PASS post-removal.
|
||||
|
||||
Latest analysis artifacts (2025-10-21)
|
||||
- ts-prune (current): saved to ts-prune.current.txt; notable false-positives include dateformats.ago and units functions used inside SFCs.
|
||||
- depcheck (current): saved to depcheck.current.json; flags core-js and several dev deps as unused. These are likely required by Vue CLI/babel/coverage tooling; defer removal pending deeper validation.
|
||||
- Follow-up result: Verified removal of core-js, node-fetch, undici did not impact tests/typecheck/build. Retained Vue CLI/Babel/coverage deps.
|
||||
|
||||
Final verification snapshots (2025-10-25)
|
||||
- cloc.final.txt (exclude node_modules, dist, coverage): Files 100, Code 32,579; Vue SFC code 2,551; TS code 2,293.
|
||||
- Build size (Vue CLI): dist total ~1.6M; key bundles: chunk-vendors ~111KB (gz 40.9KB), app ~9.5KB (gz ~3.9KB).
|
||||
- Tests + coverage: 14 files, 27 tests, all PASS. Coverage stable on domain/sdk/units; SFCs not covered (unchanged behavior).
|
||||
- ts-prune refreshed (ts-prune.current.txt). Continue to treat SFC-only usages as false positives.
|
||||
|
||||
Scope
|
||||
- In: TS/Vue app code, tests, configs, scripts; Python utilities if present.
|
||||
- Out: New features, architectural rewrites.
|
||||
|
||||
Baseline and tooling (TS/Vue/Python)
|
||||
- Commands:
|
||||
- JS/TS LOC: npx cloc . | tee cloc.baseline.txt
|
||||
- Lint/typecheck: npm run lint && npm run typecheck
|
||||
- Coverage: npm test -- --coverage
|
||||
- Unused TS exports: npx ts-prune
|
||||
- Unused deps: npx depcheck
|
||||
- Search: rg (ripgrep)
|
||||
- Bundle size (Vite): npm run build && du -h dist
|
||||
- Python (if present): vulture . and coverage run -m pytest
|
||||
|
||||
Acceptance
|
||||
- [ ] Baselines saved (LOC, coverage, bundle size).
|
||||
- [ ] CI green on main before starting.
|
||||
|
||||
Ordered checklist (actionable)
|
||||
|
||||
1) Baseline snapshot
|
||||
- [x] Run cloc; store cloc.baseline.txt.
|
||||
- [x] Run tests with coverage; store coverage report. (added dev dep @vitest/coverage-v8)
|
||||
- [x] Run npm run build; record dist size.
|
||||
- [x] Run ts-prune and depcheck; save outputs.
|
||||
|
||||
2) Dead code inventory
|
||||
- [ ] ts-prune: list unused exports; verify via rg searches.
|
||||
- [x] Refreshed ts-prune; review and mark SFC-used exports to avoid accidental removals.
|
||||
- [ ] Find unreferenced files:
|
||||
rg -l "export default|export const|export function" |
|
||||
while read f; do rg -q "(from|import).*$f" -g "!$f" . || echo "$f"; done
|
||||
- [ ] depcheck: identify unused deps/scripts.
|
||||
- [ ] Python (optional): vulture . for unused code.
|
||||
|
||||
3) Delete proven dead code
|
||||
- [ ] Remove files/symbols with zero references.
|
||||
- [ ] Delete tests/mocks for removed code.
|
||||
- [ ] Fix imports; run lint, typecheck, tests, build.
|
||||
|
||||
Acceptance
|
||||
- [ ] LOC decreased; CI green.
|
||||
|
||||
4) Consolidate duplicated types and logic (repo targets)
|
||||
- Shopping requests
|
||||
- [x] Export PurchaseExisting/PurchaseRefs (or a single request builder) from src/api/sdk.ts.
|
||||
- [x] Reuse in src/composables/useShopping.ts; delete local duplicates.
|
||||
- [x] Added PurchaseRequest union in SDK to simplify signatures and centralize request typing.
|
||||
- UIShoppingListItem mapping
|
||||
- [x] Make itemsToGroups/uniqueMeals accept ShoppingListItemWithRefs.
|
||||
- [x] Remove UIShoppingListItem and mapItemToUI; update callers.
|
||||
- Meals composable
|
||||
- [x] Delete src/composables/useMeals.ts OR keep only toMealInput.
|
||||
- [x] Move toMealInput to src/domain/decoders.ts if file removed.
|
||||
- [x] Update components to import sdk directly for API calls.
|
||||
- Common decoder
|
||||
- [x] Move decodeLookup<T>() to src/domain/decoders.ts; import in sdk.ts.
|
||||
- Component-local types
|
||||
- [ ] Replace ad-hoc component types (e.g., MealCard.vue) with domain types.
|
||||
- [x] Align ShoppingListItem.vue template/computed logic with domain changes (optional ingredient refs; removed person ref).
|
||||
- Unused refs
|
||||
- [x] Remove unused reactive refs/vars (e.g., stray person ref in shopping pages if unused).
|
||||
|
||||
Acceptance
|
||||
- [ ] Single source for purchase request types/builders.
|
||||
- [ ] One fewer UI-only item shape in shopping flow.
|
||||
- [ ] Meals API calls go through sdk; no pass-through wrappers.
|
||||
|
||||
5) Shrink public API surface
|
||||
- [x] Export only used symbols from domain and sdk modules.
|
||||
- [ ] Remove unused overloads/params; prefer narrower interfaces.
|
||||
- [ ] Update re-exports; fix imports accordingly.
|
||||
- [x] Reduce unnecessary null/undefined on public SDK returns (getRecipe/getMeal/markMealConsumed/getCurrentShoppingList).
|
||||
- [x] Narrowed visibility of internal helpers/types (listPersons made internal; PurchaseExisting/PurchaseRefs internal, union type exported).
|
||||
- [x] Hide internal-only helpers/types: decoders.decodeMealRecipe/decodeShoppingListItem made module-private; units.UnitKey/equivalentUnits/Quantity/Total are no longer exported.
|
||||
|
||||
Acceptance
|
||||
- [ ] ts-prune emits fewer/no unused export warnings.
|
||||
|
||||
6) Simplify control flow and inline pass-throughs
|
||||
- [x] Inline trivial wrappers where applicable (meals composable removed; components call sdk directly).
|
||||
- [ ] Prefer guard clauses over nested branches in remaining hotspots (audit pending).
|
||||
- [ ] Remove speculative extension points not used.
|
||||
- [ ] Replace small class-style modules with plain functions where LOC decreases.
|
||||
|
||||
Acceptance
|
||||
- [ ] Complexity lower in touched files; tests unchanged.
|
||||
|
||||
7) Test suite consolidation
|
||||
- [x] Remove duplicate tests (e.g., .js vs .ts duplicates), fixtures for deleted code.
|
||||
- Removed tests/useAlert.test.js (duplicate of tests/useAlert.test.ts).
|
||||
- [ ] Ensure assertions are meaningful; avoid testing implementation details.
|
||||
- [ ] Keep or improve branch coverage on critical paths.
|
||||
|
||||
Acceptance
|
||||
- [ ] Coverage ≥ baseline; runtime stable or faster.
|
||||
|
||||
8) Dependency cleanup
|
||||
- [ ] Remove unused npm deps (depcheck) and scripts.
|
||||
- [x] Refreshed depcheck results saved; plan conservative removals only after confirming toolchain needs.
|
||||
- [ ] Prefer stdlib/small helpers over heavy libs where equal.
|
||||
- [ ] npm prune && clean install; verify build.
|
||||
|
||||
Acceptance
|
||||
- [ ] Smaller dependency graph; no new vulns; CI green.
|
||||
|
||||
9) Docs and examples
|
||||
- [ ] Update README/ARCHITECTURE and code comments to reflect removals.
|
||||
- [ ] Document canonical modules replacing duplicates.
|
||||
- [ ] Add migration notes in CHANGELOG if applicable.
|
||||
|
||||
Acceptance
|
||||
- [ ] Docs consistent; onboarding simpler.
|
||||
|
||||
10) Final verification
|
||||
- [x] Re-run cloc; saved cloc.final.txt and noted summary.
|
||||
- [x] Re-run tests with coverage; all PASS; coverage consistent with previous runs.
|
||||
- [x] Re-run build; build size recorded in build-size.final.txt (dist ~1.6M).
|
||||
- [ ] Sanity test main user flows locally.
|
||||
|
||||
Acceptance
|
||||
- [ ] All metrics at or better than baseline; no regressions.
|
||||
|
||||
Quick command reference
|
||||
- LOC: npx cloc .
|
||||
- Typecheck: npm run typecheck
|
||||
- Lint: npm run lint
|
||||
- Tests + coverage: npm test -- --coverage
|
||||
- Unused TS exports: npx ts-prune
|
||||
- Unused deps: npx depcheck
|
||||
- Search: rg -n "<pattern>"
|
||||
- Build + size: npm run build && du -h dist
|
||||
|
||||
Exit criteria
|
||||
- [ ] Target LOC reduction met with stable quality metrics.
|
||||
- [ ] Duplicates removed; canonical modules in place.
|
||||
- [ ] No pending deprecations awaiting removal.
|
||||
|
||||
Next actions (clear, actionable)
|
||||
1) Capture baselines
|
||||
- [x] Run cloc and save cloc.baseline.txt
|
||||
- [x] Run tests with coverage and save report summary
|
||||
- [x] Build app and note dist size
|
||||
- [x] Run ts-prune and depcheck; save outputs
|
||||
2) Component type audit
|
||||
- [ ] Replace any ad-hoc component types with domain types (scan components/*)
|
||||
3) Unused refs/vars cleanup
|
||||
- [x] Scan shopping/meals components for unused refs/variables; remove
|
||||
4) Shrink public API surface
|
||||
- [x] Use ts-prune results to remove unused exports in domain and sdk
|
||||
- [ ] Second pass: validate remaining ts-prune hints against .vue usage; prune safely
|
||||
- [ ] Consider reducing parseRecipe nullability (throw on parse failures) if UI updated accordingly
|
||||
5) Test suite consolidation
|
||||
- [x] De-duplicate tests with .js and .ts counterparts (e.g., prefer TypeScript)
|
||||
6) Dependency cleanup
|
||||
- [x] Remove unused npm deps (depcheck) and scripts (removed core-js, node-fetch, undici, @babel/eslint-parser). Verified tests/typecheck/build PASS.
|
||||
7) Final verification
|
||||
- [ ] Re-run cloc/tests/build; record deltas vs. baseline; sanity test user flows
|
||||
- [x] Interim verification after dep cleanup: tests/typecheck/vue-tsc/lint/build PASS
|
||||
|
||||
Additional targeted opportunities (2025-10-25)
|
||||
- Simplify env typings: removed Vite-specific triple-slash reference and duplicate ImportMeta declarations; kept a minimal optional ImportMetaEnv in `src/env.d.ts`. All checks PASS.
|
||||
- Component type audit: migrated `MealCard.vue` to domain `Meal` type with null guards. Remaining spots use purposeful `Pick<>` types; can leave as-is unless standardizing is preferred.
|
||||
- ts-prune: refreshed; remaining flags are either used by SFC templates or part of public SDK surface. No further safe removals identified without deeper UI refactors.
|
||||
|
||||
Cleanup (completed)
|
||||
- Removed intermediate artifacts committed for this consolidation pass:
|
||||
- cloc.baseline.txt, cloc.final.txt
|
||||
- build-size.final.txt
|
||||
- ts-prune.baseline.txt, ts-prune.current.txt
|
||||
- depcheck.baseline.json, depcheck.current.json
|
||||
- coverage/ directory
|
||||
- Removed coverage script and plugin from package.json:
|
||||
- scripts: deleted `test:coverage`
|
||||
- devDependencies: removed `@vitest/coverage-v8`
|
||||
- All quality gates remain PASS after cleanup.
|
||||
404
package-lock.json
generated
404
package-lock.json
generated
|
|
@ -8,14 +8,16 @@
|
|||
"name": "doof",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@types/node": "^20.19.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||
|
|
@ -25,10 +27,12 @@
|
|||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.3.0",
|
||||
"msw": "^2.5.2",
|
||||
"node-fetch": "^2.6.9",
|
||||
"openapi-fetch": "^0.9.5",
|
||||
"openapi-typescript": "^7.4.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "~5.5.4",
|
||||
"undici": "^6.19.8",
|
||||
"vitest": "^1.6.0",
|
||||
"vue-tsc": "^2.0.29"
|
||||
}
|
||||
|
|
@ -123,6 +127,24 @@
|
|||
"url": "https://opencollective.com/babel"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/eslint-parser": {
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz",
|
||||
"integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
|
||||
"eslint-visitor-keys": "^2.1.0",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || >=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.11.0",
|
||||
"eslint": "^7.5.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
|
||||
|
|
@ -1799,13 +1821,6 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
|
||||
|
|
@ -2516,16 +2531,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/schema": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/schemas": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||
|
|
@ -2588,11 +2593,10 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
|
|
@ -2622,6 +2626,15 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eslint-scope": "5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-ipc/js-queue": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz",
|
||||
|
|
@ -3710,34 +3723,6 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz",
|
||||
"integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
"@bcoe/v8-coverage": "^0.2.3",
|
||||
"debug": "^4.3.4",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.4",
|
||||
"istanbul-reports": "^3.1.6",
|
||||
"magic-string": "^0.30.5",
|
||||
"magicast": "^0.3.3",
|
||||
"picocolors": "^1.0.0",
|
||||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^2.0.0",
|
||||
"test-exclude": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
||||
|
|
@ -6264,7 +6249,6 @@
|
|||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz",
|
||||
"integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
|
@ -7366,6 +7350,15 @@
|
|||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-webpack-plugin": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz",
|
||||
|
|
@ -9301,112 +9294,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-source-maps": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.23",
|
||||
"debug": "^4.1.1",
|
||||
"istanbul-lib-coverage": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
|
|
@ -10479,18 +10366,6 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
|
|
@ -13641,21 +13516,6 @@
|
|||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^7.1.4",
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
|
@ -14052,6 +13912,16 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
|
|
@ -15645,6 +15515,17 @@
|
|||
"semver": "^6.3.1"
|
||||
}
|
||||
},
|
||||
"@babel/eslint-parser": {
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz",
|
||||
"integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
|
||||
"eslint-visitor-keys": "^2.1.0",
|
||||
"semver": "^6.3.1"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
|
||||
|
|
@ -16786,12 +16667,6 @@
|
|||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"@discoveryjs/json-ext": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
|
||||
|
|
@ -17148,12 +17023,6 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@istanbuljs/schema": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
||||
"dev": true
|
||||
},
|
||||
"@jest/schemas": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||
|
|
@ -17202,9 +17071,9 @@
|
|||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
||||
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
|
|
@ -17231,6 +17100,15 @@
|
|||
"strict-event-emitter": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-scope": "5.1.1"
|
||||
}
|
||||
},
|
||||
"@node-ipc/js-queue": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz",
|
||||
|
|
@ -17990,27 +17868,6 @@
|
|||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"dev": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz",
|
||||
"integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
"@bcoe/v8-coverage": "^0.2.3",
|
||||
"debug": "^4.3.4",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.4",
|
||||
"istanbul-reports": "^3.1.6",
|
||||
"magic-string": "^0.30.5",
|
||||
"magicast": "^0.3.3",
|
||||
"picocolors": "^1.0.0",
|
||||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^2.0.0",
|
||||
"test-exclude": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@vitest/expect": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
||||
|
|
@ -19898,8 +19755,7 @@
|
|||
"core-js": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.0.tgz",
|
||||
"integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.35.0",
|
||||
|
|
@ -20877,6 +20733,12 @@
|
|||
"estraverse": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-webpack-plugin": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz",
|
||||
|
|
@ -22045,76 +21907,6 @@
|
|||
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
|
||||
"dev": true
|
||||
},
|
||||
"istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true
|
||||
},
|
||||
"istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "^7.5.3"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"istanbul-lib-source-maps": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/trace-mapping": "^0.3.23",
|
||||
"debug": "^4.1.1",
|
||||
"istanbul-lib-coverage": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
|
|
@ -22868,17 +22660,6 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
|
|
@ -25158,17 +24939,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^7.1.4",
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
|
@ -25442,6 +25212,12 @@
|
|||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||
"dev": true
|
||||
},
|
||||
"undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"dev": true
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
|
|
|
|||
37
package.json
37
package.json
|
|
@ -10,35 +10,39 @@
|
|||
"prepare": "husky install",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:vue": "vue-tsc --noEmit",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:vue": "vue-tsc --noEmit",
|
||||
"codegen:api": "openapi-typescript ../munch-ease-backend/openapi.json -o src/api/types.ts",
|
||||
"codegen": "npm run codegen:api",
|
||||
"codegen:check": "node scripts/codegen-check.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@types/node": "^20.19.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.3.0",
|
||||
"msw": "^2.5.2",
|
||||
"node-fetch": "^2.6.9",
|
||||
"openapi-fetch": "^0.9.5",
|
||||
"openapi-typescript": "^7.4.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "~5.5.4",
|
||||
"typescript": "~5.5.4",
|
||||
"undici": "^6.19.8",
|
||||
"vitest": "^1.6.0",
|
||||
"vue-tsc": "^2.0.29"
|
||||
},
|
||||
|
|
@ -76,19 +80,10 @@
|
|||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.vue",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"excludedFiles": [
|
||||
"src/api/types.ts",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts"],
|
||||
"excludedFiles": ["src/api/types.ts", "src/**/*.d.ts"],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"./tsconfig.eslint.json"
|
||||
],
|
||||
"project": ["./tsconfig.eslint.json"],
|
||||
"tsconfigRootDir": "."
|
||||
},
|
||||
"rules": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { api } from '@/api/client'
|
||||
import type { components } from '@/api/types'
|
||||
import { toDate, decodeMeal, decodeRecipe, decodeIngredients, decodeShoppingList, decodeShoppingListItems, decodeIngredient, decodeLookup } from '@/domain/decoders'
|
||||
import { toDate, decodeMeal, decodeRecipe, decodeIngredients, decodeShoppingList, decodeShoppingListItems, decodeIngredient } from '@/domain/decoders'
|
||||
import type {
|
||||
Recipe,
|
||||
Meal,
|
||||
|
|
@ -19,7 +19,22 @@ function httpError(response: Response, error: unknown): Error {
|
|||
return new Error(`${response.status} ${response.statusText || 'HTTP error'}`)
|
||||
}
|
||||
|
||||
// decodeLookup moved to domain/decoders to be reused across SDK and other modules
|
||||
// Small helper to decode optional lookup maps without repeating loops everywhere
|
||||
function decodeLookup<TIn, TOut>(
|
||||
raw: Record<string, TIn> | null | undefined,
|
||||
decode: (v: TIn) => TOut | null
|
||||
): Record<string, TOut> | undefined {
|
||||
if (!raw) return undefined
|
||||
const out: Record<string, TOut> = {}
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(raw, key)) continue
|
||||
const maybe = raw[key]
|
||||
if (maybe === undefined) continue
|
||||
const decoded = decode(maybe)
|
||||
if (decoded) out[String(key)] = decoded
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Shopping list mapped view types now come from domain/types
|
||||
|
||||
|
|
@ -35,21 +50,17 @@ function attachItemRefs(
|
|||
const listId = item.listId ?? undefined
|
||||
const created = item.createdDate
|
||||
|
||||
if (ingredientId !== undefined && lookups.ingredientsLookup) {
|
||||
const v = lookups.ingredientsLookup[String(ingredientId)]
|
||||
if (v !== undefined) item.ingredient = v
|
||||
if (ingredientId !== undefined && lookups.ingredientsLookup && lookups.ingredientsLookup[String(ingredientId)] !== undefined) {
|
||||
item.ingredient = lookups.ingredientsLookup[String(ingredientId)]
|
||||
}
|
||||
if (mealId !== undefined && lookups.mealsLookup) {
|
||||
const v = lookups.mealsLookup[String(mealId)]
|
||||
if (v !== undefined) item.meal = v
|
||||
if (mealId !== undefined && lookups.mealsLookup && lookups.mealsLookup[String(mealId)] !== undefined) {
|
||||
item.meal = lookups.mealsLookup[String(mealId)]
|
||||
}
|
||||
if (recipeId !== undefined && lookups.recipesLookup) {
|
||||
const v = lookups.recipesLookup[String(recipeId)]
|
||||
if (v !== undefined) item.recipe = v
|
||||
if (recipeId !== undefined && lookups.recipesLookup && lookups.recipesLookup[String(recipeId)] !== undefined) {
|
||||
item.recipe = lookups.recipesLookup[String(recipeId)]
|
||||
}
|
||||
if (listId !== undefined && lookups.shoppingListLookup) {
|
||||
const v = lookups.shoppingListLookup[String(listId)]
|
||||
if (v !== undefined) item.list = v
|
||||
if (listId !== undefined && lookups.shoppingListLookup && lookups.shoppingListLookup[String(listId)] !== undefined) {
|
||||
item.list = lookups.shoppingListLookup[String(listId)]
|
||||
}
|
||||
if (created !== undefined) item.createdDate = toDate(created)
|
||||
}
|
||||
|
|
@ -77,7 +88,7 @@ export function mapPurchasedShoppingList(dto: components['schemas']['PurchasedSh
|
|||
...(ingredientsLookup && { ingredientsLookup }),
|
||||
...(mealsLookup && { mealsLookup }),
|
||||
...(recipesLookup && { recipesLookup }),
|
||||
...(list && { shoppingListLookup: { [String(list.id)]: list } }),
|
||||
shoppingListLookup: { [String(list.id)]: list },
|
||||
}
|
||||
attachItemRefs(list.items, lookups)
|
||||
}
|
||||
|
|
@ -88,8 +99,6 @@ export function mapPurchasedShoppingList(dto: components['schemas']['PurchasedSh
|
|||
...(mealsLookup && { mealsLookup }),
|
||||
...(recipesLookup && { recipesLookup }),
|
||||
...(list && { list }),
|
||||
// include shoppingListLookup when list exists for consistency with lookups type
|
||||
...(list && { shoppingListLookup: { [String(list.id)]: list } }),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,15 +155,14 @@ export async function listRecipes(params?: { q?: string | null; cursor?: string
|
|||
}
|
||||
const { data, error, response } = await api.GET('/api/v1/recipes', { params: { query } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
return fromOpenApiPage(data ?? null, (r) => decodeRecipe(r))
|
||||
const mapped = fromOpenApiPage(data ?? null, (r) => decodeRecipe(r))
|
||||
return { ...mapped, items: mapped.items.filter((r): r is Recipe => !!r) }
|
||||
}
|
||||
|
||||
export async function getRecipe(id: number | string): Promise<Recipe> {
|
||||
export async function getRecipe(id: number | string): Promise<Recipe | null> {
|
||||
const { data, error, response } = await api.GET('/api/v1/recipes/{recipe_id}', { params: { path: { recipe_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = decodeRecipe(data)
|
||||
if (!mapped) throw new Error('Recipe not found')
|
||||
return mapped
|
||||
return decodeRecipe(data)
|
||||
}
|
||||
|
||||
export async function saveRecipe(recipe: components['schemas']['Recipe-Input']): Promise<Recipe | null> {
|
||||
|
|
@ -193,7 +201,7 @@ export async function parseProduct(
|
|||
}
|
||||
|
||||
// Persons
|
||||
async function listPersons(params?: { q?: string | null; cursor?: string | null; limit?: number }): Promise<Page<components['schemas']['Person']>> {
|
||||
export async function listPersons(params?: { q?: string | null; cursor?: string | null; limit?: number }): Promise<Page<components['schemas']['Person']>> {
|
||||
const query: Record<string, unknown> = {}
|
||||
if (params) {
|
||||
if (params.q !== undefined) query.q = params.q
|
||||
|
|
@ -227,12 +235,10 @@ export async function getUpcomingMeals(from: Date, to: Date): Promise<Meal[]> {
|
|||
.sort((a, b) => ((a.suggestedDate?.getTime() ?? 0) - (b.suggestedDate?.getTime() ?? 0)))
|
||||
}
|
||||
|
||||
export async function getMeal(id: number | string): Promise<Meal> {
|
||||
export async function getMeal(id: number | string): Promise<Meal | null> {
|
||||
const { data, error, response } = await api.GET('/api/v1/meals/{meal_id}', { params: { path: { meal_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = decodeMeal(data)
|
||||
if (!mapped) throw new Error('Meal not found')
|
||||
return mapped
|
||||
return decodeMeal(data)
|
||||
}
|
||||
|
||||
export async function saveMeal(meal: components['schemas']['Meal-Input']): Promise<Meal | null> {
|
||||
|
|
@ -251,14 +257,12 @@ export async function saveMeal(meal: components['schemas']['Meal-Input']): Promi
|
|||
}
|
||||
}
|
||||
|
||||
export async function markMealConsumed(mealId: number | string): Promise<Meal> {
|
||||
export async function markMealConsumed(mealId: number | string): Promise<Meal | null> {
|
||||
const { data, error, response } = await api.POST('/api/v1/meals/{meal_id}/consumed', {
|
||||
params: { path: { meal_id: Number(mealId) }, cookie: { user_id: 0 } },
|
||||
})
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = decodeMeal(data)
|
||||
if (!mapped) throw new Error('Meal not found')
|
||||
return mapped
|
||||
return decodeMeal(data)
|
||||
}
|
||||
|
||||
export async function deleteMeal(mealId: number | string): Promise<void> {
|
||||
|
|
@ -284,27 +288,24 @@ export async function saveMyShoppingList(items: components['schemas']['Ingredien
|
|||
return decodeIngredients(data ?? [])
|
||||
}
|
||||
|
||||
export async function getShoppingList(id: number | string): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
||||
export async function getShoppingList(id: number | string) {
|
||||
const { data, error, response } = await api.GET('/api/v1/shopping/{list_id}', { params: { path: { list_id: Number(id) } } })
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = mapPurchasedShoppingList(data)
|
||||
return mapped?.list ?? null
|
||||
}
|
||||
|
||||
export async function getCurrentShoppingList(): Promise<CurrentShoppingListDTO> {
|
||||
export async function getCurrentShoppingList() {
|
||||
const { data, error, response } = await api.GET('/api/v1/shopping/current')
|
||||
if (!response.ok) throw httpError(response, error)
|
||||
const mapped = mapCurrentShoppingList(data)
|
||||
if (!mapped) throw new Error('Failed to map current shopping list')
|
||||
return mapped
|
||||
return mapCurrentShoppingList(data)
|
||||
}
|
||||
|
||||
type PurchaseExisting = { type: 'existing'; id: number; personId: number; ingredientId?: number | null }
|
||||
type PurchaseRefs = { type: 'refs'; personId: number; ingredientId?: number | null; recipeId?: number | null; mealId?: number | null }
|
||||
export type PurchaseRequest = PurchaseExisting | PurchaseRefs
|
||||
|
||||
export async function purchaseShoppingList(
|
||||
completedRequests: PurchaseRequest[]
|
||||
completedRequests: Array<PurchaseExisting | PurchaseRefs>
|
||||
): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
||||
if (!Array.isArray(completedRequests) || completedRequests.length === 0) return null
|
||||
// Map incoming requests: if id provided and >= 0, use it; otherwise send identifiers for ingredient/recipe/meal
|
||||
|
|
|
|||
|
|
@ -506,10 +506,9 @@ export interface components {
|
|||
prevCursor?: string | null;
|
||||
/**
|
||||
* Total
|
||||
* @description Total count
|
||||
* @default 0
|
||||
* @description Optional total count
|
||||
*/
|
||||
total: number;
|
||||
total?: number | null;
|
||||
};
|
||||
/** Page[Recipe] */
|
||||
Page_Recipe_: {
|
||||
|
|
@ -521,10 +520,9 @@ export interface components {
|
|||
prevCursor?: string | null;
|
||||
/**
|
||||
* Total
|
||||
* @description Total count
|
||||
* @default 0
|
||||
* @description Optional total count
|
||||
*/
|
||||
total: number;
|
||||
total?: number | null;
|
||||
};
|
||||
/** Person */
|
||||
Person: {
|
||||
|
|
@ -577,6 +575,8 @@ export interface components {
|
|||
imgSmall: string;
|
||||
/** Imglarge */
|
||||
imgLarge: string;
|
||||
/** Rawdata */
|
||||
rawData?: Record<string, never> | null;
|
||||
};
|
||||
/** ProductUrl */
|
||||
ProductUrl: {
|
||||
|
|
@ -626,7 +626,7 @@ export interface components {
|
|||
*/
|
||||
dateCreated?: string;
|
||||
/** Createdbyid */
|
||||
createdById: number;
|
||||
createdById: number | null;
|
||||
createdBy?: components["schemas"]["Person"] | null;
|
||||
/** Datehidden */
|
||||
dateHidden?: string | null;
|
||||
|
|
@ -659,7 +659,7 @@ export interface components {
|
|||
*/
|
||||
dateCreated?: string;
|
||||
/** Createdbyid */
|
||||
createdById: number;
|
||||
createdById: number | null;
|
||||
createdBy?: components["schemas"]["Person"] | null;
|
||||
/** Datehidden */
|
||||
dateHidden?: string | null;
|
||||
|
|
|
|||
|
|
@ -118,8 +118,8 @@
|
|||
<script setup lang="ts">
|
||||
import { reactive, onBeforeMount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getMeal, saveMeal, getRecipe } from '@/api/sdk'
|
||||
import { toMealInput } from '@/domain/decoders'
|
||||
import { getMeal, saveMeal, toMealInput } from '@/composables/useMeals'
|
||||
import { getRecipe } from '@/api/sdk'
|
||||
import { currentUser } from '@/api/auth'
|
||||
import { useAlert } from '@/composables/useAlert'
|
||||
import { parseRouteId } from '@/router/helpers'
|
||||
|
|
@ -162,11 +162,13 @@ const meal = reactive<Meal>({
|
|||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const id = parseRouteId(route.params.id)
|
||||
if (id !== null) {
|
||||
const loaded = await getMeal(id)
|
||||
Object.assign(meal, loaded)
|
||||
} else {
|
||||
const id = parseRouteId(route.params.id)
|
||||
if (id !== null) {
|
||||
const loaded = await getMeal(id)
|
||||
if (loaded) {
|
||||
Object.assign(meal, loaded)
|
||||
}
|
||||
} else {
|
||||
const self = await currentUser()
|
||||
if (self) {
|
||||
meal.chefs = [self]
|
||||
|
|
@ -212,7 +214,8 @@ function addPerson(list: PeopleKey, person: Person) {
|
|||
|
||||
async function selectRecipe(recipe: { id: number | string }) {
|
||||
// Refetch to get additional details
|
||||
const r = await getRecipe(recipe.id)
|
||||
const r = await getRecipe(recipe.id)
|
||||
if (!r) return
|
||||
|
||||
if (r.createdBy) {
|
||||
addPersonIfNotExists(meal.chefs, r.createdBy)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,18 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ago } from '@/dateformats'
|
||||
import type { Meal } from '@/domain/types'
|
||||
|
||||
type Person = { id: number; name: string }
|
||||
type MealRecipe = { recipe: { name: string } }
|
||||
type Ingredient = { name: string }
|
||||
type Meal = {
|
||||
suggestedDate: Date | null
|
||||
chefs: Person[]
|
||||
consumers: Person[]
|
||||
recipes?: MealRecipe[]
|
||||
extraIngredients?: Ingredient[]
|
||||
purchaseDate?: Date | null
|
||||
}
|
||||
|
||||
const props = defineProps<{ meal: Meal }>()
|
||||
|
||||
|
|
@ -75,10 +86,7 @@ const dayOfWeek = computed(() =>
|
|||
const mealTitle = computed(() => {
|
||||
const recipes = props.meal.recipes ?? []
|
||||
const extras = props.meal.extraIngredients ?? []
|
||||
const recipeNames = recipes
|
||||
.map((mr) => mr.recipe?.name)
|
||||
.filter((n): n is string => typeof n === 'string' && n.length > 0)
|
||||
const recipesText = englishList(recipeNames)
|
||||
const recipesText = englishList(recipes.map((mr) => mr.recipe.name))
|
||||
const ingredientsText = englishList(extras.map((i) => i.name))
|
||||
|
||||
if (recipesText && ingredientsText) return `${recipesText} with ${ingredientsText}`
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
import { ref, onBeforeMount } from 'vue'
|
||||
import ActionItem from '@/components/ActionItem.vue'
|
||||
import MealCard from '@/components/meals/MealCard.vue'
|
||||
import { getUpcomingMeals, markMealConsumed, deleteMeal } from '@/api/sdk'
|
||||
import { getUpcomingMeals, markMealConsumed, deleteMeal } from '@/composables/useMeals'
|
||||
const chevronDown = new URL('@/assets/chevron-down.svg', import.meta.url).toString()
|
||||
const chevronUp = new URL('@/assets/chevron-up.svg', import.meta.url).toString()
|
||||
const planMeal = new URL('@/assets/plan-meal.svg', import.meta.url).toString()
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ async function refreshRecipe() {
|
|||
if (id !== null && id >= 0) {
|
||||
const r = await getRecipe(id)
|
||||
recipe.value = r
|
||||
link.value = r.link ?? ''
|
||||
link.value = r?.link ?? ''
|
||||
return
|
||||
} else if (link.value) {
|
||||
const r = await parseRecipe(link.value)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ import { ref, computed, onBeforeMount } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { useAlert } from '@/composables/useAlert'
|
||||
import { useShopping } from '@/composables/useShopping'
|
||||
import { getUpcomingMeals } from '@/api/sdk'
|
||||
import { useMeals } from '@/composables/useMeals'
|
||||
import { type Group } from '@/composables/useShopping'
|
||||
type UIMeal = import('@/domain/types').Meal
|
||||
import MealSelectionList from './MealSelectionList.vue'
|
||||
|
|
@ -117,6 +117,7 @@ const closeIcon = new URL('@/assets/close.svg', import.meta.url).toString()
|
|||
const router = useRouter()
|
||||
const { show: showAlert } = useAlert()
|
||||
const { getCurrentShoppingList, requestMeal, unrequestMeal, purchaseFromGroups, groupsFrom, mealsFrom } = useShopping()
|
||||
const { getUpcomingMeals } = useMeals()
|
||||
|
||||
const from = new Date()
|
||||
from.setTime(0)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ const router = useRouter()
|
|||
const { loadUser } = useAuth()
|
||||
const { getMyShoppingList, saveMyShoppingList } = useShopping()
|
||||
|
||||
const person = ref<{ id?: number; name?: string } | null>(null)
|
||||
const ingredients = ref<Ingredient[]>([])
|
||||
|
||||
async function updateShoppingList(save = false) {
|
||||
|
|
@ -81,6 +82,7 @@ async function onEditing(isStartingEdit: boolean) {
|
|||
onBeforeMount(async () => {
|
||||
const u = await loadUser()
|
||||
if (!u) return router.push({ name: 'login' })
|
||||
person.value = u
|
||||
await updateShoppingList()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -37,11 +37,11 @@
|
|||
:key="source.id"
|
||||
>
|
||||
<span v-if="index">, and </span>
|
||||
<!-- Generic ingredient-only display when no recipe/meal context; person ref removed -->
|
||||
<span v-if="!source.recipe && !source.meal && source.ingredient">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }}
|
||||
<span v-if="source.person">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for
|
||||
{{ source.person.name }}
|
||||
</span>
|
||||
<span v-else-if="source.recipe && source.ingredient">
|
||||
<span v-else-if="source.recipe">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in
|
||||
<router-link :to="`/recipes/${source.recipe.id}/`">{{
|
||||
source.recipe.name
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
: ''
|
||||
}}</router-link>
|
||||
</span>
|
||||
<span v-else-if="source.meal && source.ingredient">
|
||||
<span v-else-if="source.meal">
|
||||
{{ source.ingredient.line }} for
|
||||
<router-link :to="`/meals/${source.meal?.id}/`">{{
|
||||
source.meal?.suggestedDate
|
||||
|
|
@ -78,11 +78,11 @@
|
|||
:key="source.id"
|
||||
>
|
||||
<span v-if="index">, and </span>
|
||||
<!-- Generic ingredient-only display when no recipe/meal context; person ref removed -->
|
||||
<span v-if="!source.recipe && !source.meal && source.ingredient">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }}
|
||||
<span v-if="source.person">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for
|
||||
{{ source.person.name }}
|
||||
</span>
|
||||
<span v-else-if="source.recipe && source.ingredient">
|
||||
<span v-else-if="source.recipe">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} in
|
||||
<router-link :to="`/recipes/${source.recipe.id}/`">{{
|
||||
source.recipe.name
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
: ''
|
||||
}}</router-link>
|
||||
</span>
|
||||
<span v-else-if="source.meal && source.ingredient">
|
||||
<span v-else-if="source.meal">
|
||||
{{ source.ingredient.line }} for
|
||||
<router-link :to="`/meals/${source.meal.id}/`">{{
|
||||
source.meal.suggestedDate
|
||||
|
|
@ -134,13 +134,10 @@ const imageSrc = computed(() => {
|
|||
|
||||
const remainingRequiredTotals = computed(() =>
|
||||
calculateTotals(
|
||||
props.shoppingListItemGroup.shoppingListItems
|
||||
.filter((item) => !!item.ingredient)
|
||||
.map((item) => ({
|
||||
// item.ingredient is defined due to filter above
|
||||
quantity: item.ingredient!.quantity,
|
||||
unit: String(item.ingredient!.unit || 'items'),
|
||||
}))
|
||||
props.shoppingListItemGroup.shoppingListItems.map((item) => ({
|
||||
quantity: item.ingredient.quantity,
|
||||
unit: String(item.ingredient.unit || 'items'),
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
46
src/composables/useMeals.ts
Normal file
46
src/composables/useMeals.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as sdk from '@/api/sdk'
|
||||
import type { MealInput, Meal } from '@/domain/types'
|
||||
|
||||
export async function getUpcomingMeals(from: Date, to: Date) {
|
||||
return sdk.getUpcomingMeals(from, to)
|
||||
}
|
||||
|
||||
export async function getMeal(id: number | string) {
|
||||
return sdk.getMeal(id)
|
||||
}
|
||||
|
||||
export async function saveMeal(meal: MealInput) {
|
||||
return sdk.saveMeal(meal)
|
||||
}
|
||||
|
||||
export async function markMealConsumed(mealId: number | string) {
|
||||
return sdk.markMealConsumed(mealId)
|
||||
}
|
||||
|
||||
export async function deleteMeal(mealId: number | string) {
|
||||
return sdk.deleteMeal(mealId)
|
||||
}
|
||||
|
||||
// Helper to convert domain Meal to MealInput, keeping Date→string conversion in boundary
|
||||
export function toMealInput(meal: Meal): MealInput {
|
||||
return {
|
||||
id: meal.id,
|
||||
suggestedDate: meal.suggestedDate ? meal.suggestedDate.toISOString() : new Date().toISOString(),
|
||||
consumedDate: meal.consumedDate ? meal.consumedDate.toISOString() : null,
|
||||
purchaseDate: meal.purchaseDate ? meal.purchaseDate.toISOString() : null,
|
||||
chefs: meal.chefs,
|
||||
cleanup: meal.cleanup,
|
||||
consumers: meal.consumers,
|
||||
recipes: meal.recipes.map((r) => ({
|
||||
mealId: r.mealId,
|
||||
recipeId: r.recipeId,
|
||||
servings: r.servings,
|
||||
recipe: null
|
||||
})),
|
||||
extraIngredients: meal.extraIngredients,
|
||||
}
|
||||
}
|
||||
|
||||
export function useMeals() {
|
||||
return { getUpcomingMeals, getMeal, saveMeal, markMealConsumed, deleteMeal, toMealInput }
|
||||
}
|
||||
|
|
@ -1,15 +1,27 @@
|
|||
import * as sdk from '@/api/sdk'
|
||||
import type { ShoppingListItemWithRefs, Product, Meal } from '@/domain/types'
|
||||
import type { ShoppingListItemWithRefs, Product, Meal, Ingredient, Person, Recipe } from '@/domain/types'
|
||||
|
||||
export type GroupByProduct = { type: 'product'; product: Product; shoppingListItems: ShoppingListItemWithRefs[] }
|
||||
export type GroupByName = { type: 'name'; name: string; shoppingListItems: ShoppingListItemWithRefs[] }
|
||||
export type UIShoppingListItem = {
|
||||
id: number
|
||||
ingredient: Ingredient
|
||||
person?: Person | null
|
||||
personId: number
|
||||
recipe?: Recipe | null
|
||||
meal?: Meal | null
|
||||
ingredientId?: number | null
|
||||
listId?: number | null
|
||||
createdDate?: Date | null
|
||||
}
|
||||
|
||||
export type GroupByProduct = { type: 'product'; product: Product; shoppingListItems: UIShoppingListItem[] }
|
||||
export type GroupByName = { type: 'name'; name: string; shoppingListItems: UIShoppingListItem[] }
|
||||
export type Group = GroupByProduct | GroupByName
|
||||
|
||||
export function groupsToItems(groups: Group[]): ShoppingListItemWithRefs[] {
|
||||
export function groupsToItems(groups: Group[]): UIShoppingListItem[] {
|
||||
return groups.map((g) => g.shoppingListItems).flat()
|
||||
}
|
||||
|
||||
export function uniqueMeals(shoppingListItems: ShoppingListItemWithRefs[]): Meal[] {
|
||||
export function uniqueMeals(shoppingListItems: UIShoppingListItem[]): Meal[] {
|
||||
const mealsWithDuplicates = shoppingListItems.map((item) => item.meal).filter((m): m is Meal => !!m)
|
||||
const mealsLookup: Record<string | number, Meal> = mealsWithDuplicates.reduce<Record<string | number, Meal>>(
|
||||
(acc, meal) => {
|
||||
|
|
@ -21,11 +33,11 @@ export function uniqueMeals(shoppingListItems: ShoppingListItemWithRefs[]): Meal
|
|||
return Object.values(mealsLookup)
|
||||
}
|
||||
|
||||
export function itemsToGroups(shoppingListItems: ShoppingListItemWithRefs[]): Group[] {
|
||||
export function itemsToGroups(shoppingListItems: UIShoppingListItem[]): Group[] {
|
||||
const ingredients_by_product_id: Record<string | number, GroupByProduct> = {}
|
||||
const ingredients_by_name: Record<string, GroupByName> = {}
|
||||
for (const item of shoppingListItems) {
|
||||
if (item.ingredient?.product) {
|
||||
if (item.ingredient.product) {
|
||||
let group = ingredients_by_product_id[item.ingredient.product.id]
|
||||
if (!group) {
|
||||
group = ingredients_by_product_id[item.ingredient.product.id] = {
|
||||
|
|
@ -36,12 +48,11 @@ export function itemsToGroups(shoppingListItems: ShoppingListItemWithRefs[]): Gr
|
|||
}
|
||||
group.shoppingListItems.push(item)
|
||||
} else {
|
||||
const name = item.ingredient?.name ?? ''
|
||||
let group = ingredients_by_name[name]
|
||||
let group = ingredients_by_name[item.ingredient.name]
|
||||
if (!group) {
|
||||
group = ingredients_by_name[name] = {
|
||||
group = ingredients_by_name[item.ingredient.name] = {
|
||||
type: 'name',
|
||||
name,
|
||||
name: item.ingredient.name,
|
||||
shoppingListItems: [],
|
||||
}
|
||||
}
|
||||
|
|
@ -52,8 +63,34 @@ export function itemsToGroups(shoppingListItems: ShoppingListItemWithRefs[]): Gr
|
|||
}
|
||||
|
||||
export function useShopping() {
|
||||
const groupsFrom = (items?: ShoppingListItemWithRefs[]): Group[] => itemsToGroups(items ?? [])
|
||||
const mealsFrom = (items?: ShoppingListItemWithRefs[]): Meal[] => uniqueMeals(items ?? [])
|
||||
// Request shapes expected by sdk.purchaseShoppingList
|
||||
type PurchaseExisting = { type: 'existing'; id: number; personId: number; ingredientId?: number | null }
|
||||
type PurchaseRefs = { type: 'refs'; personId: number; ingredientId?: number | null; recipeId?: number | null; mealId?: number | null }
|
||||
const toExisting = (id: number, personId: number, ingredientId?: number | null): PurchaseExisting => ({ type: 'existing', id, personId, ingredientId: ingredientId ?? null })
|
||||
const toRefs = (personId: number, ingredientId?: number | null, recipeId?: number | null, mealId?: number | null): PurchaseRefs => ({
|
||||
type: 'refs',
|
||||
personId,
|
||||
ingredientId: ingredientId ?? null,
|
||||
recipeId: recipeId ?? null,
|
||||
mealId: mealId ?? null,
|
||||
})
|
||||
const mapItemToUI = (i: ShoppingListItemWithRefs): UIShoppingListItem => ({
|
||||
id: Number(i.id),
|
||||
ingredient: i.ingredient!,
|
||||
person: null,
|
||||
personId: i.personId,
|
||||
recipe: i.recipe ?? null,
|
||||
meal: i.meal ?? null,
|
||||
ingredientId: i.ingredient?.id ?? null,
|
||||
listId: i.listId ?? null,
|
||||
createdDate: i.createdDate,
|
||||
})
|
||||
const groupsFrom = (items?: ShoppingListItemWithRefs[]): Group[] => {
|
||||
return itemsToGroups((items ?? []).map(mapItemToUI))
|
||||
}
|
||||
const mealsFrom = (items?: ShoppingListItemWithRefs[]): Meal[] => {
|
||||
return uniqueMeals((items ?? []).map(mapItemToUI))
|
||||
}
|
||||
return {
|
||||
getCurrentShoppingList: sdk.getCurrentShoppingList,
|
||||
getShoppingList: sdk.getShoppingList,
|
||||
|
|
@ -63,21 +100,15 @@ export function useShopping() {
|
|||
getMyShoppingList: sdk.getMyShoppingList,
|
||||
saveMyShoppingList: sdk.saveMyShoppingList,
|
||||
// View-model helpers
|
||||
mapItemToUI,
|
||||
groupsFrom,
|
||||
mealsFrom,
|
||||
async purchaseFromGroups(groups: Group[]) {
|
||||
const items = groupsToItems(groups).map((i): sdk.PurchaseRequest => {
|
||||
if (typeof i.id === 'number' && i.id >= 0) {
|
||||
return { type: 'existing', id: i.id, personId: i.personId, ingredientId: i.ingredient?.id ?? null }
|
||||
}
|
||||
return {
|
||||
type: 'refs',
|
||||
personId: i.personId,
|
||||
ingredientId: i.ingredient?.id ?? null,
|
||||
recipeId: i.recipe?.id ?? null,
|
||||
mealId: i.meal?.id ?? null,
|
||||
}
|
||||
})
|
||||
const items = groupsToItems(groups).map((i) =>
|
||||
typeof i.id === 'number' && i.id >= 0
|
||||
? toExisting(i.id, i.personId, i.ingredientId)
|
||||
: toRefs(i.personId, i.ingredient?.id ?? null, i.recipe?.id ?? null, i.meal?.id ?? null)
|
||||
)
|
||||
if (!items?.length) return null
|
||||
return sdk.purchaseShoppingList(items)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { RecipeOut, Recipe, MealOut, Meal, MealRecipe, Ingredient as DomainIngredient, ShoppingList, ShoppingListItem, ShoppingListItemWithRefs, MealInput } from './types'
|
||||
import type { RecipeOut, Recipe, MealOut, Meal, MealRecipe, Ingredient as DomainIngredient, ShoppingList, ShoppingListItem } from './types'
|
||||
import type { components } from '@/api/types'
|
||||
|
||||
export function toDate(value: string | Date | null | undefined): Date | null {
|
||||
|
|
@ -6,25 +6,8 @@ export function toDate(value: string | Date | null | undefined): Date | null {
|
|||
return typeof value === 'string' ? new Date(value) : value
|
||||
}
|
||||
|
||||
// Small helper to decode optional lookup maps without repeating loops everywhere
|
||||
export function decodeLookup<TIn, TOut>(
|
||||
raw: Record<string, TIn> | null | undefined,
|
||||
decode: (v: TIn) => TOut
|
||||
): Record<string, TOut> | undefined {
|
||||
if (!raw) return undefined
|
||||
const out: Record<string, TOut> = {}
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(raw, key)) continue
|
||||
const maybe = raw[key]
|
||||
if (maybe === undefined) continue
|
||||
const decoded = decode(maybe)
|
||||
out[String(key)] = decoded
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function decodeRecipe(r: RecipeOut | null | undefined): Recipe {
|
||||
if (!r) throw new Error('Invalid recipe payload')
|
||||
export function decodeRecipe(r: RecipeOut | null | undefined): Recipe | null {
|
||||
if (!r) return null
|
||||
return {
|
||||
...r,
|
||||
dateCreated: toDate(r.dateCreated),
|
||||
|
|
@ -32,10 +15,15 @@ export function decodeRecipe(r: RecipeOut | null | undefined): Recipe {
|
|||
}
|
||||
}
|
||||
|
||||
export function decodeMeal(m: MealOut | null | undefined): Meal {
|
||||
if (!m) throw new Error('Invalid meal payload')
|
||||
export function decodeRecipes(list: RecipeOut[] | null | undefined): Recipe[] {
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map((r) => decodeRecipe(r)).filter((r): r is Recipe => !!r)
|
||||
}
|
||||
|
||||
export function decodeMeal(m: MealOut | null | undefined): Meal | null {
|
||||
if (!m) return null
|
||||
const recipes = Array.isArray(m.recipes)
|
||||
? m.recipes.map((mr) => decodeMealRecipe(mr))
|
||||
? m.recipes.map(mr => decodeMealRecipe(mr)).filter((r): r is MealRecipe => !!r)
|
||||
: []
|
||||
|
||||
return {
|
||||
|
|
@ -51,41 +39,40 @@ export function decodeMeal(m: MealOut | null | undefined): Meal {
|
|||
}
|
||||
}
|
||||
|
||||
function decodeMealRecipe(mr: components['schemas']['MealRecipe-Output'] | null | undefined): MealRecipe {
|
||||
if (!mr) throw new Error('Invalid meal recipe payload')
|
||||
export function decodeMealRecipe(mr: components['schemas']['MealRecipe-Output'] | null | undefined): MealRecipe | null {
|
||||
if (!mr) return null
|
||||
return {
|
||||
...mr,
|
||||
recipe: mr.recipe ? decodeRecipe(mr.recipe) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeIngredient(i: components['schemas']['Ingredient'] | null | undefined): DomainIngredient {
|
||||
if (!i) throw new Error('Invalid ingredient payload')
|
||||
export function decodeIngredient(i: components['schemas']['Ingredient'] | null | undefined): DomainIngredient | null {
|
||||
if (!i) return null
|
||||
// API guarantees quantity is a number; pass through
|
||||
return { ...i }
|
||||
}
|
||||
|
||||
export function decodeIngredients(list: components['schemas']['Ingredient'][] | null | undefined): DomainIngredient[] {
|
||||
if (!Array.isArray(list)) return []
|
||||
return list.map((i) => decodeIngredient(i))
|
||||
return list.map((i) => decodeIngredient(i)).filter((x): x is DomainIngredient => !!x)
|
||||
}
|
||||
|
||||
function decodeShoppingListItem(i: components['schemas']['ShoppingListItem'] | null | undefined): ShoppingListItem {
|
||||
if (!i) throw new Error('Invalid shopping list item payload')
|
||||
export function decodeShoppingListItem(i: components['schemas']['ShoppingListItem'] | null | undefined): ShoppingListItem | null {
|
||||
if (!i) return null
|
||||
return {
|
||||
...i,
|
||||
createdDate: toDate(i.createdDate),
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeShoppingListItems(list: components['schemas']['ShoppingListItem'][] | null | undefined): ShoppingListItemWithRefs[] {
|
||||
export function decodeShoppingListItems(list: components['schemas']['ShoppingListItem'][] | null | undefined): ShoppingListItem[] {
|
||||
if (!Array.isArray(list)) return []
|
||||
// Build a new array with item clones to allow optional refs to be attached later
|
||||
return list.map((raw) => ({ ...decodeShoppingListItem(raw) }))
|
||||
return list.map((i) => decodeShoppingListItem(i)).filter((x): x is ShoppingListItem => !!x)
|
||||
}
|
||||
|
||||
export function decodeShoppingList(v: components['schemas']['ShoppingList'] | null | undefined): ShoppingList {
|
||||
if (!v) throw new Error('Invalid shopping list payload')
|
||||
export function decodeShoppingList(v: components['schemas']['ShoppingList'] | null | undefined): ShoppingList | null {
|
||||
if (!v) return null
|
||||
const { items: rawItems, ...rest } = v
|
||||
const items = Array.isArray(rawItems) ? decodeShoppingListItems(rawItems) : undefined
|
||||
return {
|
||||
|
|
@ -94,23 +81,3 @@ export function decodeShoppingList(v: components['schemas']['ShoppingList'] | nu
|
|||
...(items ? { items } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert domain Meal to MealInput, keeping Date→string conversion in boundary
|
||||
export function toMealInput(meal: Meal): MealInput {
|
||||
return {
|
||||
id: meal.id,
|
||||
suggestedDate: meal.suggestedDate ? meal.suggestedDate.toISOString() : new Date().toISOString(),
|
||||
consumedDate: meal.consumedDate ? meal.consumedDate.toISOString() : null,
|
||||
purchaseDate: meal.purchaseDate ? meal.purchaseDate.toISOString() : null,
|
||||
chefs: meal.chefs,
|
||||
cleanup: meal.cleanup,
|
||||
consumers: meal.consumers,
|
||||
recipes: meal.recipes.map((r) => ({
|
||||
mealId: r.mealId,
|
||||
recipeId: r.recipeId,
|
||||
servings: r.servings,
|
||||
recipe: null,
|
||||
})),
|
||||
extraIngredients: meal.extraIngredients,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import type { components } from '@/api/types'
|
|||
export type Replace<T, M> = Omit<T, keyof M> & M
|
||||
export type WithDates<T, K extends keyof T> = Replace<T, { [P in K]: Date | null }>
|
||||
|
||||
// Common helpers (intentionally minimal to avoid unused exports)
|
||||
// Common helpers
|
||||
export type Maybe<T> = T | null | undefined
|
||||
export type NonNull<T> = Exclude<T, null | undefined>
|
||||
export type Lookup<T> = Record<string, T>
|
||||
// Refs are optional and may be attached later by mappers (e.g., in sdk)
|
||||
export type WithRefs<T, Refs extends object> = T & { [K in keyof Refs]?: Refs[K] | undefined }
|
||||
|
||||
// Domain type aliases
|
||||
|
|
@ -48,7 +49,7 @@ export type ShoppingListItemWithRefs = WithRefs<ShoppingListItem, { ingredient:
|
|||
|
||||
export type ShoppingListWithRefs = Replace<ShoppingList, { items?: ShoppingListItemWithRefs[] }>
|
||||
|
||||
// Lookup maps used by shopping mappings (all optional; mappers may omit absent ones)
|
||||
// Lookup maps used by shopping mappings
|
||||
export type ShoppingLookups = {
|
||||
ingredientsLookup?: Lookup<Ingredient>
|
||||
mealsLookup?: Lookup<Meal>
|
||||
|
|
|
|||
17
src/env.d.ts
vendored
17
src/env.d.ts
vendored
|
|
@ -1,10 +1,17 @@
|
|||
/* Minimal ambient typing for optional import.meta.env usage in tooling */
|
||||
/* Ambient env typing for optional Vite-style env access */
|
||||
declare interface ImportMeta {
|
||||
env?: {
|
||||
VITE_API_BASE_URL?: string
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL?: string
|
||||
}
|
||||
|
||||
declare interface ImportMeta {
|
||||
readonly env?: ImportMetaEnv
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
export {}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const UNIT_KEYS_ARRAY: readonly ['kg', 'litres', 'items'] = ['kg', 'litres', 'items']
|
||||
type UnitKey = typeof UNIT_KEYS_ARRAY[number]
|
||||
export type UnitKey = typeof UNIT_KEYS_ARRAY[number]
|
||||
|
||||
const equivalentUnits: Record<UnitKey, Record<string, number>> = {
|
||||
export const equivalentUnits: Record<UnitKey, Record<string, number>> = {
|
||||
kg: {
|
||||
kgs: 1,
|
||||
kilograms: 1,
|
||||
|
|
@ -124,8 +124,8 @@ export function getConversionFactor(unit: string): { unit: UnitKey | string; fac
|
|||
return null
|
||||
}
|
||||
|
||||
type Quantity = { quantity: number; unit: string }
|
||||
type Total = { unit: string; quantity: number }
|
||||
export type Quantity = { quantity: number; unit: string }
|
||||
export type Total = { unit: string; quantity: number }
|
||||
|
||||
export function calculateTotals(quantityList: Quantity[]): Total[] {
|
||||
const totals: Record<string, number> = {}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { afterAll, afterEach, beforeAll } from 'vitest'
|
|||
import { setupServer } from 'msw/node'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
|
||||
// Ensure global fetch is available in Node tests (Node 18+ provides global fetch)
|
||||
// Do not override so MSW can intercept.
|
||||
// Ensure global fetch is available in Node tests
|
||||
// Use Node 18+ global fetch (undici). Do not override so MSW can intercept.
|
||||
|
||||
// Some CI envs inject corporate roots causing TLS issues; disable cert checks in tests only
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
|
|
|||
7
tests/useAlert.test.js
Normal file
7
tests/useAlert.test.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Legacy JS test shim: the authoritative tests live in tests/useAlert.test.ts
|
||||
// Keep a trivial passing test here so Vitest doesn't fail this file collection.
|
||||
import { it, expect } from 'vitest'
|
||||
|
||||
it('noop shim (see useAlert.test.ts)', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
"allowJs": false,
|
||||
"checkJs": false,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"],
|
||||
"types": ["node", "vite/client"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
|||
},
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.{test,spec}.{js,ts}'],
|
||||
include: ['tests/**/*.{test,spec}.js'],
|
||||
globals: true,
|
||||
reporters: 'default',
|
||||
setupFiles: ['tests/test-setup.js'],
|
||||
|
|
|
|||
Loading…
Reference in a new issue