From f09ee0b84be0a62d182a6fa118d33381482bcd64 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Sat, 7 Dec 2024 16:38:25 +0200 Subject: [PATCH] ci: add types testing with `tstyche` (#9803) As proposed here https://github.com/payloadcms/payload/pull/9782#issuecomment-2522090135 with additional testing of our types we can be more sure that we don't break them between updates. This PR already adds types testing for most Local API methods https://github.com/payloadcms/payload/blob/6beb921c2e232ab4edfa38c480af40a1bec1106e/test/types/types.spec.ts but new tests for types can be easily added, either to that same file or you can create `types.spec.ts` in any other test folder. The new test folder uses `strict: true` to ensure our types do not break with it. --------- Co-authored-by: Tom Mrazauskas --- .github/workflows/main.yml | 27 ++++ package.json | 2 + pnpm-lock.yaml | 17 +++ test/types/.gitignore | 2 + test/types/config.ts | 45 ++++++ test/types/eslint.config.js | 19 +++ test/types/payload-types.ts | 236 ++++++++++++++++++++++++++++++++ test/types/tsconfig.eslint.json | 13 ++ test/types/tsconfig.json | 8 ++ test/types/types.spec.ts | 80 +++++++++++ tstyche.config.json | 3 + 11 files changed, 452 insertions(+) create mode 100644 test/types/.gitignore create mode 100644 test/types/config.ts create mode 100644 test/types/eslint.config.js create mode 100644 test/types/payload-types.ts create mode 100644 test/types/tsconfig.eslint.json create mode 100644 test/types/tsconfig.json create mode 100644 test/types/types.spec.ts create mode 100644 tstyche.config.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fc9c6cf843..186e98a8b0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -134,6 +134,33 @@ jobs: env: NODE_OPTIONS: --max-old-space-size=8096 + tests-types: + runs-on: ubuntu-24.04 + needs: [changes, build] + if: ${{ needs.changes.outputs.needs_tests == 'true' }} + steps: + - uses: actions/checkout@v4 + + - name: Node setup + uses: ./.github/actions/setup + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + pnpm-run-install: false + pnpm-restore-cache: false # Full build is restored below + pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Restore build + uses: actions/cache@v4 + with: + path: ./* + key: ${{ github.sha }}-${{ github.run_number }} + + - name: Types Tests + run: pnpm test:types --target '>=5.7' + env: + NODE_OPTIONS: --max-old-space-size=8096 + tests-int: runs-on: ubuntu-24.04 needs: [changes, build] diff --git a/package.json b/package.json index 7ed03aaec0..95962c9fc2 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand", "test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand", "test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand", + "test:types": "tstyche", "test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand", "translateNewKeys": "pnpm --filter payload run translateNewKeys" }, @@ -165,6 +166,7 @@ "sort-package-json": "^2.10.0", "swc-plugin-transform-remove-imports": "2.0.0", "tempy": "1.0.1", + "tstyche": "^3.1.1", "tsx": "4.19.2", "turbo": "^2.1.3", "typescript": "5.7.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3c55f9fab..e16dd6ad5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,6 +196,9 @@ importers: tempy: specifier: 1.0.1 version: 1.0.1 + tstyche: + specifier: ^3.1.1 + version: 3.1.1(typescript@5.7.2) tsx: specifier: 4.19.2 version: 4.19.2 @@ -9787,6 +9790,16 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tstyche@3.1.1: + resolution: {integrity: sha512-GB1GEApaoYHPREwbFuzx0D/dY3ohZdZ7tNbZJNjxoe3Xv1ibR6c+D8agOou6dYK3pOiSl1peaHCpbu9N40WQiw==} + engines: {node: '>=18.19'} + hasBin: true + peerDependencies: + typescript: 5.7.2 + peerDependenciesMeta: + typescript: + optional: true + tsx@4.19.2: resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} engines: {node: '>=18.0.0'} @@ -19984,6 +19997,10 @@ snapshots: tslib@2.8.1: {} + tstyche@3.1.1(typescript@5.7.2): + optionalDependencies: + typescript: 5.7.2 + tsx@4.19.2: dependencies: esbuild: 0.23.1 diff --git a/test/types/.gitignore b/test/types/.gitignore new file mode 100644 index 0000000000..cce01755f4 --- /dev/null +++ b/test/types/.gitignore @@ -0,0 +1,2 @@ +/media +/media-gif diff --git a/test/types/config.ts b/test/types/config.ts new file mode 100644 index 0000000000..a81d2b4c5d --- /dev/null +++ b/test/types/config.ts @@ -0,0 +1,45 @@ +import { lexicalEditor } from '@payloadcms/richtext-lexical' +import { fileURLToPath } from 'node:url' +import path from 'path' + +import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +export default buildConfigWithDefaults({ + // ...extend config here + collections: [ + { + slug: 'posts', + versions: true, + fields: [ + { + type: 'text', + name: 'text', + }, + ], + }, + ], + admin: { + importMap: { + baseDir: path.resolve(dirname), + }, + }, + editor: lexicalEditor({}), + globals: [ + { + slug: 'menu', + versions: true, + fields: [ + { + type: 'text', + name: 'text', + }, + ], + }, + ], + typescript: { + outputFile: path.resolve(dirname, 'payload-types.ts'), + }, +}) diff --git a/test/types/eslint.config.js b/test/types/eslint.config.js new file mode 100644 index 0000000000..f295df083f --- /dev/null +++ b/test/types/eslint.config.js @@ -0,0 +1,19 @@ +import { rootParserOptions } from '../../eslint.config.js' +import { testEslintConfig } from '../eslint.config.js' + +/** @typedef {import('eslint').Linter.Config} Config */ + +/** @type {Config[]} */ +export const index = [ + ...testEslintConfig, + { + languageOptions: { + parserOptions: { + ...rootParserOptions, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +] + +export default index diff --git a/test/types/payload-types.ts b/test/types/payload-types.ts new file mode 100644 index 0000000000..2d8c1cd872 --- /dev/null +++ b/test/types/payload-types.ts @@ -0,0 +1,236 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * This file was automatically generated by Payload. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run `payload generate:types` to regenerate this file. + */ + +export interface Config { + auth: { + users: UserAuthOperations; + }; + collections: { + posts: Post; + users: User; + 'payload-locked-documents': PayloadLockedDocument; + 'payload-preferences': PayloadPreference; + 'payload-migrations': PayloadMigration; + }; + collectionsJoins: {}; + collectionsSelect: { + posts: PostsSelect | PostsSelect; + users: UsersSelect | UsersSelect; + 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; + 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; + 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; + }; + db: { + defaultIDType: string; + }; + globals: { + menu: Menu; + }; + globalsSelect: { + menu: MenuSelect | MenuSelect; + }; + locale: null; + user: User & { + collection: 'users'; + }; + jobs: { + tasks: unknown; + workflows: unknown; + }; +} +export interface UserAuthOperations { + forgotPassword: { + email: string; + password: string; + }; + login: { + email: string; + password: string; + }; + registerFirstUser: { + email: string; + password: string; + }; + unlock: { + email: string; + password: string; + }; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts". + */ +export interface Post { + id: string; + text?: string | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users". + */ +export interface User { + id: string; + updatedAt: string; + createdAt: string; + email: string; + resetPasswordToken?: string | null; + resetPasswordExpiration?: string | null; + salt?: string | null; + hash?: string | null; + loginAttempts?: number | null; + lockUntil?: string | null; + password?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents". + */ +export interface PayloadLockedDocument { + id: string; + document?: + | ({ + relationTo: 'posts'; + value: string | Post; + } | null) + | ({ + relationTo: 'users'; + value: string | User; + } | null); + globalSlug?: string | null; + user: { + relationTo: 'users'; + value: string | User; + }; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences". + */ +export interface PayloadPreference { + id: string; + user: { + relationTo: 'users'; + value: string | User; + }; + key?: string | null; + value?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations". + */ +export interface PayloadMigration { + id: string; + name?: string | null; + batch?: number | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts_select". + */ +export interface PostsSelect { + text?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users_select". + */ +export interface UsersSelect { + updatedAt?: T; + createdAt?: T; + email?: T; + resetPasswordToken?: T; + resetPasswordExpiration?: T; + salt?: T; + hash?: T; + loginAttempts?: T; + lockUntil?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents_select". + */ +export interface PayloadLockedDocumentsSelect { + document?: T; + globalSlug?: T; + user?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences_select". + */ +export interface PayloadPreferencesSelect { + user?: T; + key?: T; + value?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations_select". + */ +export interface PayloadMigrationsSelect { + name?: T; + batch?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "menu". + */ +export interface Menu { + id: string; + text?: string | null; + updatedAt?: string | null; + createdAt?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "menu_select". + */ +export interface MenuSelect { + text?: T; + updatedAt?: T; + createdAt?: T; + globalType?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "auth". + */ +export interface Auth { + [k: string]: unknown; +} + + +declare module 'payload' { + // @ts-ignore + export interface GeneratedTypes extends Config {} +} \ No newline at end of file diff --git a/test/types/tsconfig.eslint.json b/test/types/tsconfig.eslint.json new file mode 100644 index 0000000000..b34cc7afbb --- /dev/null +++ b/test/types/tsconfig.eslint.json @@ -0,0 +1,13 @@ +{ + // extend your base config to share compilerOptions, etc + //"extends": "./tsconfig.json", + "compilerOptions": { + // ensure that nobody can accidentally use this config for a build + "noEmit": true + }, + "include": [ + // whatever paths you intend to lint + "./**/*.ts", + "./**/*.tsx" + ] +} diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json new file mode 100644 index 0000000000..72f71380b6 --- /dev/null +++ b/test/types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/test/types/types.spec.ts b/test/types/types.spec.ts new file mode 100644 index 0000000000..adb4af088e --- /dev/null +++ b/test/types/types.spec.ts @@ -0,0 +1,80 @@ +import type { BulkOperationResult, PaginatedDocs, SelectType, TypeWithVersion } from 'payload' + +import payload from 'payload' +import { describe, expect, test } from 'tstyche' + +import type { Menu, Post, User } from './payload-types.js' + +describe('Types testing', () => { + test('payload.find', () => { + expect(payload.find({ collection: 'users' })).type.toBe>>() + }) + + test('payload.findByID', () => { + expect(payload.findByID({ id: 1, collection: 'users' })).type.toBe>() + }) + + test('payload.findByID with disableErrors: true', () => { + expect(payload.findByID({ id: 1, collection: 'users', disableErrors: true })).type.toBe< + Promise + >() + }) + + test('payload.create', () => { + expect(payload.create({ collection: 'users', data: { email: 'user@email.com' } })).type.toBe< + Promise + >() + }) + + test('payload.update by ID', () => { + expect(payload.update({ id: 1, collection: 'users', data: {} })).type.toBe>() + }) + + test('payload.update many', () => { + expect(payload.update({ where: {}, collection: 'users', data: {} })).type.toBe< + Promise> + >() + }) + + test('payload.delete by ID', () => { + expect(payload.delete({ id: 1, collection: 'users' })).type.toBe>() + }) + + test('payload.delete many', () => { + expect(payload.delete({ where: {}, collection: 'users' })).type.toBe< + Promise> + >() + }) + + test('payload.findGlobal', () => { + expect(payload.findGlobal({ slug: 'menu' })).type.toBe>() + }) + + test('payload.updateGlobal', () => { + expect(payload.updateGlobal({ data: {}, slug: 'menu' })).type.toBe>() + }) + + test('payload.findVersions', () => { + expect(payload.findVersions({ collection: 'posts' })).type.toBe< + Promise>> + >() + }) + + test('payload.findVersionByID', () => { + expect(payload.findVersionByID({ id: 'id', collection: 'posts' })).type.toBe< + Promise> + >() + }) + + test('payload.findGlobalVersions', () => { + expect(payload.findGlobalVersions({ slug: 'menu' })).type.toBe< + Promise>> + >() + }) + + test('payload.findGlobalVersionByID', () => { + expect(payload.findGlobalVersionByID({ id: 'id', slug: 'menu' })).type.toBe< + Promise> + >() + }) +}) diff --git a/tstyche.config.json b/tstyche.config.json new file mode 100644 index 0000000000..9b8217a2f1 --- /dev/null +++ b/tstyche.config.json @@ -0,0 +1,3 @@ +{ + "testFileMatch": ["test/**/types.spec.ts"] +}