diff --git a/.changeset/ui-test-jest-dom-types.md b/.changeset/ui-test-jest-dom-types.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/ui-test-jest-dom-types.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/eslint.config.mjs b/eslint.config.mjs index 0edfe0d6b60..b1e6bdcc4fe 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,3 +1,6 @@ +import fs from 'node:fs'; +import path from 'node:path'; + import eslint from '@eslint/js'; import configPrettier from 'eslint-config-prettier'; import configTurbo from 'eslint-config-turbo/flat'; @@ -17,10 +20,43 @@ import tseslint from 'typescript-eslint'; import { CUSTOM_BLOCK_TAGS, CUSTOM_MODIFIER_TAGS } from './.typedoc/custom-tags.mjs'; +const REPO_ROOT = import.meta.dirname; const ECMA_VERSION = 2021, JAVASCRIPT_FILES = ['**/*.cjs', '**/*.js', '**/*.jsx', '**/*.mjs'], TEST_FILES = ['**/*.test.js', '**/*.test.jsx', '**/*.test.ts', '**/*.test.tsx', '**/test/**', '**/__tests__/**'], - TYPESCRIPT_FILES = ['**/*.cts', '**/*.mts', '**/*.ts', '**/*.tsx']; + TYPESCRIPT_FILES = ['**/*.cts', '**/*.mts', '**/*.ts', '**/*.tsx'], + // turbo lint runs `eslint src` from each package cwd; these must be repo-absolute + IMPORT_RESOLVER_TSCONFIGS = [`${REPO_ROOT}/integration/tsconfig.json`], + IMPORT_RESOLVER_TSCONFIG_NAMES = ['tsconfig.json', 'tsconfig.src.json', 'tsconfig.test.json', 'tsconfig.mosaic.json']; + +// Each package must see only its own tsconfigs. Several packages define `@/*`, and a +// shared project list lets the resolver apply clerk-js's alias to UI or shared files. +const PACKAGE_IMPORT_RESOLVER_CONFIGS = fs + .readdirSync(path.join(REPO_ROOT, 'packages'), { withFileTypes: true }) + .filter(entry => entry.isDirectory() && fs.existsSync(path.join(REPO_ROOT, 'packages', entry.name, 'package.json'))) + .flatMap(entry => { + const project = IMPORT_RESOLVER_TSCONFIG_NAMES.map(name => + path.join(REPO_ROOT, 'packages', entry.name, name), + ).filter(file => fs.existsSync(file)); + if (project.length === 0) { + return []; + } + return [ + { + name: `packages/${entry.name}/import-resolver`, + files: [`packages/${entry.name}/**/*.{ts,tsx,js,jsx,mts,cts}`], + settings: { + 'import/resolver': { + node: true, + typescript: { + alwaysTryTypes: true, + project, + }, + }, + }, + }, + ]; + }); const noNavigateUseClerk = { meta: { @@ -339,7 +375,7 @@ export default tseslint.config([ node: true, typescript: { alwaysTryTypes: true, - project: ['packages/*/tsconfig.json', 'integration/tsconfig.json'], + project: IMPORT_RESOLVER_TSCONFIGS, }, }, }, @@ -517,6 +553,7 @@ export default tseslint.config([ 'react-hooks/rules-of-hooks': 'warn', }, }, + ...PACKAGE_IMPORT_RESOLVER_CONFIGS, { name: 'packages/clerk-js', files: ['packages/clerk-js/src/ui/**/*'], diff --git a/packages/ui/package.json b/packages/ui/package.json index 82e3ae346a4..47c5a287918 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -100,11 +100,11 @@ "lint": "eslint src", "lint:attw": "attw --pack . --exclude-entrypoints themes/shadcn.css --profile esm-only", "lint:publint": "publint", - "showerrors": "tsc", + "showerrors": "tsc -p tsconfig.src.json", "test": "vitest run", "test:ci": "vitest run --maxWorkers=70%", "test:coverage": "vitest --collectCoverage && open coverage/lcov-report/index.html", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit -p tsconfig.src.json" }, "dependencies": { "@clerk/localizations": "workspace:^", diff --git a/packages/ui/src/components/UserProfile/__tests__/utils.test.ts b/packages/ui/src/components/UserProfile/__tests__/utils.test.ts index 95d67d29ce1..9e7cd95b662 100644 --- a/packages/ui/src/components/UserProfile/__tests__/utils.test.ts +++ b/packages/ui/src/components/UserProfile/__tests__/utils.test.ts @@ -1,10 +1,20 @@ -import type { VerificationJSON } from '@clerk/shared/types'; +import type { EmailAddressResource, PhoneNumberResource } from '@clerk/shared/types'; import { describe, expect, it } from 'vitest'; -import { EmailAddress, PhoneNumber } from '@/core/resources'; - import { sortIdentificationBasedOnVerification } from '../utils'; +const email = (id: string, status: string, expireAtMs = 0) => + ({ + id, + verification: { status, expireAt: new Date(expireAtMs) }, + }) as EmailAddressResource; + +const phone = (id: string, status: string, expireAtMs = 0) => + ({ + id, + verification: { status, expireAt: new Date(expireAtMs) }, + }) as PhoneNumberResource; + describe('UserProfile utils', () => { describe('sortIdentificationBasedOnVerification', () => { it('should return an empty array if the input is null or undefined', () => { @@ -22,71 +32,11 @@ describe('UserProfile utils', () => { `1) primary, 2) verified (sorted alphabetically by id), 3) unverified (sorted by expiresAt verification property)`, () => { const input = [ - new EmailAddress( - { - id: '1', - email_address: 'test@clerk.com', - verification: { - strategy: 'email_code', - status: 'unverified', - attempts: 0, - expire_at: 200, - } as VerificationJSON, - }, - '', - ), - new EmailAddress( - { - id: '2', - email_address: 'test@clerk.com', - verification: { - strategy: 'email_code', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new EmailAddress( - { - id: '3', - email_address: 'test@clerk.com', - verification: { - strategy: 'email_code', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new EmailAddress( - { - id: '4', - email_address: 'test@clerk.com', - verification: { - strategy: 'email_code', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new EmailAddress( - { - id: '5', - email_address: 'test@clerk.com', - verification: { - strategy: 'email_code', - status: 'unverified', - attempts: 0, - expire_at: 100, - } as VerificationJSON, - }, - '', - ), + email('1', 'unverified', 200), + email('2', 'verified'), + email('3', 'verified'), + email('4', 'verified'), + email('5', 'unverified', 100), ]; const result = sortIdentificationBasedOnVerification(input, '3'); expect(result[0].id).toEqual('3'); @@ -102,71 +52,11 @@ describe('UserProfile utils', () => { `1) primary, 2) verified (sorted alphabetically by id), 3) unverified (sorted by expiresAt verification property)`, () => { const input = [ - new PhoneNumber( - { - id: '1', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'unverified', - attempts: 0, - expire_at: 200, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '2', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '3', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '4', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '5', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'unverified', - attempts: 0, - expire_at: 100, - } as VerificationJSON, - }, - '', - ), + phone('1', 'unverified', 200), + phone('2', 'verified'), + phone('3', 'verified'), + phone('4', 'verified'), + phone('5', 'unverified', 100), ]; const result = sortIdentificationBasedOnVerification(input, '3'); @@ -180,71 +70,11 @@ describe('UserProfile utils', () => { it('should return the correct order if the primaryId is not in the array', () => { const input = [ - new PhoneNumber( - { - id: '1', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'unverified', - attempts: 0, - expire_at: 200, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '2', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '3', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '4', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '5', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'unverified', - attempts: 0, - expire_at: 100, - } as VerificationJSON, - }, - '', - ), + phone('1', 'unverified', 200), + phone('2', 'verified'), + phone('3', 'verified'), + phone('4', 'verified'), + phone('5', 'unverified', 100), ]; const result = sortIdentificationBasedOnVerification(input, '10'); @@ -257,84 +87,12 @@ describe('UserProfile utils', () => { it('should return last the item without verification status', () => { const input = [ - new PhoneNumber( - { - id: '1', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'unverified', - attempts: 0, - expire_at: 200, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '2', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '3', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '4', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'verified', - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '5', - phone_number: '+1234567890', - verification: { - strategy: 'sms', - status: 'unverified', - attempts: 0, - expire_at: 100, - } as VerificationJSON, - }, - '', - ), - new PhoneNumber( - { - id: '6', - phone_number: '+1234567890', - verification: { - strategy: '', - status: '' as any, - attempts: 0, - expire_at: 0, - } as VerificationJSON, - }, - '', - ), + phone('1', 'unverified', 200), + phone('2', 'verified'), + phone('3', 'verified'), + phone('4', 'verified'), + phone('5', 'unverified', 100), + phone('6', ''), ]; const result = sortIdentificationBasedOnVerification(input, '3'); diff --git a/packages/ui/src/test/core-shims/clerk.ts b/packages/ui/src/test/core-shims/clerk.ts new file mode 100644 index 00000000000..cc93b325868 --- /dev/null +++ b/packages/ui/src/test/core-shims/clerk.ts @@ -0,0 +1,4 @@ +export class Clerk { + constructor(_publishableKey?: string) {} + async load() {} +} diff --git a/packages/ui/src/test/core-shims/resources.ts b/packages/ui/src/test/core-shims/resources.ts new file mode 100644 index 00000000000..8c164162cfb --- /dev/null +++ b/packages/ui/src/test/core-shims/resources.ts @@ -0,0 +1,13 @@ +export class Client { + static getOrCreateInstance() { + return { fetch: async () => new Client() }; + } + constructor(_data?: unknown) {} +} + +export class Environment { + static getInstance() { + return { fetch: async () => new Environment() }; + } + constructor(_data?: unknown) {} +} diff --git a/packages/ui/src/test/fixture-helpers.ts b/packages/ui/src/test/fixture-helpers.ts index a35c7f8adb6..c9684669c8b 100644 --- a/packages/ui/src/test/fixture-helpers.ts +++ b/packages/ui/src/test/fixture-helpers.ts @@ -1,3 +1,4 @@ +import { SIGN_UP_MODES } from '@clerk/shared/internal/clerk-js/constants'; import type { ClientJSON, DisplayConfigJSON, @@ -16,7 +17,6 @@ import type { VerificationJSON, } from '@clerk/shared/types'; -import { SIGN_UP_MODES } from '@/core/constants'; import type { OrgParams } from '@/test/core-fixtures'; import { createUser, getOrganizationId } from '@/test/core-fixtures'; diff --git a/packages/ui/src/test/jest-dom.d.ts b/packages/ui/src/test/jest-dom.d.ts new file mode 100644 index 00000000000..bb02c60cd05 --- /dev/null +++ b/packages/ui/src/test/jest-dom.d.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest'; diff --git a/packages/ui/src/test/mock-helpers.ts b/packages/ui/src/test/mock-helpers.ts index 5ff677f038a..0323cf8ac12 100644 --- a/packages/ui/src/test/mock-helpers.ts +++ b/packages/ui/src/test/mock-helpers.ts @@ -1,13 +1,13 @@ import { __createClerkTestQueryClient } from '@clerk/shared/react'; import type { ActiveSessionResource, LoadedClerk } from '@clerk/shared/types'; -import { type Mocked, vi } from 'vitest'; +import { type Mock, vi } from 'vitest'; import type { RouteContextValue } from '@/ui/router'; type FunctionLike = (...args: any) => any; type DeepVitestMocked = T extends FunctionLike - ? Mocked + ? Mock : T extends object ? { [k in keyof T]: DeepVitestMocked; diff --git a/packages/ui/tsconfig.base.json b/packages/ui/tsconfig.base.json index acdf124e912..b787838a2a9 100644 --- a/packages/ui/tsconfig.base.json +++ b/packages/ui/tsconfig.base.json @@ -17,8 +17,8 @@ "allowJs": true, "verbatimModuleSyntax": true, "paths": { - // Test-only imports; vitest.config.mts aliases these to clerk-js at runtime. - "@/core/*": ["../clerk-js/src/core/*"], + // Test-only; tsserver stays in-package. vitest.config.mts aliases these to clerk-js at runtime. + "@/core/*": ["./src/test/core-shims/*"], "@/*": ["./src/*"], // Adding this to avoid changes in the ui files // in order to make git merges easier diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 05dc93a5b78..5f9e2a21b8b 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,22 +1,5 @@ { - "extends": "./tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "preserveWatchOutput": true, - "outDir": "dist", - "declaration": true, - "declarationMap": true, - "types": ["@rspack/core/module"] - }, - "exclude": [ - "node_modules", - "**/*.test.ts", - "**/*.test.tsx", - "**/*.spec.ts", - "**/*.spec.tsx", - "**/__tests__/**", - "./src/test/**" - ], - "include": ["src", "src/global.d.ts"], - "references": [{ "path": "./tsconfig.test.json" }] + "files": [], + "include": [], + "references": [{ "path": "./tsconfig.src.json" }, { "path": "./tsconfig.test.json" }] } diff --git a/packages/ui/tsconfig.mosaic.json b/packages/ui/tsconfig.mosaic.json index 05fde7579f6..e8f3f160eb7 100644 --- a/packages/ui/tsconfig.mosaic.json +++ b/packages/ui/tsconfig.mosaic.json @@ -1,6 +1,7 @@ { - "extends": "./tsconfig.json", + "extends": "./tsconfig.src.json", "compilerOptions": { + "composite": false, "jsxImportSource": "react", "paths": { // Resolve the private, unpublished `@clerk/headless` to its SOURCE for the Mosaic @@ -10,7 +11,7 @@ "@clerk/headless/utils": ["../headless/src/utils/index.ts"], "@clerk/headless/*": ["../headless/src/*"], // Preserve the base config's test-only aliases (extends replaces `paths` wholesale). - "@/core/*": ["../clerk-js/src/core/*"], + "@/core/*": ["./src/test/core-shims/*"], "@/*": ["./src/*"], "@/ui*": ["./src/*"] } diff --git a/packages/ui/tsconfig.src.json b/packages/ui/tsconfig.src.json new file mode 100644 index 00000000000..86e4191133b --- /dev/null +++ b/packages/ui/tsconfig.src.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "preserveWatchOutput": true, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "node_modules/.cache/tsconfig-src", + "tsBuildInfoFile": "node_modules/.cache/tsconfig-src.tsbuildinfo", + "types": ["@rspack/core/module"] + }, + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/__tests__/**", + "./src/test/**" + ], + "include": ["src", "src/global.d.ts"] +} diff --git a/packages/ui/tsconfig.test.json b/packages/ui/tsconfig.test.json index 2f48da331b8..9f7f40e1d90 100644 --- a/packages/ui/tsconfig.test.json +++ b/packages/ui/tsconfig.test.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - // composite so tsserver can load this via tsconfig.json references (tsc --noEmit requires it). "composite": true, "declaration": true, "emitDeclarationOnly": true, @@ -16,7 +15,9 @@ "src/**/__tests__/**/*.ts", "src/**/__tests__/**/*.tsx", "src/test/**/*.ts", - "src/test/**/*.tsx" + "src/test/**/*.tsx", + "src/global.d.ts" ], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "references": [{ "path": "./tsconfig.src.json" }] } diff --git a/packages/ui/tsdown.config.mts b/packages/ui/tsdown.config.mts index f5b8a74e396..881f3ea3ebf 100644 --- a/packages/ui/tsdown.config.mts +++ b/packages/ui/tsdown.config.mts @@ -6,7 +6,18 @@ import uiPackage from './package.json' with { type: 'json' }; export default defineConfig(({ watch }) => { const common = { - dts: true, + tsconfig: './tsconfig.src.json', + dts: { + // The .src. file is for typechecking, this overrides the relevant parts for build output + compilerOptions: { + composite: false, + declaration: true, + declarationMap: true, + emitDeclarationOnly: false, + incremental: false, + outDir: 'dist', + }, + }, sourcemap: true, clean: false, target: 'es2022', diff --git a/packages/ui/turbo.json b/packages/ui/turbo.json index d001d7daa95..e4784386d91 100644 --- a/packages/ui/turbo.json +++ b/packages/ui/turbo.json @@ -11,8 +11,11 @@ "*.d.ts", "src/**", "tsconfig.json", + "tsconfig.base.json", + "tsconfig.src.json", "tsconfig.mosaic.json", "rspack.config.js", + "tsdown.config.mts", "tsdown.mosaic.config.mts", "!**/*.test.*",