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
6beb921c2e/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 <tom@mrazauskas.de>
This commit is contained in:
Sasha
2024-12-07 16:38:25 +02:00
committed by GitHub
parent 1fdc7cc70d
commit f09ee0b84b
11 changed files with 452 additions and 0 deletions

View File

@@ -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]

View File

@@ -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"

17
pnpm-lock.yaml generated
View File

@@ -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

2
test/types/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/media
/media-gif

45
test/types/config.ts Normal file
View File

@@ -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'),
},
})

View File

@@ -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

236
test/types/payload-types.ts Normal file
View File

@@ -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<false> | PostsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {
menu: Menu;
};
globalsSelect: {
menu: MenuSelect<false> | MenuSelect<true>;
};
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<T extends boolean = true> {
text?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
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<T extends boolean = true> {
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<T extends boolean = true> {
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<T extends boolean = true> {
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<T extends boolean = true> {
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 {}
}

View File

@@ -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"
]
}

8
test/types/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true
}
}

80
test/types/types.spec.ts Normal file
View File

@@ -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<Promise<PaginatedDocs<User>>>()
})
test('payload.findByID', () => {
expect(payload.findByID({ id: 1, collection: 'users' })).type.toBe<Promise<User>>()
})
test('payload.findByID with disableErrors: true', () => {
expect(payload.findByID({ id: 1, collection: 'users', disableErrors: true })).type.toBe<
Promise<null | User>
>()
})
test('payload.create', () => {
expect(payload.create({ collection: 'users', data: { email: 'user@email.com' } })).type.toBe<
Promise<User>
>()
})
test('payload.update by ID', () => {
expect(payload.update({ id: 1, collection: 'users', data: {} })).type.toBe<Promise<User>>()
})
test('payload.update many', () => {
expect(payload.update({ where: {}, collection: 'users', data: {} })).type.toBe<
Promise<BulkOperationResult<'users', SelectType>>
>()
})
test('payload.delete by ID', () => {
expect(payload.delete({ id: 1, collection: 'users' })).type.toBe<Promise<User>>()
})
test('payload.delete many', () => {
expect(payload.delete({ where: {}, collection: 'users' })).type.toBe<
Promise<BulkOperationResult<'users', SelectType>>
>()
})
test('payload.findGlobal', () => {
expect(payload.findGlobal({ slug: 'menu' })).type.toBe<Promise<Menu>>()
})
test('payload.updateGlobal', () => {
expect(payload.updateGlobal({ data: {}, slug: 'menu' })).type.toBe<Promise<Menu>>()
})
test('payload.findVersions', () => {
expect(payload.findVersions({ collection: 'posts' })).type.toBe<
Promise<PaginatedDocs<TypeWithVersion<Post>>>
>()
})
test('payload.findVersionByID', () => {
expect(payload.findVersionByID({ id: 'id', collection: 'posts' })).type.toBe<
Promise<TypeWithVersion<Post>>
>()
})
test('payload.findGlobalVersions', () => {
expect(payload.findGlobalVersions({ slug: 'menu' })).type.toBe<
Promise<PaginatedDocs<TypeWithVersion<Menu>>>
>()
})
test('payload.findGlobalVersionByID', () => {
expect(payload.findGlobalVersionByID({ id: 'id', slug: 'menu' })).type.toBe<
Promise<TypeWithVersion<Menu>>
>()
})
})

3
tstyche.config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"testFileMatch": ["test/**/types.spec.ts"]
}