From 785970f830ca0680ff50e8868af40f0a4b792ec4 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Fri, 7 Jun 2024 08:42:16 +0200 Subject: [PATCH 01/13] Convert types of generator functions to async functions in stores --- packages/interactivity/src/store.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index 281a6c266021e..0cec3637b8e7a 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -207,6 +207,24 @@ interface StoreOptions { lock?: boolean | string; } +// Utility type to check if a function is a generator function. +type IsGenerator< T > = T extends ( + ...args: infer A +) => Generator< infer R, any, any > + ? ( ...args: A ) => Promise< R > + : never; + +// Utility type to convert all generator functions in an object to async functions. +type ConvertGenerators< T > = { + [ K in keyof T ]: T[ K ] extends ( ...args: any[] ) => any + ? IsGenerator< T[ K ] > extends never + ? T[ K ] + : IsGenerator< T[ K ] > + : T[ K ] extends object + ? ConvertGenerators< T[ K ] > + : T[ K ]; +}; + export const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; @@ -259,13 +277,13 @@ export function store< S extends object = {} >( namespace: string, storePart?: S, options?: StoreOptions -): S; +): ConvertGenerators< S >; export function store< T extends object >( namespace: string, storePart?: T, options?: StoreOptions -): T; +): ConvertGenerators< T >; export function store( namespace: string, From 5c73a529452d0b8c82ec0dc1827681e5a198ead2 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Fri, 7 Jun 2024 10:57:22 +0200 Subject: [PATCH 02/13] Fix return type --- packages/interactivity/src/store.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index 0cec3637b8e7a..324578a6f2c88 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -208,18 +208,18 @@ interface StoreOptions { } // Utility type to check if a function is a generator function. -type IsGenerator< T > = T extends ( +type ConvertGenerator< T > = T extends ( ...args: infer A -) => Generator< infer R, any, any > +) => Generator< any, infer R, any > ? ( ...args: A ) => Promise< R > : never; // Utility type to convert all generator functions in an object to async functions. type ConvertGenerators< T > = { [ K in keyof T ]: T[ K ] extends ( ...args: any[] ) => any - ? IsGenerator< T[ K ] > extends never + ? ConvertGenerator< T[ K ] > extends never ? T[ K ] - : IsGenerator< T[ K ] > + : ConvertGenerator< T[ K ] > : T[ K ] extends object ? ConvertGenerators< T[ K ] > : T[ K ]; From a9775f581f296f49a231ce2789594c763b82e337 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Fri, 7 Jun 2024 10:59:42 +0200 Subject: [PATCH 03/13] Add types tests --- packages/interactivity/src/test/types.ts | 28 ++++++++++++++++++++++++ packages/interactivity/tsconfig.json | 1 + 2 files changed, 29 insertions(+) create mode 100644 packages/interactivity/src/test/types.ts diff --git a/packages/interactivity/src/test/types.ts b/packages/interactivity/src/test/types.ts new file mode 100644 index 0000000000000..124dc27572a02 --- /dev/null +++ b/packages/interactivity/src/test/types.ts @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import { store } from '../store'; + +const { actions } = store( 'test', { + actions: { + sync: () => 123, + *async() { + return 123; + }, + }, +} ); + +// Test types. +const n1: number = actions.sync(); +n1; +const p1: Promise< number > = actions.async(); +p1; +const n2: number = await actions.async(); +n2; + +// @ts-expect-error +const n3: string = actions.sync(); +// @ts-expect-error +const p2: Promise< string > = actions.async(); +// @ts-expect-error +const n4: string = await actions.async(); diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index 1d154e2089065..211a61eedff96 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -7,5 +7,6 @@ "noImplicitAny": false }, + "files": [ "src/test/types.ts" ], "include": [ "src/**/*" ] } From 53a25f3bc61551c5f09f5e0bd60a131f1090df3b Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Fri, 7 Jun 2024 14:39:08 +0200 Subject: [PATCH 04/13] Refactor tests and update tsconfig.json to disable noUnusedLocals rule --- packages/interactivity/src/test/types.ts | 31 ++++++++++++++---------- packages/interactivity/tsconfig.json | 4 +-- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/interactivity/src/test/types.ts b/packages/interactivity/src/test/types.ts index 124dc27572a02..c9e958f9c879c 100644 --- a/packages/interactivity/src/test/types.ts +++ b/packages/interactivity/src/test/types.ts @@ -12,17 +12,22 @@ const { actions } = store( 'test', { }, } ); -// Test types. -const n1: number = actions.sync(); -n1; -const p1: Promise< number > = actions.async(); -p1; -const n2: number = await actions.async(); -n2; +/** + * Test types. + */ +{ + const var1: number = actions.sync(); + const var2: Promise< number > = actions.async(); + const var3: number = await actions.async(); +} + +{ + // This is intentionally included to ensure that this test fails on GitHub + // before replacing it with @ts-expect-error. + const var1: string = actions.sync(); -// @ts-expect-error -const n3: string = actions.sync(); -// @ts-expect-error -const p2: Promise< string > = actions.async(); -// @ts-expect-error -const n4: string = await actions.async(); + // @ts-expect-error + const var2: Promise< string > = actions.async(); + // @ts-expect-error + const var3: string = await actions.async(); +} diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index 211a61eedff96..8f940bd053cc8 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -4,8 +4,8 @@ "compilerOptions": { "rootDir": "src", "declarationDir": "build-types", - - "noImplicitAny": false + "noImplicitAny": false, + "noUnusedLocals": false }, "files": [ "src/test/types.ts" ], "include": [ "src/**/*" ] From 83aa6989351f5364f6a8e541eb4755c9f9178574 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Fri, 7 Jun 2024 14:52:13 +0200 Subject: [PATCH 05/13] Move the tests to the `test-types` folder --- .../src/{test/types.ts => test-types/store.ts} | 0 packages/interactivity/src/test-types/tsconfig.json | 11 +++++++++++ packages/interactivity/tsconfig.json | 4 +--- tsconfig.base.json | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) rename packages/interactivity/src/{test/types.ts => test-types/store.ts} (100%) create mode 100644 packages/interactivity/src/test-types/tsconfig.json diff --git a/packages/interactivity/src/test/types.ts b/packages/interactivity/src/test-types/store.ts similarity index 100% rename from packages/interactivity/src/test/types.ts rename to packages/interactivity/src/test-types/store.ts diff --git a/packages/interactivity/src/test-types/tsconfig.json b/packages/interactivity/src/test-types/tsconfig.json new file mode 100644 index 0000000000000..3cbdd5e4fd985 --- /dev/null +++ b/packages/interactivity/src/test-types/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "..", + "noImplicitAny": false, + "noUnusedLocals": false + }, + "include": [ "../**/*" ], + "exclude": [ "../test/**/*" ] +} diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index 8f940bd053cc8..4be4fe7bae1d7 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -4,9 +4,7 @@ "compilerOptions": { "rootDir": "src", "declarationDir": "build-types", - "noImplicitAny": false, - "noUnusedLocals": false + "noImplicitAny": false }, - "files": [ "src/test/types.ts" ], "include": [ "src/**/*" ] } diff --git a/tsconfig.base.json b/tsconfig.base.json index 18388b7f71854..9168c023a4b5e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -41,6 +41,7 @@ "packages/*/build-*/**", "packages/*/build/**", "**/test/**", + "**/test-types/**", "packages/**/react-native-*/**" ] } From 3544ad44bb7a6179437c5a98f28dfecfcbc6b37f Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 10 Jun 2024 13:05:18 +0200 Subject: [PATCH 06/13] Add types test runner --- bin/run-test-types.js | 27 +++++++++++++++++++ package.json | 1 + .../src/test-types/tsconfig.json | 2 ++ 3 files changed, 30 insertions(+) create mode 100644 bin/run-test-types.js diff --git a/bin/run-test-types.js b/bin/run-test-types.js new file mode 100644 index 0000000000000..1f05862f9c98f --- /dev/null +++ b/bin/run-test-types.js @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +const glob = require( 'glob' ); +const { execSync } = require( 'child_process' ); +const path = require( 'path' ); +let hasError = false; + +// Find all tsconfig.json files within test-types directories. +const tsconfigPaths = glob.sync( '**/test-types/**/tsconfig.json' ); + +if ( tsconfigPaths.length > 0 ) { + tsconfigPaths.forEach( ( tsconfigPath ) => { + const dir = path.dirname( tsconfigPath ); + const command = `npx tsc --project ${ tsconfigPath }`; + console.log( `Testing types in ${ dir }` ); + try { + execSync( command, { stdio: 'inherit' } ); + } catch ( error ) { + hasError = true; + } + } ); + + if ( hasError ) { + process.exit( 1 ); + } +} diff --git a/package.json b/package.json index ca8ebb89876a6..642ea638db67a 100644 --- a/package.json +++ b/package.json @@ -350,6 +350,7 @@ "test:unit:php:multisite:debug": "npm-run-all test:unit:php:setup:debug test:unit:php:multisite:base", "test:unit:update": "npm run test:unit -- --updateSnapshot", "test:unit:watch": "npm run test:unit -- --watch", + "test:types": "node bin/run-test-types.js", "wp-env": "wp-env" }, "lint-staged": { diff --git a/packages/interactivity/src/test-types/tsconfig.json b/packages/interactivity/src/test-types/tsconfig.json index 3cbdd5e4fd985..81819cbd0344c 100644 --- a/packages/interactivity/src/test-types/tsconfig.json +++ b/packages/interactivity/src/test-types/tsconfig.json @@ -2,6 +2,8 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../../../tsconfig.base.json", "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, "rootDir": "..", "noImplicitAny": false, "noUnusedLocals": false From a50eeb2c8a7972a1ae4e1554c697792b4eebb9bf Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 10 Jun 2024 17:21:45 +0200 Subject: [PATCH 07/13] Add GH action --- .github/workflows/unit-test.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index a813e4d2d8f5b..091e2cc10f78e 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -92,6 +92,22 @@ jobs: - name: Run the date tests run: npm run test:unit:date -- --ci --maxWorkers=${{ steps.cpu-cores.outputs.count }} --cacheDirectory="$HOME/.jest-cache" + types: + name: TypeScript Tests + runs-on: ubuntu-latest + if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + + - name: Run the types tests + run: npm run test:types + compute-previous-wordpress-version: name: Compute previous WordPress version runs-on: ubuntu-latest From 19a7f0bbacc251d04803959cdaf8b1882ef17d25 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 10 Jun 2024 17:24:09 +0200 Subject: [PATCH 08/13] Setup Node.js and install dependencies --- .github/workflows/unit-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 091e2cc10f78e..9674704d46dad 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -105,6 +105,9 @@ jobs: with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node + - name: Run the types tests run: npm run test:types From a74f64c6b558f00154898a6791422f2c3a430ac9 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 10 Jun 2024 17:43:57 +0200 Subject: [PATCH 09/13] Fix test and add note --- packages/interactivity/src/test-types/store.ts | 4 +--- packages/interactivity/src/test-types/tsconfig.json | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/interactivity/src/test-types/store.ts b/packages/interactivity/src/test-types/store.ts index c9e958f9c879c..322a351581356 100644 --- a/packages/interactivity/src/test-types/store.ts +++ b/packages/interactivity/src/test-types/store.ts @@ -22,10 +22,8 @@ const { actions } = store( 'test', { } { - // This is intentionally included to ensure that this test fails on GitHub - // before replacing it with @ts-expect-error. + // @ts-expect-error const var1: string = actions.sync(); - // @ts-expect-error const var2: Promise< string > = actions.async(); // @ts-expect-error diff --git a/packages/interactivity/src/test-types/tsconfig.json b/packages/interactivity/src/test-types/tsconfig.json index 81819cbd0344c..40ac5fb51f07c 100644 --- a/packages/interactivity/src/test-types/tsconfig.json +++ b/packages/interactivity/src/test-types/tsconfig.json @@ -2,9 +2,11 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../../../tsconfig.base.json", "compilerOptions": { + "rootDir": "..", "emitDeclarationOnly": false, "noEmit": true, - "rootDir": "..", + // Implicit any can be removed once this work is finished: + // https://github.com/WordPress/gutenberg/pull/61695 "noImplicitAny": false, "noUnusedLocals": false }, From bd2eaa4a08eb7ddfec955422442c60b4928efd15 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Wed, 14 Aug 2024 11:37:09 +0200 Subject: [PATCH 10/13] Add the TS tests to the unit tests file --- packages/interactivity/src/test/store.ts | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/interactivity/src/test/store.ts diff --git a/packages/interactivity/src/test/store.ts b/packages/interactivity/src/test/store.ts new file mode 100644 index 0000000000000..0257c56055143 --- /dev/null +++ b/packages/interactivity/src/test/store.ts @@ -0,0 +1,39 @@ +import { store } from '../store'; + +describe( 'Interactivity API', () => { + it( 'needs at least one test', () => { + expect( true ).toBe( true ); + } ); + + describe( 'static typing', () => { + async () => { + const { actions } = store( 'test', { + actions: { + sync: () => 123, + *async() { + return 123; + }, + }, + } ); + + /** + * Test types. + */ + { + const var1: number = actions.sync(); + const var2: Promise< number > = actions.async(); + const var3: number = await actions.async(); + } + + { + // This is expected to fail. + // // @ts-expect-error + const var1: string = actions.sync(); + // @ts-expect-error + const var2: Promise< string > = actions.async(); + // @ts-expect-error + const var3: string = await actions.async(); + } + }; + } ); +} ); From 14cc9c4b70d8d6939b4d499c0238268d4b604827 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Wed, 14 Aug 2024 11:52:16 +0200 Subject: [PATCH 11/13] Add eslint disablers --- packages/interactivity/src/test/store.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/interactivity/src/test/store.ts b/packages/interactivity/src/test/store.ts index 0257c56055143..3290521003bab 100644 --- a/packages/interactivity/src/test/store.ts +++ b/packages/interactivity/src/test/store.ts @@ -1,3 +1,9 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/** + * Internal dependencies + */ import { store } from '../store'; describe( 'Interactivity API', () => { @@ -6,7 +12,7 @@ describe( 'Interactivity API', () => { } ); describe( 'static typing', () => { - async () => { + ( async () => { const { actions } = store( 'test', { actions: { sync: () => 123, @@ -34,6 +40,6 @@ describe( 'Interactivity API', () => { // @ts-expect-error const var3: string = await actions.async(); } - }; + } )(); } ); } ); From 433cd28795c6d1e528105cf00c7946af0edbb7e3 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Wed, 14 Aug 2024 12:03:02 +0200 Subject: [PATCH 12/13] Add eslint disablers for old file --- packages/interactivity/src/test-types/store.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/interactivity/src/test-types/store.ts b/packages/interactivity/src/test-types/store.ts index 322a351581356..71264db915f9d 100644 --- a/packages/interactivity/src/test-types/store.ts +++ b/packages/interactivity/src/test-types/store.ts @@ -1,3 +1,6 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + /** * Internal dependencies */ From b6ec0bad0eb2f14721716a9b984848e359fe8e12 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Thu, 15 Aug 2024 12:33:25 +0200 Subject: [PATCH 13/13] Prettify TS output to show the final types --- packages/interactivity/src/store.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index 227cebba2dcc0..e51ae757848ad 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -64,10 +64,12 @@ type ConvertGenerators< T > = { ? T[ K ] : ConvertGenerator< T[ K ] > : T[ K ] extends object - ? ConvertGenerators< T[ K ] > + ? Prettify> : T[ K ]; }; +type Prettify = { [ K in keyof T ]: T[ K ]; } & {}; + export const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; @@ -120,13 +122,13 @@ export function store< S extends object = {} >( namespace: string, storePart?: S, options?: StoreOptions -): ConvertGenerators< S >; +): Prettify>; export function store< T extends object >( namespace: string, storePart?: T, options?: StoreOptions -): ConvertGenerators< T >; +): Prettify>; export function store( namespace: string,