diff --git a/apps/bff/package.json b/apps/bff/package.json index 72114bec..22ebf5bc 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -53,7 +53,7 @@ "rxjs": "^7.8.2", "salesforce-pubsub-api-client": "^5.5.1", "ssh2-sftp-client": "^12.0.1", - "zod": "4.1.13" + "zod": "catalog:" }, "devDependencies": { "@nestjs/cli": "^11.0.14", @@ -65,6 +65,6 @@ "@types/ssh2-sftp-client": "^9.0.6", "pino-pretty": "^13.1.3", "tsc-alias": "^1.8.16", - "typescript": "5.9.3" + "typescript": "catalog:" } } diff --git a/apps/bff/tsconfig.json b/apps/bff/tsconfig.json index 67088c6b..0d09c85c 100644 --- a/apps/bff/tsconfig.json +++ b/apps/bff/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.node.json", "compilerOptions": { "verbatimModuleSyntax": true, "emitDecoratorMetadata": true, diff --git a/apps/portal/package.json b/apps/portal/package.json index bfcfabe2..bfc7fce2 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -29,7 +29,7 @@ "react-dom": "19.2.1", "tailwind-merge": "^3.4.0", "world-countries": "^5.1.0", - "zod": "4.1.13", + "zod": "catalog:", "zustand": "^5.0.9" }, "devDependencies": { @@ -39,6 +39,6 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "tailwindcss": "^4.1.17", - "typescript": "5.9.3" + "typescript": "catalog:" } } diff --git a/apps/portal/scripts/bundle-monitor.mjs b/apps/portal/scripts/bundle-monitor.mjs index d96c73bd..1fe77fb5 100644 --- a/apps/portal/scripts/bundle-monitor.mjs +++ b/apps/portal/scripts/bundle-monitor.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node /* eslint-env node */ -/* global console, process */ /** * Bundle size monitoring script diff --git a/apps/portal/scripts/dev-prep.mjs b/apps/portal/scripts/dev-prep.mjs index d7a42a3c..9923b8e2 100644 --- a/apps/portal/scripts/dev-prep.mjs +++ b/apps/portal/scripts/dev-prep.mjs @@ -5,7 +5,6 @@ import { mkdirSync, existsSync, writeFileSync, rmSync } from "fs"; import { join } from "path"; import { URL } from "node:url"; -/* global console */ const root = new URL("..", import.meta.url).pathname; // apps/portal const nextDir = join(root, ".next"); diff --git a/apps/portal/scripts/test-request-password-reset.cjs b/apps/portal/scripts/test-request-password-reset.cjs index 784cb2be..f1503d89 100755 --- a/apps/portal/scripts/test-request-password-reset.cjs +++ b/apps/portal/scripts/test-request-password-reset.cjs @@ -1,6 +1,5 @@ #!/usr/bin/env node /* eslint-env node */ -/* global __dirname, console, process */ const fs = require("node:fs"); const path = require("node:path"); diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json index 3a57a1ba..6827fc99 100644 --- a/apps/portal/tsconfig.json +++ b/apps/portal/tsconfig.json @@ -1,12 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.next.json", "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "Bundler", - "jsx": "preserve", - "noEmit": true, "plugins": [{ "name": "next" }], "composite": true, "tsBuildInfoFile": ".typecheck/tsconfig.tsbuildinfo", diff --git a/eslint.config.mjs b/eslint.config.mjs index e307cfff..6c09583e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,12 +4,24 @@ import process from "node:process"; import js from "@eslint/js"; import nextPlugin from "@next/eslint-plugin-next"; import reactHooks from "eslint-plugin-react-hooks"; -import tseslint from "typescript-eslint"; -import prettier from "eslint-plugin-prettier"; import globals from "globals"; +import tseslint from "typescript-eslint"; + +const TS_FILES = ["**/*.{ts,tsx}"]; +const BFF_TS_FILES = ["apps/bff/**/*.ts"]; +const PACKAGES_TS_FILES = ["packages/**/*.ts"]; +const PORTAL_FILES = ["apps/portal/**/*.{js,jsx,ts,tsx}"]; + +const withFiles = (configs, files) => + configs.map(config => ({ + ...config, + files, + })); export default [ + // ============================================================================= // Global ignores + // ============================================================================= { ignores: [ "**/node_modules/**", @@ -26,30 +38,22 @@ export default [ ], }, - // Base JS recommendations + // ============================================================================= + // Base JS + // ============================================================================= js.configs.recommended, - // Prettier integration - { - plugins: { prettier }, - rules: { "prettier/prettier": "warn" }, - }, + // ============================================================================= + // TypeScript (fast rules, no type information) + // ============================================================================= + ...withFiles(tseslint.configs.recommended, TS_FILES), - // TypeScript recommended rules (fast, no type info) - ...tseslint.configs.recommended.map((config) => ({ + // ============================================================================= + // TypeScript (type-aware rules) — only where we want the cost + // ============================================================================= + ...tseslint.configs.recommendedTypeChecked.map(config => ({ ...config, - files: ["**/*.{ts,tsx}"], - languageOptions: { - ...(config.languageOptions || {}), - // Keep config simple: allow both environments; app-specific blocks can tighten later - globals: { ...globals.browser, ...globals.node }, - }, - })), - - // TypeScript type-aware rules only where we really want them (backend + shared packages) - ...tseslint.configs.recommendedTypeChecked.map((config) => ({ - ...config, - files: ["apps/bff/**/*.ts", "packages/**/*.ts"], + files: [...BFF_TS_FILES, ...PACKAGES_TS_FILES], languageOptions: { ...(config.languageOptions || {}), parserOptions: { @@ -57,13 +61,14 @@ export default [ projectService: true, tsconfigRootDir: process.cwd(), }, - globals: { ...globals.node }, }, })), - // Backend & domain packages + // ============================================================================= + // Backend + domain packages: sensible defaults + // ============================================================================= { - files: ["apps/bff/**/*.ts", "packages/domain/**/*.ts"], + files: [...BFF_TS_FILES, "packages/domain/**/*.ts"], rules: { "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], @@ -71,9 +76,11 @@ export default [ }, }, - // Strict rules for shared packages + // ============================================================================= + // Shared packages: stricter safety + // ============================================================================= { - files: ["packages/**/*.ts"], + files: PACKAGES_TS_FILES, rules: { "@typescript-eslint/no-redundant-type-constituents": "error", "@typescript-eslint/no-explicit-any": "error", @@ -83,13 +90,14 @@ export default [ }, }, - // Portal (Next.js) rules (flat-config friendly) + // ============================================================================= + // Portal (Next.js) + // ============================================================================= { ...nextPlugin.configs["core-web-vitals"], - files: ["apps/portal/**/*.{js,jsx,ts,tsx}"], + files: PORTAL_FILES, }, { - // Keep this minimal (defaults-first): only the two classic hook rules. files: ["apps/portal/**/*.{jsx,tsx}"], plugins: { "react-hooks": reactHooks }, rules: { @@ -97,23 +105,23 @@ export default [ "react-hooks/exhaustive-deps": "warn", }, }, - - // Portal overrides { files: ["apps/portal/**/*.{ts,tsx}"], - languageOptions: { - globals: { ...globals.browser, ...globals.node }, - }, rules: { "@typescript-eslint/no-unused-vars": [ "error", - { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, ], "@next/next/no-html-link-for-pages": "off", }, }, - // Prevent hard navigation in authenticated pages + // ============================================================================= + // Portal navigation guardrails + // ============================================================================= { files: ["apps/portal/src/app/(authenticated)/**/*.{ts,tsx}"], rules: { @@ -126,8 +134,6 @@ export default [ ], }, }, - - // Exceptions for specific files { files: ["apps/portal/src/app/(authenticated)/layout.tsx"], rules: { "no-restricted-imports": "off" }, @@ -137,7 +143,9 @@ export default [ rules: { "no-restricted-syntax": "off" }, }, - // Enforce domain imports architecture + // ============================================================================= + // Architecture: import boundaries + // ============================================================================= { files: ["apps/portal/src/**/*.{ts,tsx}", "apps/bff/src/**/*.ts"], rules: { @@ -155,9 +163,11 @@ export default [ }, }, - // BFF strict type safety + // ============================================================================= + // BFF: stricter type safety (type-aware) + // ============================================================================= { - files: ["apps/bff/**/*.ts"], + files: BFF_TS_FILES, rules: { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unsafe-assignment": "error", @@ -170,13 +180,16 @@ export default [ }, }, - // Node globals for config files + // ============================================================================= + // Node globals for tooling/config files + // ============================================================================= { files: [ "*.config.*", "apps/portal/next.config.mjs", "config/**/*.{js,cjs,mjs}", "scripts/**/*.{js,cjs,mjs}", + "apps/**/scripts/**/*.{js,cjs,mjs}", ], languageOptions: { globals: { ...globals.node }, diff --git a/package.json b/package.json index a264cba8..b1c8f496 100644 --- a/package.json +++ b/package.json @@ -53,24 +53,20 @@ "devDependencies": { "@next/eslint-plugin-next": "16.0.9", "@eslint/js": "^9.39.1", - "@types/node": "^24.10.3", + "@types/node": "catalog:", "eslint": "^9.39.1", "lint-staged": "^16.2.7", - "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^16.5.0", "husky": "^9.1.7", "prettier": "^3.7.4", "tsx": "^4.21.0", - "typescript": "^5.9.3", + "typescript": "catalog:", "typescript-eslint": "^8.49.0" }, "pnpm": { "overrides": { - "js-yaml": ">=4.1.1", - "typescript": "5.9.3", - "@types/node": "24.10.3", - "zod": "4.1.13" + "js-yaml": ">=4.1.1" } } } diff --git a/packages/domain/dashboard/contract.ts b/packages/domain/dashboard/contract.ts index 99e247d8..efd1deab 100644 --- a/packages/domain/dashboard/contract.ts +++ b/packages/domain/dashboard/contract.ts @@ -1,4 +1,4 @@ -import type { infer as ZodInfer } from "zod"; +import type { z } from "zod"; import type { activityTypeSchema, activitySchema, @@ -11,12 +11,12 @@ import type { dashboardSummaryResponseSchema, } from "./schema.js"; -export type ActivityType = ZodInfer; -export type Activity = ZodInfer; -export type DashboardStats = ZodInfer; -export type NextInvoice = ZodInfer; -export type DashboardSummary = ZodInfer; -export type DashboardError = ZodInfer; -export type ActivityFilter = ZodInfer; -export type ActivityFilterConfig = ZodInfer; -export type DashboardSummaryResponse = ZodInfer; +export type ActivityType = z.infer; +export type Activity = z.infer; +export type DashboardStats = z.infer; +export type NextInvoice = z.infer; +export type DashboardSummary = z.infer; +export type DashboardError = z.infer; +export type ActivityFilter = z.infer; +export type ActivityFilterConfig = z.infer; +export type DashboardSummaryResponse = z.infer; diff --git a/packages/domain/orders/validation.ts b/packages/domain/orders/validation.ts index 562bca31..85435eeb 100644 --- a/packages/domain/orders/validation.ts +++ b/packages/domain/orders/validation.ts @@ -5,7 +5,7 @@ * These rules represent domain logic and should be reusable across frontend/backend. */ -import type { infer as ZodInfer } from "zod"; +import type { z } from "zod"; import { orderBusinessValidationSchema } from "./schema.js"; // ============================================================================ @@ -101,7 +101,7 @@ export const orderWithSkuValidationSchema = orderBusinessValidationSchema path: ["skus"], }); -export type OrderWithSkuValidation = ZodInfer; +export type OrderWithSkuValidation = z.infer; // ============================================================================ // Validation Error Messages diff --git a/packages/domain/package.json b/packages/domain/package.json index 0f3ea386..52cbce58 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -133,10 +133,10 @@ "typecheck": "pnpm run type-check" }, "peerDependencies": { - "zod": "4.1.13" + "zod": "catalog:" }, "devDependencies": { - "typescript": "5.9.3", - "zod": "4.1.13" + "typescript": "catalog:", + "zod": "catalog:" } } diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json index 7e64a9be..8757a636 100644 --- a/packages/domain/tsconfig.json +++ b/packages/domain/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.node.json", "compilerOptions": { "composite": true, "declaration": true, diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 286cf7f5..0c3dcd31 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,9 @@ packages: - apps/* - packages/* + +# Centralized dependency versions (pnpm Catalogs) +catalog: + zod: "4.1.13" + typescript: "5.9.3" + "@types/node": "24.10.3" diff --git a/scripts/validate-deps.sh b/scripts/validate-deps.sh index 5966fa62..65741bdb 100755 --- a/scripts/validate-deps.sh +++ b/scripts/validate-deps.sh @@ -65,6 +65,31 @@ if (hasDrift) { } " +# 2b. Ensure a single installed Zod version (lockfile-level) +echo "🧩 Checking for multiple installed Zod versions..." +node -e " +const fs = require('fs'); +const lock = fs.readFileSync('pnpm-lock.yaml', 'utf8'); +const versions = new Set(); +const re = /\\n\\s{2}zod@([^:\\s]+):/g; +let m; +while ((m = re.exec(lock)) !== null) versions.add(m[1]); + +if (versions.size === 0) { + console.log('āš ļø No zod entries found in lockfile (unexpected).'); + process.exit(1); +} + +if (versions.size > 1) { + console.log('āŒ Multiple zod versions installed:'); + [...versions].sort().forEach(v => console.log(' - zod@' + v)); + console.log('\\nšŸ’” Fix by aligning dependencies or adding a pnpm override for zod.'); + process.exit(1); +} + +console.log('āœ… Single zod version installed:', 'zod@' + [...versions][0]); +" + # 3. Security audit echo "šŸ”’ Running security audit..." if pnpm audit --audit-level moderate; then diff --git a/tsconfig.base.json b/tsconfig.base.json index 9c8b7044..cf105a96 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,10 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - "target": "ES2024", - "lib": ["ES2024"], - "module": "NodeNext", - "moduleResolution": "NodeNext", "strict": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, diff --git a/tsconfig.next.json b/tsconfig.next.json new file mode 100644 index 00000000..0b689487 --- /dev/null +++ b/tsconfig.next.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "preserve", + "noEmit": true + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..dbd2605a --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.base.json", + "compilerOptions": { + "target": "ES2024", + "lib": ["ES2024"], + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +}