Skip to content

Commit 67b5dc3

Browse files
feat: add typescript.postProcess hook for type generation (#16103)
## Summary This pr would allow us to import a generic from the plugin and then use it. The limitation with json-schema-to-typescript is the inability to generate a type in the file that takes generics. There are some generated generics plopped in, internally by our addSelectGenericsToGeneratedTypes function. This PR exposes a way for external plugins to manipulate the generated config post-processing of json-schema-to-typescript which is not possible today. ## Implementation In `packages/payload/src/bin/generateTypes.ts`, after `addSelectGenericsToGeneratedTypes`: ```ts if (config.typescript?.postProcess?.length) { for (const fn of config.typescript.postProcess) { compiled = fn(compiled); } } ``` Add to TypeScript config type: ```ts typescript?: { postProcess?: Array<(compiled: string) => string>; } ``` **Use cases** - Inject imports from plugin packages - Add utility types **Example** ```ts // Plugin adds an import and uses it via tsType on fields typescript: { postProcess: [ ...(config.typescript?.postProcess || []), (compiled) => { const importStmt = `import type { MyPluginType } from '@payloadcms/my-plugin';`; return compiled.replace(/(\/\*[\s\S]*?\*\/\n)/, `$1\n${importStmt}\n`); }, ], } ``` **Notes** - Follows existing pattern (Payload already post-processes for Select generics)
1 parent 6184b0c commit 67b5dc3

5 files changed

Lines changed: 87 additions & 0 deletions

File tree

packages/payload/src/bin/generateTypes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ export async function generateTypes(
5050

5151
compiled = addSelectGenericsToGeneratedTypes({ compiledGeneratedTypes: compiled })
5252

53+
if (config.typescript.postProcess?.length) {
54+
for (const fn of config.typescript.postProcess) {
55+
compiled = fn({ compiledTypes: compiled, config })
56+
}
57+
}
58+
5359
if (config.typescript.declare !== false) {
5460
if (config.typescript.declare?.ignoreTSError) {
5561
compiled += `\n\n${declareWithTSIgnoreError}`

packages/payload/src/config/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,24 @@ export type Config = {
14861486
/** Filename to write the generated types to */
14871487
outputFile?: string
14881488

1489+
/**
1490+
* Post-process the generated TypeScript types string before writing to file.
1491+
* Useful for plugins that need to inject generic types that JSON Schema cannot express.
1492+
*
1493+
* Functions are applied in order after the built-in Select generics are added.
1494+
*
1495+
* @example
1496+
* ```ts
1497+
* postProcess: [
1498+
* ({ compiledTypes, config }) => {
1499+
* const genericType = `export type MyGeneric<T> = { value: T };`
1500+
* return compiledTypes.replace(/(\/\*[\s\S]*?\*\/\n)/, `$1\n${genericType}\n`)
1501+
* },
1502+
* ]
1503+
* ```
1504+
*/
1505+
postProcess?: Array<(args: { compiledTypes: string; config: SanitizedConfig }) => string>
1506+
14891507
/**
14901508
* Allows you to modify the base JSON schema that is generated during generate:types. This JSON schema will be used
14911509
* to generate the TypeScript interfaces.

test/types/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,19 @@ export default buildConfigWithDefaults({
171171
typescript: {
172172
outputFile: path.resolve(dirname, 'payload-types.ts'),
173173
strictDraftTypes: true,
174+
postProcess: [
175+
({ compiledTypes }) => {
176+
const genericType = `export type TestPluginGeneric<T> = { value: T };`
177+
// Insert after banner comment
178+
return compiledTypes.replace(/(\/\*[\s\S]*?\*\/\n)/, `$1\n${genericType}\n`)
179+
},
180+
({ compiledTypes }) => {
181+
// Second function adds another type after the first
182+
return compiledTypes.replace(
183+
'export type TestPluginGeneric<T>',
184+
'export type SecondGeneric<K, V> = { key: K; value: V };\nexport type TestPluginGeneric<T>',
185+
)
186+
},
187+
],
174188
},
175189
})

test/types/int.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import fs from 'fs/promises'
2+
import path from 'path'
3+
import { fileURLToPath } from 'url'
4+
import { describe, expect, it } from 'vitest'
5+
6+
const dirname = path.dirname(fileURLToPath(import.meta.url))
7+
8+
describe('typescript.postProcess', () => {
9+
it('should apply postProcess functions to generated types', async () => {
10+
const outputFile = path.resolve(dirname, 'payload-types.ts')
11+
const content = await fs.readFile(outputFile, 'utf-8')
12+
13+
// Verify custom types were injected by postProcess
14+
expect(content).toContain('export type TestPluginGeneric<T> = { value: T };')
15+
expect(content).toContain('export type SecondGeneric<K, V> = { key: K; value: V };')
16+
})
17+
18+
it('should apply multiple postProcess functions in order', async () => {
19+
const outputFile = path.resolve(dirname, 'payload-types.ts')
20+
const content = await fs.readFile(outputFile, 'utf-8')
21+
22+
// SecondGeneric (from second function) should appear before TestPluginGeneric
23+
// because the second function prepends to TestPluginGeneric
24+
const secondIndex = content.indexOf('SecondGeneric')
25+
const firstIndex = content.indexOf('TestPluginGeneric')
26+
const configIndex = content.indexOf('export interface Config')
27+
28+
expect(secondIndex).toBeGreaterThan(-1)
29+
expect(firstIndex).toBeGreaterThan(-1)
30+
expect(secondIndex).toBeLessThan(firstIndex)
31+
expect(firstIndex).toBeLessThan(configIndex)
32+
})
33+
})

test/types/payload-types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
/* tslint:disable */
2+
3+
export type SecondGeneric<K, V> = { key: K; value: V };
4+
export type TestPluginGeneric<T> = { value: T };
25
/* eslint-disable */
36
/**
47
* This file was automatically generated by Payload.
@@ -116,6 +119,9 @@ export interface Config {
116119
settings: SettingsSelect<false> | SettingsSelect<true>;
117120
};
118121
locale: null;
122+
widgets: {
123+
collections: CollectionsWidget;
124+
};
119125
strictDraftTypes: true;
120126
user: User;
121127
jobs: {
@@ -482,6 +488,16 @@ export interface SettingsSelect<T extends boolean = true> {
482488
createdAt?: T;
483489
globalType?: T;
484490
}
491+
/**
492+
* This interface was referenced by `Config`'s JSON-Schema
493+
* via the `definition` "collections_widget".
494+
*/
495+
export interface CollectionsWidget {
496+
data?: {
497+
[k: string]: unknown;
498+
};
499+
width: 'full';
500+
}
485501
/**
486502
* This interface was referenced by `Config`'s JSON-Schema
487503
* via the `definition` "auth".

0 commit comments

Comments
 (0)