From fa056e99a4d8e4d524be118ea2471e0ec7eab1a2 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 20 Mar 2026 14:57:39 +0100 Subject: [PATCH 1/2] Enforce exact dependency versions --- .ncurc.json | 3 + .npmrc | 1 + apps/playground/.npmrc | 1 + package.json | 18 +- packages/react-native/.eslintrc.cjs | 4 +- packages/react-native/package.json | 1 + pnpm-lock.yaml | 277 +++++++++++++++++++++------- scripts/check-exact-deps.mjs | 116 ++++++++++++ 8 files changed, 342 insertions(+), 79 deletions(-) create mode 100644 .ncurc.json create mode 100644 scripts/check-exact-deps.mjs diff --git a/.ncurc.json b/.ncurc.json new file mode 100644 index 0000000..23dd3d5 --- /dev/null +++ b/.ncurc.json @@ -0,0 +1,3 @@ +{ + "removeRange": true +} diff --git a/.npmrc b/.npmrc index e69de29..cffe8cd 100644 --- a/.npmrc +++ b/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/apps/playground/.npmrc b/apps/playground/.npmrc index d67f374..0e5379f 100644 --- a/apps/playground/.npmrc +++ b/apps/playground/.npmrc @@ -1 +1,2 @@ node-linker=hoisted +save-exact=true diff --git a/package.json b/package.json index d1e2cc2..4ad658e 100644 --- a/package.json +++ b/package.json @@ -4,28 +4,28 @@ "scripts": { "build": "turbo run build", "dev": "turbo run dev", - "lint": "turbo run lint", + "lint": "node scripts/check-exact-deps.mjs && turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "check-types": "turbo run check-types", "test": "turbo run test --no-cache", "test:coverage": "turbo run test:coverage --no-cache" }, "devDependencies": { - "prettier": "^3.8.1", - "turbo": "^2.8.8", + "prettier": "3.8.1", + "turbo": "2.8.20", "typescript": "5.9.3", - "rimraf": "6.1.2" + "rimraf": "6.1.3" }, - "packageManager": "pnpm@10.29.3", + "packageManager": "pnpm@10.32.1", "engines": { "node": ">=20" }, "pnpm": { "overrides": { - "on-headers": ">=1.1.0", - "glob": ">=11.1.0", - "node-forge": ">=1.3.2", - "js-yaml": ">=4.1.1", + "on-headers": "1.1.0", + "glob": "13.0.4", + "node-forge": "1.3.3", + "js-yaml": "4.1.1", "tar": "7.5.7" } } diff --git a/packages/react-native/.eslintrc.cjs b/packages/react-native/.eslintrc.cjs index c6d80de..1d52b15 100644 --- a/packages/react-native/.eslintrc.cjs +++ b/packages/react-native/.eslintrc.cjs @@ -1,3 +1,5 @@ +const project = "tsconfig.json"; + module.exports = { extends: [ "@vercel/style-guide/eslint/browser", @@ -5,7 +7,7 @@ module.exports = { "@vercel/style-guide/eslint/react", ].map(require.resolve), parserOptions: { - project: "tsconfig.json", + project, tsconfigRootDir: __dirname, }, globals: { diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 4651143..fbad92d 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -47,6 +47,7 @@ "devDependencies": { "@types/react": "19.2.14", "@vercel/style-guide": "6.0.0", + "@vitest/eslint-plugin": "1.6.12", "@vitest/coverage-v8": "4.0.18", "react": "19.2.4", "react-native": "0.84.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b86767..7d3004c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,10 +5,10 @@ settings: excludeLinksFromLockfile: false overrides: - on-headers: '>=1.1.0' - glob: '>=11.1.0' - node-forge: '>=1.3.2' - js-yaml: '>=4.1.1' + on-headers: 1.1.0 + glob: 13.0.4 + node-forge: 1.3.3 + js-yaml: 4.1.1 tar: 7.5.7 importers: @@ -16,14 +16,14 @@ importers: .: devDependencies: prettier: - specifier: ^3.8.1 + specifier: 3.8.1 version: 3.8.1 rimraf: - specifier: 6.1.2 - version: 6.1.2 + specifier: 6.1.3 + version: 6.1.3 turbo: - specifier: ^2.8.8 - version: 2.8.9 + specifier: 2.8.20 + version: 2.8.20 typescript: specifier: 5.9.3 version: 5.9.3 @@ -89,6 +89,9 @@ importers: '@vitest/coverage-v8': specifier: 4.0.18 version: 4.0.18(vitest@4.0.18(@types/node@25.2.3)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) + '@vitest/eslint-plugin': + specifier: 1.6.12 + version: 1.6.12(eslint@8.57.0)(typescript@5.9.3)(vitest@4.0.18(@types/node@25.2.3)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) react: specifier: 19.2.4 version: 19.2.4 @@ -1328,6 +1331,36 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@turbo/darwin-64@2.8.20': + resolution: {integrity: sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.8.20': + resolution: {integrity: sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.8.20': + resolution: {integrity: sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.8.20': + resolution: {integrity: sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.8.20': + resolution: {integrity: sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.8.20': + resolution: {integrity: sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw==} + cpu: [arm64] + os: [win32] + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1415,6 +1448,12 @@ packages: typescript: optional: true + '@typescript-eslint/project-service@8.57.1': + resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@5.62.0': resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1423,6 +1462,16 @@ packages: resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.57.1': + resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.57.1': + resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1441,6 +1490,10 @@ packages: resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.57.1': + resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1459,6 +1512,12 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@8.57.1': + resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@5.62.0': resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1471,6 +1530,13 @@ packages: peerDependencies: eslint: ^8.56.0 + '@typescript-eslint/utils@8.57.1': + resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@5.62.0': resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1479,6 +1545,10 @@ packages: resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@8.57.1': + resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1620,6 +1690,19 @@ packages: '@vitest/browser': optional: true + '@vitest/eslint-plugin@1.6.12': + resolution: {integrity: sha512-4kI47BJNFE+EQ5bmPbHzBF+ibNzx2Fj0Jo9xhWsTPxMddlHwIWl6YAxagefh461hrwx/W0QwBZpxGS404kBXyg==} + engines: {node: '>=18'} + peerDependencies: + eslint: '>=8.57.0' + typescript: '>=5.0.0' + vitest: '*' + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} @@ -2494,6 +2577,10 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3446,6 +3533,10 @@ packages: resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} engines: {node: 20 || >=22} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3940,8 +4031,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@6.1.2: - resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==} + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} hasBin: true @@ -4291,6 +4382,12 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -4309,38 +4406,8 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - turbo-darwin-64@2.8.9: - resolution: {integrity: sha512-KnCw1ZI9KTnEAhdI9avZrnZ/z4wsM++flMA1w8s8PKOqi5daGpFV36qoPafg4S8TmYMe52JPWEoFr0L+lQ5JIw==} - cpu: [x64] - os: [darwin] - - turbo-darwin-arm64@2.8.9: - resolution: {integrity: sha512-CbD5Y2NKJKBXTOZ7z7Cc7vGlFPZkYjApA7ri9lH4iFwKV1X7MoZswh9gyRLetXYWImVX1BqIvP8KftulJg/wIA==} - cpu: [arm64] - os: [darwin] - - turbo-linux-64@2.8.9: - resolution: {integrity: sha512-OXC9HdCtsHvyH+5KUoH8ds+p5WU13vdif0OPbsFzZca4cUXMwKA3HWwUuCgQetk0iAE4cscXpi/t8A263n3VTg==} - cpu: [x64] - os: [linux] - - turbo-linux-arm64@2.8.9: - resolution: {integrity: sha512-yI5n8jNXiFA6+CxnXG0gO7h5ZF1+19K8uO3/kXPQmyl37AdiA7ehKJQOvf9OPAnmkGDHcF2HSCPltabERNRmug==} - cpu: [arm64] - os: [linux] - - turbo-windows-64@2.8.9: - resolution: {integrity: sha512-/OztzeGftJAg258M/9vK2ZCkUKUzqrWXJIikiD2pm8TlqHcIYUmepDbyZSDfOiUjMy6NzrLFahpNLnY7b5vNgg==} - cpu: [x64] - os: [win32] - - turbo-windows-arm64@2.8.9: - resolution: {integrity: sha512-xZ2VTwVTjIqpFZKN4UBxDHCPM3oJ2J5cpRzCBSmRpJ/Pn33wpiYjs+9FB2E03svKaD04/lSSLlEUej0UYsugfg==} - cpu: [arm64] - os: [win32] - - turbo@2.8.9: - resolution: {integrity: sha512-G+Mq8VVQAlpz/0HTsxiNNk/xywaHGl+dk1oiBREgOEVCCDjXInDlONWUn5srRnC9s5tdHTFD1bx1N19eR4hI+g==} + turbo@2.8.20: + resolution: {integrity: sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ==} hasBin: true type-check@0.4.0: @@ -6215,6 +6282,24 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@turbo/darwin-64@2.8.20': + optional: true + + '@turbo/darwin-arm64@2.8.20': + optional: true + + '@turbo/linux-64@2.8.20': + optional: true + + '@turbo/linux-arm64@2.8.20': + optional: true + + '@turbo/windows-64@2.8.20': + optional: true + + '@turbo/windows-arm64@2.8.20': + optional: true + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -6321,6 +6406,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) + '@typescript-eslint/types': 8.57.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -6331,6 +6425,15 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/scope-manager@8.57.1': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/visitor-keys': 8.57.1 + + '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) @@ -6347,6 +6450,8 @@ snapshots: '@typescript-eslint/types@7.18.0': {} + '@typescript-eslint/types@8.57.1': {} + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 5.62.0 @@ -6376,6 +6481,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.0) @@ -6402,6 +6522,17 @@ snapshots: - supports-color - typescript + '@typescript-eslint/utils@8.57.1(eslint@8.57.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.0) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + eslint: 8.57.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@5.62.0': dependencies: '@typescript-eslint/types': 5.62.0 @@ -6412,6 +6543,11 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@8.57.1': + dependencies: + '@typescript-eslint/types': 8.57.1 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -6532,6 +6668,17 @@ snapshots: tinyrainbow: 3.0.3 vitest: 4.0.18(@types/node@25.2.3)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) + '@vitest/eslint-plugin@1.6.12(eslint@8.57.0)(typescript@5.9.3)(vitest@4.0.18(@types/node@25.2.3)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))': + dependencies: + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/utils': 8.57.1(eslint@8.57.0)(typescript@5.9.3) + eslint: 8.57.0 + optionalDependencies: + typescript: 5.9.3 + vitest: 4.0.18(@types/node@25.2.3)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@vitest/expect@4.0.18': dependencies: '@standard-schema/spec': 1.1.0 @@ -7607,6 +7754,8 @@ snapshots: eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@5.0.1: {} + eslint@8.57.0: dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.0) @@ -8707,6 +8856,10 @@ snapshots: dependencies: brace-expansion: 5.0.2 + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.2 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -9239,7 +9392,7 @@ snapshots: dependencies: glob: 13.0.4 - rimraf@6.1.2: + rimraf@6.1.3: dependencies: glob: 13.0.4 package-json-from-dist: 1.0.1 @@ -9655,6 +9808,10 @@ snapshots: dependencies: typescript: 5.9.3 + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-interface-checker@0.1.13: {} tsconfig-paths@3.15.0: @@ -9674,32 +9831,14 @@ snapshots: tslib: 1.14.1 typescript: 5.9.3 - turbo-darwin-64@2.8.9: - optional: true - - turbo-darwin-arm64@2.8.9: - optional: true - - turbo-linux-64@2.8.9: - optional: true - - turbo-linux-arm64@2.8.9: - optional: true - - turbo-windows-64@2.8.9: - optional: true - - turbo-windows-arm64@2.8.9: - optional: true - - turbo@2.8.9: + turbo@2.8.20: optionalDependencies: - turbo-darwin-64: 2.8.9 - turbo-darwin-arm64: 2.8.9 - turbo-linux-64: 2.8.9 - turbo-linux-arm64: 2.8.9 - turbo-windows-64: 2.8.9 - turbo-windows-arm64: 2.8.9 + '@turbo/darwin-64': 2.8.20 + '@turbo/darwin-arm64': 2.8.20 + '@turbo/linux-64': 2.8.20 + '@turbo/linux-arm64': 2.8.20 + '@turbo/windows-64': 2.8.20 + '@turbo/windows-arm64': 2.8.20 type-check@0.4.0: dependencies: diff --git a/scripts/check-exact-deps.mjs b/scripts/check-exact-deps.mjs new file mode 100644 index 0000000..2f1ac25 --- /dev/null +++ b/scripts/check-exact-deps.mjs @@ -0,0 +1,116 @@ +import fs from "node:fs"; +import path from "node:path"; + +const rootDir = process.cwd(); +const dependencyFields = [ + "dependencies", + "devDependencies", + "optionalDependencies", +]; +const ignoredDirs = new Set([ + ".git", + ".turbo", + "coverage", + "dist", + "build", + "node_modules", +]); +const exactVersionPattern = + /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/; + +const manifestPaths = []; +collectPackageJsonPaths(rootDir, manifestPaths); + +const violations = []; +for (const manifestPath of manifestPaths) { + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + + for (const field of dependencyFields) { + const dependencies = manifest[field] ?? {}; + for (const [name, spec] of Object.entries(dependencies)) { + if (!isAllowedDependencySpec(spec)) { + violations.push(`${relativePath(manifestPath)} :: ${field}.${name} = ${spec}`); + } + } + } + + if (manifest.pnpm?.overrides) { + for (const [name, spec] of Object.entries(manifest.pnpm.overrides)) { + if (!isAllowedOverrideSpec(spec)) { + violations.push( + `${relativePath(manifestPath)} :: pnpm.overrides.${name} = ${spec}`, + ); + } + } + } +} + +if (violations.length > 0) { + console.error("Exact dependency versions are required."); + for (const violation of violations) { + console.error(`- ${violation}`); + } + process.exit(1); +} + +function collectPackageJsonPaths(dirPath, results) { + for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { + if (ignoredDirs.has(entry.name)) { + continue; + } + + const entryPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + collectPackageJsonPaths(entryPath, results); + continue; + } + + if (entry.isFile() && entry.name === "package.json") { + results.push(entryPath); + } + } +} + +function isAllowedDependencySpec(spec) { + return ( + isExactVersion(spec) || + spec === "workspace:*" || + isExactWorkspaceVersion(spec) || + spec.startsWith("file:") || + spec.startsWith("link:") || + spec.startsWith("portal:") + ); +} + +function isAllowedOverrideSpec(spec) { + return isExactVersion(spec); +} + +function isExactVersion(spec) { + if (typeof spec !== "string") { + return false; + } + + if (exactVersionPattern.test(spec)) { + return true; + } + + if (spec.startsWith("npm:")) { + const aliasIndex = spec.lastIndexOf("@"); + return aliasIndex > "npm:".length && isExactVersion(spec.slice(aliasIndex + 1)); + } + + return false; +} + +function isExactWorkspaceVersion(spec) { + if (!spec.startsWith("workspace:")) { + return false; + } + + return isExactVersion(spec.slice("workspace:".length)); +} + +function relativePath(filePath) { + return path.relative(rootDir, filePath) || "."; +} From 516685c3ad41bb18c3251fd35e1817a73b4129ad Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 20 Mar 2026 16:05:05 +0100 Subject: [PATCH 2/2] Fix react-native package lint issues --- .../src/components/formbricks.tsx | 4 +- .../src/components/survey-web-view.tsx | 12 +- packages/react-native/src/index.ts | 2 +- packages/react-native/src/lib/common/api.ts | 10 +- .../react-native/src/lib/common/config.ts | 3 +- .../src/lib/common/event-listeners.ts | 4 +- packages/react-native/src/lib/common/setup.ts | 141 +++++++++++------- .../react-native/src/lib/common/storage.ts | 15 +- .../src/lib/common/tests/api.test.ts | 2 +- .../src/lib/common/tests/config.test.ts | 2 +- .../src/lib/common/tests/setup.test.ts | 8 +- .../src/lib/common/tests/utils.test.ts | 2 +- packages/react-native/src/lib/common/utils.ts | 11 +- .../src/lib/environment/tests/state.test.ts | 40 ++--- .../react-native/src/lib/survey/action.ts | 2 +- .../src/lib/survey/tests/action.test.ts | 2 +- .../src/lib/survey/tests/store.test.ts | 2 +- .../react-native/src/lib/user/attribute.ts | 10 +- .../src/lib/user/tests/state.test.ts | 8 +- .../src/lib/user/tests/update.test.ts | 20 +-- .../react-native/src/lib/user/update-queue.ts | 4 +- packages/react-native/src/lib/user/user.ts | 2 +- .../react-native/src/types/action-class.ts | 4 +- packages/react-native/src/types/api.ts | 4 +- packages/react-native/src/types/config.ts | 4 +- packages/react-native/src/types/project.ts | 4 +- packages/react-native/src/types/survey.ts | 8 +- 27 files changed, 192 insertions(+), 138 deletions(-) diff --git a/packages/react-native/src/components/formbricks.tsx b/packages/react-native/src/components/formbricks.tsx index 877f28e..17a866a 100644 --- a/packages/react-native/src/components/formbricks.tsx +++ b/packages/react-native/src/components/formbricks.tsx @@ -1,9 +1,9 @@ +import React, { useCallback, useEffect, useSyncExternalStore } from "react"; +import { View } from "react-native"; import { SurveyWebView } from "@/components/survey-web-view"; import { Logger } from "@/lib/common/logger"; import { setup } from "@/lib/common/setup"; import { SurveyStore } from "@/lib/survey/store"; -import React, { useCallback, useEffect, useSyncExternalStore } from "react"; -import { View } from "react-native"; interface FormbricksProps { appUrl: string; diff --git a/packages/react-native/src/components/survey-web-view.tsx b/packages/react-native/src/components/survey-web-view.tsx index 353adac..5b4ccab 100644 --- a/packages/react-native/src/components/survey-web-view.tsx +++ b/packages/react-native/src/components/survey-web-view.tsx @@ -1,13 +1,13 @@ /* eslint-disable no-console -- debugging*/ +import React, { type JSX, useEffect, useRef, useState } from "react"; +import { KeyboardAvoidingView, Modal, View, StyleSheet } from "react-native"; +import { WebView, type WebViewMessageEvent } from "react-native-webview"; import { RNConfig } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { filterSurveys, getLanguageCode, getStyling } from "@/lib/common/utils"; import { SurveyStore } from "@/lib/survey/store"; import { type TUserState, ZJsRNWebViewOnMessageData } from "@/types/config"; import type { TSurvey, SurveyContainerProps } from "@/types/survey"; -import React, { type JSX, useEffect, useRef, useState } from "react"; -import { KeyboardAvoidingView, Modal, View, StyleSheet } from "react-native"; -import { WebView, type WebViewMessageEvent } from "react-native-webview"; const logger = Logger.getInstance(); logger.configure({ logLevel: "debug" }); @@ -21,7 +21,7 @@ interface SurveyWebViewProps { export function SurveyWebView( props: SurveyWebViewProps -): JSX.Element | undefined { +): JSX.Element | null { const webViewRef = useRef(null); const [isSurveyRunning, setIsSurveyRunning] = useState(false); const [showSurvey, setShowSurvey] = useState(false); @@ -29,7 +29,7 @@ export function SurveyWebView( const [languageCode, setLanguageCode] = useState("default"); useEffect(() => { - const fetchConfig = async () => { + const fetchConfig = async (): Promise => { const config = await RNConfig.getInstance(); setAppConfig(config); }; @@ -87,7 +87,7 @@ export function SurveyWebView( }, [props.survey.delay, isSurveyRunning, props.survey.name]); if (!appConfig) { - return; + return null; } const project = appConfig.get().environment.data.project; diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index bfc7105..84a6d40 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -38,4 +38,4 @@ export const logout = async (): Promise => { await queue.wait(); }; -export { Formbricks as default } from "@/components/formbricks"; +export { Formbricks } from "@/components/formbricks"; diff --git a/packages/react-native/src/lib/common/api.ts b/packages/react-native/src/lib/common/api.ts index 1a51f52..76dff1b 100644 --- a/packages/react-native/src/lib/common/api.ts +++ b/packages/react-native/src/lib/common/api.ts @@ -1,11 +1,11 @@ import { wrapThrowsAsync } from "@/lib/common/utils"; import { - ApiResponse, - ApiSuccessResponse, - CreateOrUpdateUserResponse, + type ApiResponse, + type ApiSuccessResponse, + type CreateOrUpdateUserResponse, } from "@/types/api"; -import { TEnvironmentState } from "@/types/config"; -import { ApiErrorResponse, Result, err, ok } from "@/types/error"; +import { type TEnvironmentState } from "@/types/config"; +import { type ApiErrorResponse, type Result, err, ok } from "@/types/error"; export const makeRequest = async ( appUrl: string, diff --git a/packages/react-native/src/lib/common/config.ts b/packages/react-native/src/lib/common/config.ts index a81c796..89e6192 100644 --- a/packages/react-native/src/lib/common/config.ts +++ b/packages/react-native/src/lib/common/config.ts @@ -11,6 +11,7 @@ export class RNConfig { private config: TConfig | null = null; + // eslint-disable-next-line @typescript-eslint/no-empty-function -- singleton constructor private constructor() {} public async init(): Promise { @@ -24,7 +25,7 @@ export class RNConfig { } } - static async getInstance(): Promise { + public static async getInstance(): Promise { RNConfig.instance ??= new RNConfig(); await RNConfig.instance.init(); return RNConfig.instance; diff --git a/packages/react-native/src/lib/common/event-listeners.ts b/packages/react-native/src/lib/common/event-listeners.ts index f2fea20..fc5426a 100644 --- a/packages/react-native/src/lib/common/event-listeners.ts +++ b/packages/react-native/src/lib/common/event-listeners.ts @@ -7,8 +7,8 @@ import { addUserStateExpiryCheckListener, clearUserStateExpiryCheckListener } fr let areRemoveEventListenersAdded = false; export const addEventListeners = (): void => { - addEnvironmentStateExpiryCheckListener(); - addUserStateExpiryCheckListener(); + void addEnvironmentStateExpiryCheckListener(); + void addUserStateExpiryCheckListener(); }; export const addCleanupEventListeners = (): void => { diff --git a/packages/react-native/src/lib/common/setup.ts b/packages/react-native/src/lib/common/setup.ts index d1b1149..24ea5df 100644 --- a/packages/react-native/src/lib/common/setup.ts +++ b/packages/react-native/src/lib/common/setup.ts @@ -50,10 +50,10 @@ export const migrateUserStateAddContactId = async (): Promise<{ return { changed: false }; } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- data could be undefined + if ( - !existingConfig.user?.data?.contactId && - existingConfig.user?.data?.userId + !existingConfig.user?.data.contactId && + existingConfig.user?.data.userId ) { return { changed: true }; } @@ -63,7 +63,9 @@ export const migrateUserStateAddContactId = async (): Promise<{ }; // Helper: Handle missing field error -function handleMissingField(field: string) { +function handleMissingField( + field: string +): Result { const logger = Logger.getInstance(); logger.debug(`No ${field} provided`); return err({ @@ -91,21 +93,21 @@ async function syncEnvironmentStateIfExpired( if (environmentStateResponse.ok) { return ok(environmentStateResponse.data); - } else { - logger.error( - `Error fetching environment state: ${environmentStateResponse.error.code} - ${environmentStateResponse.error.responseMessage ?? ""}` - ); - - return err({ - code: "network_error", - message: "Error fetching environment state", - status: 500, - url: new URL( - `${configInput.appUrl}/api/v1/client/${configInput.environmentId}/environment` - ), - responseMessage: environmentStateResponse.error.message, - }); } + + logger.error( + `Error fetching environment state: ${environmentStateResponse.error.code} - ${environmentStateResponse.error.responseMessage ?? ""}` + ); + + return err({ + code: "network_error", + message: "Error fetching environment state", + status: 500, + url: new URL( + `${configInput.appUrl}/api/v1/client/${configInput.environmentId}/environment` + ), + responseMessage: environmentStateResponse.error.message, + }); } // Helper: Sync user state if expired @@ -125,7 +127,7 @@ async function syncUserStateIfExpired( logger.debug("Person state expired. Syncing."); - if (userState?.data?.userId) { + if (userState?.data.userId) { const updatesResponse = await sendUpdatesToBackend({ appUrl: configInput.appUrl, environmentId: configInput.environmentId, @@ -135,23 +137,23 @@ async function syncUserStateIfExpired( }); if (updatesResponse.ok) { return ok(updatesResponse.data.state); - } else { - logger.error( - `Error updating user state: ${updatesResponse.error.code} - ${updatesResponse.error.responseMessage ?? ""}` - ); - return err({ - code: "network_error", - message: "Error updating user state", - status: 500, - url: new URL( - `${configInput.appUrl}/api/v1/client/${configInput.environmentId}/update/contacts/${userState.data.userId}` - ), - responseMessage: "Unknown error", - } as const); } - } else { - return ok(DEFAULT_USER_STATE_NO_USER_ID); + + logger.error( + `Error updating user state: ${updatesResponse.error.code} - ${updatesResponse.error.responseMessage ?? ""}` + ); + return err({ + code: "network_error", + message: "Error updating user state", + status: 500, + url: new URL( + `${configInput.appUrl}/api/v1/client/${configInput.environmentId}/update/contacts/${userState.data.userId}` + ), + responseMessage: "Unknown error", + } as const); } + + return ok(DEFAULT_USER_STATE_NO_USER_ID); } // Helper: Update app config with synced states @@ -199,22 +201,35 @@ const createNewConfigAndSync = async ( appUrl: configInput.appUrl, environmentId: configInput.environmentId, }); - if (!environmentStateResponse.ok) { - throw environmentStateResponse.error; + + if (environmentStateResponse.ok) { + const personState = DEFAULT_USER_STATE_NO_USER_ID; + const environmentState = environmentStateResponse.data; + const filteredSurveys = filterSurveys(environmentState, personState); + appConfig.update({ + appUrl: configInput.appUrl, + environmentId: configInput.environmentId, + user: personState, + environment: environmentState, + filteredSurveys, + }); + return; } - const personState = DEFAULT_USER_STATE_NO_USER_ID; - const environmentState = environmentStateResponse.data; - const filteredSurveys = filterSurveys(environmentState, personState); - appConfig.update({ - appUrl: configInput.appUrl, - environmentId: configInput.environmentId, - user: personState, - environment: environmentState, - filteredSurveys, + + await handleErrorOnFirstSetup({ + code: environmentStateResponse.error.code, + responseMessage: + environmentStateResponse.error.responseMessage ?? + environmentStateResponse.error.message, }); - } catch (e) { + } catch (e: unknown) { + const setupError = normalizeSetupError(e); await handleErrorOnFirstSetup( - e as { code: string; responseMessage: string } + { + code: setupError.code ?? "network_error", + responseMessage: + setupError.responseMessage ?? setupError.message ?? "Unknown error", + } ); } }; @@ -260,10 +275,10 @@ const finalizeSetup = (): void => { }; // Helper: Load existing config -const loadExistingConfig = async ( +const loadExistingConfig = ( appConfig: RNConfig, logger: ReturnType -): Promise => { +): TConfig | undefined => { let existingConfig: TConfig | undefined; try { existingConfig = appConfig.get(); @@ -294,7 +309,7 @@ export const setup = async ( return okVoid(); } - const existingConfig = await loadExistingConfig(appConfig, logger); + const existingConfig = loadExistingConfig(appConfig, logger); if (shouldReturnEarlyForErrorState(existingConfig, logger)) { return okVoid(); } @@ -369,8 +384,6 @@ export const checkSetup = (): Result => { return okVoid(); }; - -// eslint-disable-next-line @typescript-eslint/require-await -- disabled for now export const tearDown = async (): Promise => { const logger = Logger.getInstance(); const appConfig = await RNConfig.getInstance(); @@ -425,3 +438,27 @@ export const handleErrorOnFirstSetup = async (e: { throw new Error("Could not set up formbricks"); }; + +const normalizeSetupError = ( + error: unknown +): Partial<{ + code: string; + responseMessage: string; + message: string; +}> => { + if (typeof error !== "object" || error === null) { + return {}; + } + + const candidate = error as Record; + + return { + code: typeof candidate.code === "string" ? candidate.code : undefined, + responseMessage: + typeof candidate.responseMessage === "string" + ? candidate.responseMessage + : undefined, + message: + typeof candidate.message === "string" ? candidate.message : undefined, + }; +}; diff --git a/packages/react-native/src/lib/common/storage.ts b/packages/react-native/src/lib/common/storage.ts index 8cd6683..c4a6e67 100644 --- a/packages/react-native/src/lib/common/storage.ts +++ b/packages/react-native/src/lib/common/storage.ts @@ -1,7 +1,14 @@ -import AsyncStorageModule from "@react-native-async-storage/async-storage"; +import AsyncStorageModule, { + type AsyncStorageStatic, +} from "@react-native-async-storage/async-storage"; -const AsyncStorage = - // @ts-expect-error: Some bundlers put the module on .default - AsyncStorageModule.default ?? AsyncStorageModule; +type AsyncStorageModuleWithDefault = AsyncStorageStatic & { + default?: AsyncStorageStatic; +}; + +const asyncStorageModule = AsyncStorageModule as AsyncStorageModuleWithDefault; + +const AsyncStorage: AsyncStorageStatic = + asyncStorageModule.default ?? asyncStorageModule; export { AsyncStorage }; diff --git a/packages/react-native/src/lib/common/tests/api.test.ts b/packages/react-native/src/lib/common/tests/api.test.ts index b0332b9..0469e9c 100644 --- a/packages/react-native/src/lib/common/tests/api.test.ts +++ b/packages/react-native/src/lib/common/tests/api.test.ts @@ -1,7 +1,7 @@ // api.test.ts +import { beforeEach, describe, expect, test, vi } from "vitest"; import { ApiClient, makeRequest } from "@/lib/common/api"; import type { TEnvironmentState } from "@/types/config"; -import { beforeEach, describe, expect, test, vi } from "vitest"; // Mock fetch const mockFetch = vi.fn(); diff --git a/packages/react-native/src/lib/common/tests/config.test.ts b/packages/react-native/src/lib/common/tests/config.test.ts index 04f13dc..57813d4 100644 --- a/packages/react-native/src/lib/common/tests/config.test.ts +++ b/packages/react-native/src/lib/common/tests/config.test.ts @@ -1,9 +1,9 @@ // config.test.ts import AsyncStorage from "@react-native-async-storage/async-storage"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { mockConfig } from "./__mocks__/config.mock"; import { RNConfig, RN_ASYNC_STORAGE_KEY } from "@/lib/common/config"; import type { TConfig, TConfigUpdateInput } from "@/types/config"; +import { mockConfig } from "./__mocks__/config.mock"; // Define mocks outside of any describe block diff --git a/packages/react-native/src/lib/common/tests/setup.test.ts b/packages/react-native/src/lib/common/tests/setup.test.ts index 3aa887c..117b28e 100644 --- a/packages/react-native/src/lib/common/tests/setup.test.ts +++ b/packages/react-native/src/lib/common/tests/setup.test.ts @@ -9,6 +9,7 @@ import { test, vi, } from "vitest"; +import type * as CommonUtilsModule from "@/lib/common/utils"; import { RNConfig, RN_ASYNC_STORAGE_KEY } from "@/lib/common/config"; import { addCleanupEventListeners, @@ -72,9 +73,12 @@ vi.mock("@/lib/environment/state", () => ({ })); // 6) Mock filterSurveys -vi.mock("@/lib/common/utils", async (importOriginal) => { +vi.mock("@/lib/common/utils", async () => { + const actual = await vi.importActual( + "@/lib/common/utils" + ); return { - ...(await importOriginal()), + ...actual, filterSurveys: vi.fn(), isNowExpired: vi.fn(), }; diff --git a/packages/react-native/src/lib/common/tests/utils.test.ts b/packages/react-native/src/lib/common/tests/utils.test.ts index 9b0d04c..c8e0710 100644 --- a/packages/react-native/src/lib/common/tests/utils.test.ts +++ b/packages/react-native/src/lib/common/tests/utils.test.ts @@ -19,7 +19,7 @@ import type { TEnvironmentStateProject, TUserState, } from "@/types/config"; -import { TSurvey } from "@/types/survey"; +import { type TSurvey } from "@/types/survey"; const mockSurveyId1 = "e3kxlpnzmdp84op9qzxl9olj"; const mockSurveyId2 = "qo9rwjmms42hoy3k85fp8vgu"; diff --git a/packages/react-native/src/lib/common/utils.ts b/packages/react-native/src/lib/common/utils.ts index 342d029..b25401c 100644 --- a/packages/react-native/src/lib/common/utils.ts +++ b/packages/react-native/src/lib/common/utils.ts @@ -111,7 +111,10 @@ export const filterSurveys = ( if (!userId) { // exclude surveys that have a segment with filters return filteredSurveys.filter((survey) => { - const segmentFiltersLength = survey.segment?.filters?.length ?? 0; + const segmentFilters = survey.segment?.filters as unknown; + const segmentFiltersLength = Array.isArray(segmentFilters) + ? segmentFilters.length + : 0; return segmentFiltersLength === 0; }); } @@ -191,5 +194,9 @@ export const isNowExpired = (expirationDate: Date): boolean => { }; export const delayedResult = async (value: T, ms: number): Promise => { - return new Promise((resolve) => setTimeout(() => resolve(value), ms)); + return new Promise((resolve) => { + setTimeout(() => { + resolve(value); + }, ms); + }); }; diff --git a/packages/react-native/src/lib/environment/tests/state.test.ts b/packages/react-native/src/lib/environment/tests/state.test.ts index 65d8417..62373cd 100644 --- a/packages/react-native/src/lib/environment/tests/state.test.ts +++ b/packages/react-native/src/lib/environment/tests/state.test.ts @@ -1,14 +1,4 @@ // state.test.ts -import { ApiClient } from "@/lib/common/api"; -import { RNConfig } from "@/lib/common/config"; -import { Logger } from "@/lib/common/logger"; -import { filterSurveys } from "@/lib/common/utils"; -import { - addEnvironmentStateExpiryCheckListener, - clearEnvironmentStateExpiryCheckListener, - fetchEnvironmentState, -} from "@/lib/environment/state"; -import type { TEnvironmentState } from "@/types/config"; import { type Mock, type MockInstance, @@ -19,10 +9,20 @@ import { test, vi, } from "vitest"; +import { ApiClient } from "@/lib/common/api"; +import { RNConfig } from "@/lib/common/config"; +import { Logger } from "@/lib/common/logger"; +import { filterSurveys } from "@/lib/common/utils"; +import { + addEnvironmentStateExpiryCheckListener, + clearEnvironmentStateExpiryCheckListener, + fetchEnvironmentState, +} from "@/lib/environment/state"; +import type { TEnvironmentState } from "@/types/config"; // Mock the FormbricksAPI so we can control environment.getState vi.mock("@/lib/common/api", () => ({ - ApiClient: vi.fn().mockImplementation(function () { + ApiClient: vi.fn().mockImplementation(() => { return { getEnvironmentState: vi.fn() }; }), })); @@ -70,7 +70,7 @@ describe("environment/state.ts", () => { describe("fetchEnvironmentState()", () => { test("returns ok(...) with environment state", async () => { // Setup mock - (ApiClient as unknown as Mock).mockImplementationOnce(function () { + (ApiClient as unknown as Mock).mockImplementationOnce(() => { return { getEnvironmentState: vi.fn().mockResolvedValue({ ok: true, @@ -103,7 +103,7 @@ describe("environment/state.ts", () => { message: "Access denied", }; - (ApiClient as unknown as Mock).mockImplementationOnce(function () { + (ApiClient as unknown as Mock).mockImplementationOnce(() => { return { getEnvironmentState: vi.fn().mockResolvedValue({ ok: false, @@ -131,7 +131,7 @@ describe("environment/state.ts", () => { responseMessage: "Network fail", }; - (ApiClient as unknown as Mock).mockImplementationOnce(function () { + (ApiClient as unknown as Mock).mockImplementationOnce(() => { return { getEnvironmentState: vi.fn().mockRejectedValue(mockNetworkError), }; @@ -208,7 +208,7 @@ describe("environment/state.ts", () => { mockRNConfig.mockReturnValue(mockConfig as unknown as Promise); - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { getEnvironmentState: vi.fn().mockResolvedValue({ ok: true, @@ -220,7 +220,7 @@ describe("environment/state.ts", () => { (filterSurveys as Mock).mockReturnValue([]); // Add listener - addEnvironmentStateExpiryCheckListener(); + await addEnvironmentStateExpiryCheckListener(); // Fast-forward time await vi.advanceTimersByTimeAsync(1000 * 60); @@ -244,7 +244,7 @@ describe("environment/state.ts", () => { mockRNConfig.mockReturnValue(mockConfig as unknown as Promise); // Mock API to throw an error - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { getEnvironmentState: vi .fn() @@ -252,7 +252,7 @@ describe("environment/state.ts", () => { }; }); - addEnvironmentStateExpiryCheckListener(); + await addEnvironmentStateExpiryCheckListener(); // Fast-forward time await vi.advanceTimersByTimeAsync(1000 * 60); @@ -276,13 +276,13 @@ describe("environment/state.ts", () => { mockRNConfig.mockReturnValue(mockConfig as unknown as Promise); - const apiMock = vi.fn().mockImplementation(function () { + const apiMock = vi.fn().mockImplementation(() => { return { getEnvironmentState: vi.fn() }; }); (ApiClient as Mock).mockImplementation(apiMock); - addEnvironmentStateExpiryCheckListener(); + await addEnvironmentStateExpiryCheckListener(); // Fast-forward time by less than expiry await vi.advanceTimersByTimeAsync(1000 * 60); diff --git a/packages/react-native/src/lib/survey/action.ts b/packages/react-native/src/lib/survey/action.ts index 0da6222..5d6a125 100644 --- a/packages/react-native/src/lib/survey/action.ts +++ b/packages/react-native/src/lib/survey/action.ts @@ -1,3 +1,4 @@ +import { fetch } from "@react-native-community/netinfo"; import { RNConfig } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { shouldDisplayBasedOnPercentage } from "@/lib/common/utils"; @@ -10,7 +11,6 @@ import { err, okVoid, } from "@/types/error"; -import { fetch } from "@react-native-community/netinfo"; /** * Triggers the display of a survey if it meets the display percentage criteria diff --git a/packages/react-native/src/lib/survey/tests/action.test.ts b/packages/react-native/src/lib/survey/tests/action.test.ts index 9950f27..71caa85 100644 --- a/packages/react-native/src/lib/survey/tests/action.test.ts +++ b/packages/react-native/src/lib/survey/tests/action.test.ts @@ -1,9 +1,9 @@ +import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; import { RNConfig } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { shouldDisplayBasedOnPercentage } from "@/lib/common/utils"; import { track, trackAction, triggerSurvey } from "@/lib/survey/action"; import { SurveyStore } from "@/lib/survey/store"; -import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; import type { TSurvey } from "@/types/survey"; vi.mock("@/lib/common/config", () => ({ diff --git a/packages/react-native/src/lib/survey/tests/store.test.ts b/packages/react-native/src/lib/survey/tests/store.test.ts index 82e29d3..42773ee 100644 --- a/packages/react-native/src/lib/survey/tests/store.test.ts +++ b/packages/react-native/src/lib/survey/tests/store.test.ts @@ -1,10 +1,10 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; import { mockSurveyId, mockSurveyName, } from "@/lib/survey/tests/__mocks__/store.mock"; import { SurveyStore } from "@/lib/survey/store"; import type { TSurvey } from "@/types/survey"; -import { beforeEach, describe, expect, test, vi } from "vitest"; describe("SurveyStore", () => { let store: SurveyStore; diff --git a/packages/react-native/src/lib/user/attribute.ts b/packages/react-native/src/lib/user/attribute.ts index 4e229f8..b38de4f 100644 --- a/packages/react-native/src/lib/user/attribute.ts +++ b/packages/react-native/src/lib/user/attribute.ts @@ -5,10 +5,10 @@ import { type NetworkError, type Result, okVoid } from "@/types/error"; * Sets attributes on the current user/contact. * * Attribute types are determined by the JavaScript value type: - * - String values -> string attribute - * - Number values -> number attribute - * - Date objects -> date attribute (converted to ISO string) - * - ISO 8601 date strings -> date attribute + * - String values \> string attribute + * - Number values \> number attribute + * - Date objects \> date attribute (converted to ISO string) + * - ISO 8601 date strings \> date attribute * * On first write to a new attribute, the type is set based on the JS value type. * On subsequent writes, the value must match the existing attribute type. @@ -17,7 +17,7 @@ import { type NetworkError, type Result, okVoid } from "@/types/error"; */ export const setAttributes = async ( attributes: Record - // eslint-disable-next-line @typescript-eslint/require-await -- we want to use promises here + ): Promise> => { // Normalize values: convert Date to ISO string, preserve numbers as numbers const normalizedAttributes: Record = {}; diff --git a/packages/react-native/src/lib/user/tests/state.test.ts b/packages/react-native/src/lib/user/tests/state.test.ts index d9a240f..ef3c729 100644 --- a/packages/react-native/src/lib/user/tests/state.test.ts +++ b/packages/react-native/src/lib/user/tests/state.test.ts @@ -62,7 +62,7 @@ describe("User State Expiry Check Listener", () => { }); }); - test("should not update user state expiry if userId does not exist", () => { + test("should not update user state expiry if userId does not exist", async () => { const mockConfig = { get: vi.fn().mockReturnValue({ user: { data: { userId: null } }, @@ -72,7 +72,7 @@ describe("User State Expiry Check Listener", () => { mockRNConfig.mockReturnValue(mockConfig as unknown as Promise); - addUserStateExpiryCheckListener(); + await addUserStateExpiryCheckListener(); vi.advanceTimersByTime(60_000); // Fast-forward 1 minute expect(mockConfig.update).not.toHaveBeenCalled(); // Ensures no update when no userId @@ -96,7 +96,7 @@ describe("User State Expiry Check Listener", () => { expect(mockConfig.update).toHaveBeenCalledTimes(1); }); - test("should clear interval when clearUserStateExpiryCheckListener is called", () => { + test("should clear interval when clearUserStateExpiryCheckListener is called", async () => { const mockConfig = { get: vi.fn(), update: vi.fn(), @@ -104,7 +104,7 @@ describe("User State Expiry Check Listener", () => { mockRNConfig.mockReturnValue(mockConfig as unknown as Promise); - addUserStateExpiryCheckListener(); + await addUserStateExpiryCheckListener(); clearUserStateExpiryCheckListener(); vi.advanceTimersByTime(60_000); // Fast-forward 1 minute diff --git a/packages/react-native/src/lib/user/tests/update.test.ts b/packages/react-native/src/lib/user/tests/update.test.ts index 2c360a0..9b3ee11 100644 --- a/packages/react-native/src/lib/user/tests/update.test.ts +++ b/packages/react-native/src/lib/user/tests/update.test.ts @@ -1,3 +1,4 @@ +import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; import { mockAppUrl, mockAttributes, @@ -9,7 +10,6 @@ import { RNConfig } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { sendUpdates, sendUpdatesToBackend } from "@/lib/user/update"; import { type TUpdates } from "@/types/config"; -import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("@/lib/common/config", () => ({ RNConfig: { @@ -34,7 +34,7 @@ vi.mock("@/lib/common/utils", () => ({ })); vi.mock("@/lib/common/api", () => ({ - ApiClient: vi.fn().mockImplementation(function () { + ApiClient: vi.fn().mockImplementation(() => { return { createOrUpdateUser: vi.fn() }; }), })); @@ -57,7 +57,7 @@ describe("sendUpdatesToBackend", () => { }, }; - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { createOrUpdateUser: vi.fn().mockResolvedValue(mockResponse) }; }); @@ -82,7 +82,7 @@ describe("sendUpdatesToBackend", () => { attributes: mockAttributes, }; - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { createOrUpdateUser: vi.fn().mockResolvedValue({ ok: false, @@ -116,7 +116,7 @@ describe("sendUpdatesToBackend", () => { attributes: { plan: "premium" }, }; - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { createOrUpdateUser: vi .fn() @@ -169,7 +169,7 @@ describe("sendUpdates", () => { }, }; - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { createOrUpdateUser: vi.fn().mockResolvedValue(mockResponse) }; }); @@ -193,7 +193,7 @@ describe("sendUpdates", () => { }, }; - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { createOrUpdateUser: vi.fn().mockResolvedValue(mockErrorResponse), }; @@ -224,7 +224,7 @@ describe("sendUpdates", () => { }, }; - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { createOrUpdateUser: vi.fn().mockResolvedValue(mockResponse) }; }); @@ -261,7 +261,7 @@ describe("sendUpdates", () => { }, }; - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { createOrUpdateUser: vi.fn().mockResolvedValue(mockResponse) }; }); @@ -278,7 +278,7 @@ describe("sendUpdates", () => { }); test("handles unexpected errors", async () => { - (ApiClient as Mock).mockImplementation(function () { + (ApiClient as Mock).mockImplementation(() => { return { createOrUpdateUser: vi .fn() diff --git a/packages/react-native/src/lib/user/update-queue.ts b/packages/react-native/src/lib/user/update-queue.ts index ac697e9..4052aab 100644 --- a/packages/react-native/src/lib/user/update-queue.ts +++ b/packages/react-native/src/lib/user/update-queue.ts @@ -128,9 +128,7 @@ export class UpdateQueue { if (!result.ok) { const err = result.error; - logger.error( - `Failed to send updates: ${err?.message ?? "unknown error"}`, - ); + logger.error(`Failed to send updates: ${err.message}`); return; } diff --git a/packages/react-native/src/lib/user/user.ts b/packages/react-native/src/lib/user/user.ts index 13f9f52..25bb69d 100644 --- a/packages/react-native/src/lib/user/user.ts +++ b/packages/react-native/src/lib/user/user.ts @@ -4,7 +4,7 @@ import { tearDown } from "@/lib/common/setup"; import { UpdateQueue } from "@/lib/user/update-queue"; import { type ApiErrorResponse, type Result, okVoid } from "@/types/error"; -// eslint-disable-next-line @typescript-eslint/require-await -- we want to use promises here + export const setUserId = async ( userId: string ): Promise> => { diff --git a/packages/react-native/src/types/action-class.ts b/packages/react-native/src/types/action-class.ts index a4d3ea0..7667a9f 100644 --- a/packages/react-native/src/types/action-class.ts +++ b/packages/react-native/src/types/action-class.ts @@ -1,4 +1,4 @@ -export type TActionClass = { +export interface TActionClass { id: string; createdAt: Date; updatedAt: Date; @@ -39,4 +39,4 @@ export type TActionClass = { } | null; environmentId: string; -}; +} diff --git a/packages/react-native/src/types/api.ts b/packages/react-native/src/types/api.ts index b53741f..b0cd9ad 100644 --- a/packages/react-native/src/types/api.ts +++ b/packages/react-native/src/types/api.ts @@ -1,5 +1,5 @@ -import { TUserState } from "@/types/config"; -import { ApiErrorResponse } from "@/types/error"; +import { type TUserState } from "@/types/config"; +import { type ApiErrorResponse } from "@/types/error"; export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; diff --git a/packages/react-native/src/types/config.ts b/packages/react-native/src/types/config.ts index 9639fee..b5a3683 100644 --- a/packages/react-native/src/types/config.ts +++ b/packages/react-native/src/types/config.ts @@ -1,7 +1,7 @@ -/* eslint-disable import/no-extraneous-dependencies -- required for Prisma types */ + +import { z } from "zod"; import { type TResponseUpdate } from "@/types/response"; import { type TFileUploadParams } from "@/types/storage"; -import { z } from "zod"; import { type TActionClass } from "./action-class"; import type { TProject, TProjectStyling } from "./project"; import type { TSurvey } from "./survey"; diff --git a/packages/react-native/src/types/project.ts b/packages/react-native/src/types/project.ts index 6acc4e6..711b8de 100644 --- a/packages/react-native/src/types/project.ts +++ b/packages/react-native/src/types/project.ts @@ -1,7 +1,7 @@ import type { TOverlay } from "./common"; import type { TBaseStyling } from "./styling"; -export type TProject = { +export interface TProject { id: string; createdAt: Date; updatedAt: Date; @@ -26,7 +26,7 @@ export type TProject = { url?: string; bgColor?: string; } | null; -}; +} export interface TProjectStyling extends TBaseStyling { allowStyleOverwrite: boolean; diff --git a/packages/react-native/src/types/survey.ts b/packages/react-native/src/types/survey.ts index 09d7660..f61c5d0 100644 --- a/packages/react-native/src/types/survey.ts +++ b/packages/react-native/src/types/survey.ts @@ -3,7 +3,7 @@ import type { TFileUploadParams, TUploadFileConfig } from "@/types/storage"; import type { TOverlay } from "./common"; import type { TProjectStyling } from "./project"; -export type TJsFileUploadParams = { +export interface TJsFileUploadParams { file: { type: string; name: string; @@ -14,7 +14,7 @@ export type TJsFileUploadParams = { folder?: string; allowedExtensions?: string[]; }; -}; +} export interface SurveyBaseProps { survey: TSurvey; @@ -81,7 +81,7 @@ export interface SurveyContainerProps isWebEnvironment?: boolean; } -export type TSurvey = { +export interface TSurvey { id: string; name: string; welcomeCard: { @@ -318,4 +318,4 @@ export type TSurvey = { } | null; overwriteThemeStyling?: boolean | null; }; -}; +}