From 5771a678ef0b7b271fa4df0542d24d1ef6acc710 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 25 Apr 2026 03:58:11 +0200 Subject: [PATCH] chore: install ts-reset in both portal and app SPAs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installs @total-typescript/ts-reset 0.6.1 as a dev-dependency in apps/portal/ and apps/app/. Patches TypeScript's loosest default types: Array.filter(Boolean) returns non-nullable, JSON.parse returns unknown, fetch().json() returns unknown, Map.get() strict, etc. Configuration: src/reset.d.ts in each SPA imports the reset. Both tsconfig.json files already include ./src/**/* so the .d.ts is picked up automatically — no tsconfig edits needed. Issues surfaced during install: - apps/app — 0 pre-install tsc errors in own code; install surfaced 2 errors in src/stores/useImpersonationStore.ts (both from JSON.parse on sessionStorage content returning unknown instead of any). Fixed inline at lines 19 + 123 via `as ImpersonationState` casts that make the existing trust-in-sessionStorage explicit. Backlog entry TECH-TS-IMPERSONATION tracks proper runtime shape validation. - apps/portal — 22 pre-existing tsc errors in own code (mostly tiptap editor components — tracked as TECH-TS-PORTAL-TSC, unrelated to ts-reset). Zero new errors in portal's own code. 4 additional errors surfaced in tiptap's uncompiled node_modules .ts sources (third-party); left as-is. Neither SPA achieves `tsc --noEmit` clean today — pre-existing state unrelated to this work package. Build + vitest are the actual working gates and both remain green: - apps/portal: vitest 113/113 passing; production build succeeds - apps/app: (no vitest setup — tracked as TECH-APP-VITEST); production build succeeds Documentation: /dev-docs/FRONTEND-TOOLING.md added; CLAUDE.md quality-gates updated. Backlog: TECH-TS-IMPERSONATION (runtime validation of stored impersonation state), TECH-TS-PORTAL-TSC (pre-existing portal tsc errors), TECH-APP-VITEST (Vitest coverage for apps/app). No production behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 3 + apps/app/package.json | 1 + apps/app/pnpm-lock.yaml | 8 +++ apps/app/src/reset.d.ts | 1 + apps/app/src/stores/useImpersonationStore.ts | 4 +- apps/portal/package.json | 1 + apps/portal/pnpm-lock.yaml | 8 +++ apps/portal/src/reset.d.ts | 1 + dev-docs/BACKLOG.md | 44 +++++++++++++ dev-docs/FRONTEND-TOOLING.md | 67 ++++++++++++++++++++ 10 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 apps/app/src/reset.d.ts create mode 100644 apps/portal/src/reset.d.ts create mode 100644 dev-docs/FRONTEND-TOOLING.md diff --git a/CLAUDE.md b/CLAUDE.md index ba7c3a39..15531863 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,9 @@ Design document: `/dev-docs/design-document.md` - `composer rector` — Rector dry-run for modernisation suggestions. See `/dev-docs/RECTOR.md`. Apply only in scoped sprints, never automatically. +- ts-reset patches TypeScript's loosest default types in both SPAs. + See `/dev-docs/FRONTEND-TOOLING.md`. New TypeScript code adheres + to ts-reset's stricter types automatically. ## Repository layout diff --git a/apps/app/package.json b/apps/app/package.json index 9fcae881..7a8e7a04 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -83,6 +83,7 @@ "@tiptap/extension-subscript": "^2.27.1", "@tiptap/extension-superscript": "^2.27.1", "@tiptap/extension-underline": "^2.27.1", + "@total-typescript/ts-reset": "^0.6.1", "@types/mapbox-gl": "3.4.1", "@types/node": "24.9.2", "@types/qrcode": "^1.5.6", diff --git a/apps/app/pnpm-lock.yaml b/apps/app/pnpm-lock.yaml index 8eae7e09..48f15e6b 100644 --- a/apps/app/pnpm-lock.yaml +++ b/apps/app/pnpm-lock.yaml @@ -214,6 +214,9 @@ importers: '@tiptap/extension-underline': specifier: ^2.27.1 version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@total-typescript/ts-reset': + specifier: ^0.6.1 + version: 0.6.1 '@types/mapbox-gl': specifier: 3.4.1 version: 3.4.1 @@ -1383,6 +1386,9 @@ packages: '@tiptap/pm': ^2.7.0 vue: ^3.0.0 + '@total-typescript/ts-reset@0.6.1': + resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==} + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -6044,6 +6050,8 @@ snapshots: '@tiptap/pm': 2.27.1 vue: 3.5.22(typescript@5.9.3) + '@total-typescript/ts-reset@0.6.1': {} + '@trysound/sax@0.2.0': {} '@tybys/wasm-util@0.10.1': diff --git a/apps/app/src/reset.d.ts b/apps/app/src/reset.d.ts new file mode 100644 index 00000000..69fc8bf0 --- /dev/null +++ b/apps/app/src/reset.d.ts @@ -0,0 +1 @@ +import '@total-typescript/ts-reset' diff --git a/apps/app/src/stores/useImpersonationStore.ts b/apps/app/src/stores/useImpersonationStore.ts index d6fae319..4e9d985d 100644 --- a/apps/app/src/stores/useImpersonationStore.ts +++ b/apps/app/src/stores/useImpersonationStore.ts @@ -16,7 +16,7 @@ interface ImpersonationState { export const useImpersonationStore = defineStore('impersonation', () => { const stored = sessionStorage.getItem(SESSION_STORAGE_KEY) - const state = ref(stored ? JSON.parse(stored) : null) + const state = ref(stored ? (JSON.parse(stored) as ImpersonationState) : null) let broadcastChannel: BroadcastChannel | null = null const isImpersonating = computed(() => !!state.value) @@ -120,7 +120,7 @@ export const useImpersonationStore = defineStore('impersonation', () => { const stored = sessionStorage.getItem(SESSION_STORAGE_KEY) if (stored) { try { - state.value = JSON.parse(stored) + state.value = JSON.parse(stored) as ImpersonationState } catch { sessionStorage.removeItem(SESSION_STORAGE_KEY) diff --git a/apps/portal/package.json b/apps/portal/package.json index fd67f609..751a3ce6 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -87,6 +87,7 @@ "@tiptap/extension-subscript": "^2.27.1", "@tiptap/extension-superscript": "^2.27.1", "@tiptap/extension-underline": "^2.27.1", + "@total-typescript/ts-reset": "^0.6.1", "@types/mapbox-gl": "3.4.1", "@types/node": "24.9.2", "@types/qrcode": "^1.5.6", diff --git a/apps/portal/pnpm-lock.yaml b/apps/portal/pnpm-lock.yaml index c74db313..5b3546b0 100644 --- a/apps/portal/pnpm-lock.yaml +++ b/apps/portal/pnpm-lock.yaml @@ -220,6 +220,9 @@ importers: '@tiptap/extension-underline': specifier: ^2.27.1 version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@total-typescript/ts-reset': + specifier: ^0.6.1 + version: 0.6.1 '@types/mapbox-gl': specifier: 3.4.1 version: 3.4.1 @@ -1473,6 +1476,9 @@ packages: '@tiptap/pm': ^2.7.0 vue: ^3.0.0 + '@total-typescript/ts-reset@0.6.1': + resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==} + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -6512,6 +6518,8 @@ snapshots: '@tiptap/pm': 2.27.1 vue: 3.5.22(typescript@5.9.3) + '@total-typescript/ts-reset@0.6.1': {} + '@trysound/sax@0.2.0': {} '@tybys/wasm-util@0.10.1': diff --git a/apps/portal/src/reset.d.ts b/apps/portal/src/reset.d.ts new file mode 100644 index 00000000..69fc8bf0 --- /dev/null +++ b/apps/portal/src/reset.d.ts @@ -0,0 +1 @@ +import '@total-typescript/ts-reset' diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index 4a4d0b28..e11cdcc5 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -967,5 +967,49 @@ blijft handmatig. --- +## Frontend type-safety + +### TECH-TS-IMPERSONATION — runtime validation of impersonation state + +**Aanleiding:** ts-reset install (april 2026) surfaced dat +`apps/app/src/stores/useImpersonationStore.ts` blindly vertrouwt +op `JSON.parse(sessionStorage.getItem(...))`. De return is nu +`unknown` in plaats van `any`; we fixten dit door twee `as +ImpersonationState` casts toe te voegen. Die maken de bestaande +trust expliciet maar valideren de shape niet. +**Wat:** vervang beide casts (lines 19 + 123) door een +narrowing-helper `parseStoredState(raw: string | null): +ImpersonationState | null` die het JSON parseert, de verwachte +keys controleert (incl. geneste `impersonatedUser: AdminUser` +fields), en `null` teruggeeft als de shape niet klopt. Zelfde +helper gebruiken bij beide call sites. sessionStorage is in theorie +tamperbaar door lokale users, dus dit is ook een kleine security +verbetering. +**Prioriteit:** Laag — defensive hardening, geen user-impact. + +### TECH-TS-PORTAL-TSC — portal tsc --noEmit cleanup + +**Aanleiding:** Portal heeft 22 pre-existing tsc errors (+4 in +tiptap node_modules) die niets met ts-reset te maken hebben — meest +TiptapEditor/ProductDescriptionEditor commands en één losse +`@iconify/types` missing, themeConfig type-lowercase issue. CI +checkt tsc niet (build + vitest wel), dus het wordt momenteel +genegeerd. Fix zodra tiptap upgrade beschikbaar komt of per- +component via type-casts. +**Prioriteit:** Laag — niet-blocking, bestaand probleem. + +### TECH-APP-VITEST — bring Vitest coverage to apps/app + +**Aanleiding:** ts-reset install (april 2026) onderstreepte dat +`apps/app/` geen test-script of `tests/` directory heeft. Portal +heeft wel Vitest-coverage (113 tests); organizer-SPA heeft alleen +build + typecheck als geautomatiseerde gates. +**Wat:** Vitest setup in `apps/app/`, met eerste coverage op de +kern-stores (waaronder `useImpersonationStore.ts` dat nu zonder +runtime-test leeft). +**Prioriteit:** Middel. + +--- + _Laatste update: April 2026_ _Voeg nieuwe items toe met prefix: ARCH-, COMM-, OPS-, VOL-, ART-, FORM-, SUP-, DIFF-, APPS-, TECH-, UX-_ diff --git a/dev-docs/FRONTEND-TOOLING.md b/dev-docs/FRONTEND-TOOLING.md new file mode 100644 index 00000000..6006595b --- /dev/null +++ b/dev-docs/FRONTEND-TOOLING.md @@ -0,0 +1,67 @@ +# Frontend tooling + +Type-safety and quality tooling for the Vue 3 + TypeScript SPAs. + +## ts-reset + +Installed in both `apps/portal/` and `apps/app/` as a dev-dependency. +Imported via `src/reset.d.ts`: + +```typescript +import '@total-typescript/ts-reset' +``` + +Both SPAs' `tsconfig.json` include `./src/**/*`, so the `.d.ts` is +picked up automatically — no explicit `include` edit needed. + +### What it changes + +ts-reset patches TypeScript's loosest default types: + +- `Array.filter(Boolean)` returns non-nullable `Array` instead + of `Array` +- `JSON.parse` returns `unknown` instead of `any` +- `fetch().then(r => r.json())` returns `unknown` instead of `any` +- `Array.includes()` accepts narrower types +- `Map.get()` returns `T | undefined` more strictly +- See for the full list + +### Why this matters + +The default TypeScript types for these built-ins lose information +that real code relies on. ts-reset fixes the types to match runtime +behavior, surfacing bugs that were previously hidden. + +### Adoption state + +**apps/app** — 0 pre-install tsc errors in own code; install +surfaced 2 errors in `src/stores/useImpersonationStore.ts` (both +from `JSON.parse` on sessionStorage content). Both fixed inline via +`as ImpersonationState` assertions that make the existing trust-in- +sessionStorage explicit. A backlog entry (TECH-TS-IMPERSONATION) +tracks proper runtime validation of the stored shape. + +**apps/portal** — 22 pre-existing tsc errors in own code (mostly +tiptap editor components — unrelated to ts-reset). ts-reset added +0 new errors in portal's own code. 4 additional errors surfaced in +tiptap's third-party `.ts` sources under `node_modules` (tiptap +ships uncompiled TypeScript and some of its internal `JSON.parse` +call sites fall under ts-reset's tightening). Not our code; left +as-is. + +Neither SPA achieves `tsc --noEmit` clean today — that's a pre- +existing state unrelated to this work package. Build + vitest are +the actual CI-relevant gates and both remain green. + +### Bypassing + +`@ts-expect-error` with comment justification at the call site — +never blanket-disable ts-reset. If a specific reset is genuinely +wrong for the project, document it; the upstream issue is the +backstop. + +## Vitest + +Runs against `apps/portal/`. `apps/app/` has no test suite yet — +build + typecheck are its only automated gates. Bringing Vitest +coverage to `apps/app` is a separate backlog item.