Skip to content

Commit 7639664

Browse files
authored
feat: typescript plugin for import paths (#15779)
Adds `@payloadcms/typescript-plugin`, a TypeScript language service plugin that validates PayloadComponent import paths in the IDE. https://github.com/user-attachments/assets/2a8a8f73-a8fd-4e79-9dc4-795d817b5b75 It checks that referenced files and exports actually exist, provides autocomplete for both file paths and export names, and supports go-to-definition on component path strings. The plugin understands all of Payload's path conventions including absolute paths relative to baseDir, relative paths, tsconfig aliases, and package imports. It auto-detects baseDir by finding the nearest payload config file and can be overridden via tsconfig plugin options.
1 parent 02da3fd commit 7639664

22 files changed

Lines changed: 1992 additions & 4 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"css.validate": false,
2929
"scss.validate": false,
3030
"typescript.tsdk": "node_modules/typescript/lib",
31+
"typescript.enablePromptUseWorkspaceTsdk": true,
3132
// Load .git-blame-ignore-revs file
3233
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
3334
// Essentially disables bun test buttons

docs/typescript/ts-plugin.mdx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
title: TypeScript Plugin (Experimental)
3+
label: TypeScript Plugin
4+
order: 30
5+
desc: IDE support for PayloadComponent import paths – inline validation, autocomplete, and go-to-definition.
6+
keywords: headless cms, typescript, documentation, ide, plugin, autocomplete, validation, import paths
7+
---
8+
9+
<Banner type="warning">
10+
**Experimental** — This plugin is experimental and may change in future
11+
releases. Please [report any
12+
issues](https://github.com/payloadcms/payload/issues) you encounter.
13+
</Banner>
14+
15+
Payload ships an optional TypeScript Language Service Plugin (`@payloadcms/typescript-plugin`) that enhances your IDE experience when working with [Custom Components](../custom-components/overview). It understands Payload's `PayloadComponent` import path conventions and provides:
16+
17+
- **Path validation** — red squigglies when a component path doesn't resolve to a real file
18+
- **Export validation** — errors when the named export doesn't exist in the target module, with "Did you mean?" suggestions
19+
- **Autocomplete** — file and directory suggestions while typing the path, and export name suggestions after `#`
20+
- **Go-to-definition** — Ctrl/Cmd+click on a component path string to jump to the component's source
21+
22+
## Installation
23+
24+
Install the plugin as a dev dependency:
25+
26+
```bash
27+
pnpm add -D @payloadcms/typescript-plugin
28+
```
29+
30+
Then add it to the `plugins` array in your `tsconfig.json`:
31+
32+
```json
33+
{
34+
"compilerOptions": {
35+
"plugins": [{ "name": "next" }, { "name": "@payloadcms/typescript-plugin" }]
36+
}
37+
}
38+
```
39+
40+
### VS Code / Cursor Setup
41+
42+
TypeScript language service plugins only load when the editor uses the **workspace version** of TypeScript (the one installed in your project's `node_modules`). By default, VS Code uses its own bundled version which won't load the plugin.
43+
44+
To switch: open the command palette (`Cmd+Shift+P`) and run **"TypeScript: Select TypeScript Version"**, then choose **"Use Workspace Version"**.
45+
46+
For teams, add the following to `.vscode/settings.json` so everyone is prompted to switch:
47+
48+
```json
49+
{
50+
"typescript.tsdk": "node_modules/typescript/lib",
51+
"typescript.enablePromptUseWorkspaceTsdk": true
52+
}
53+
```
54+
55+
After selecting the workspace version, restart the TypeScript server (`Cmd+Shift+P` → "TypeScript: Restart TS Server").
56+
57+
<Banner type="info">
58+
TypeScript Language Service Plugins only run inside your IDE. They do not
59+
affect `tsc` compilation or your build output.
60+
</Banner>
61+
62+
## Supported Path Conventions
63+
64+
The plugin supports all of Payload's component path formats:
65+
66+
| Format | Example | Resolution |
67+
| ----------------------- | ---------------------------------- | ------------------------------ |
68+
| Absolute (from baseDir) | `'/components/MyField#MyField'` | Resolved relative to `baseDir` |
69+
| Relative | `'./components/MyField#MyField'` | Resolved relative to `baseDir` |
70+
| tsconfig alias | `'@/components/MyField#MyField'` | Resolved via tsconfig `paths` |
71+
| Package import | `'@payloadcms/ui/rsc#MyComponent'` | Resolved via node_modules |
72+
| Default export | `'/components/MyField'` | Uses `default` export |
73+
74+
Both the **string form** and the **object form** are supported:
75+
76+
```ts
77+
// String form
78+
{
79+
admin: {
80+
components: {
81+
Field: '/components/MyField#MyField',
82+
}
83+
}
84+
}
85+
86+
// Object form
87+
{
88+
admin: {
89+
components: {
90+
Field: {
91+
path: '/components/MyField',
92+
exportName: 'MyField',
93+
}
94+
}
95+
}
96+
}
97+
```
98+
99+
## Configuration
100+
101+
### Base Directory
102+
103+
The plugin automatically detects `baseDir` by walking up from the current file to find the nearest `payload.config.ts`. Absolute paths (starting with `/`) and relative paths (starting with `./`) are resolved relative to this directory.
104+
105+
If your project uses a non-standard layout, you can override `baseDir` in the plugin config:
106+
107+
```json
108+
{
109+
"compilerOptions": {
110+
"plugins": [
111+
{
112+
"name": "@payloadcms/typescript-plugin",
113+
"baseDir": "./src"
114+
}
115+
]
116+
}
117+
}
118+
```
119+
120+
The `baseDir` path is relative to the `tsconfig.json` location.
121+
122+
## How It Works
123+
124+
The plugin detects `PayloadComponent` positions by checking the contextual type of string literals in your config. Any string typed as `PayloadComponent`, `CustomComponent`, or any type that resolves to the same `false | RawPayloadComponent | string` union shape is automatically validated.
125+
126+
This means the plugin works everywhere Payload expects a component path — collection field components, global components, admin panel components, dashboard widgets, and custom views — without needing to hardcode specific config positions.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
"lint:fix": "turbo run lint:fix --log-order=grouped --continue --filter \"!blank\" --filter \"!website\" --filter \"!ecommerce\"",
107107
"lint:scss": "stylelint \"packages/ui/src/**/*.scss\"",
108108
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
109-
"prepare": "husky",
109+
"prepare": "husky && pnpm turbo build --filter @payloadcms/typescript-plugin",
110110
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
111111
"prepare-run-test-against-prod:ci": "rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
112112
"publish-prerelease": "pnpm --filter releaser publish-prerelease",
@@ -163,6 +163,7 @@
163163
"@payloadcms/eslint-config": "workspace:*",
164164
"@payloadcms/eslint-plugin": "workspace:*",
165165
"@payloadcms/live-preview-react": "workspace:*",
166+
"@payloadcms/typescript-plugin": "workspace:*",
166167
"@playwright/test": "1.58.2",
167168
"@sentry/nextjs": "^8.33.1",
168169
"@sentry/node": "^8.33.1",

packages/payload/src/versions/payloadPackageList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const PAYLOAD_PACKAGE_LIST = [
2929
'@payloadcms/plugin-seo',
3030
'@payloadcms/plugin-stripe',
3131
'@payloadcms/plugin-zapier',
32+
'@payloadcms/typescript-plugin',
3233
'@payloadcms/richtext-lexical',
3334
'@payloadcms/richtext-slate',
3435
'@payloadcms/sdk',
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "https://json.schemastore.org/swcrc",
3+
"sourceMaps": true,
4+
"exclude": [
5+
"/**/*.spec.ts"
6+
],
7+
"jsc": {
8+
"target": "esnext",
9+
"parser": {
10+
"syntax": "typescript",
11+
"tsx": false,
12+
"dts": true
13+
}
14+
},
15+
"module": {
16+
"type": "commonjs"
17+
}
18+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "@payloadcms/typescript-plugin",
3+
"version": "3.77.0",
4+
"description": "TypeScript Language Service Plugin for Payload CMS - validates PayloadComponent import paths and provides autocomplete",
5+
"homepage": "https://payloadcms.com",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/payloadcms/payload.git",
9+
"directory": "packages/typescript-plugin"
10+
},
11+
"license": "MIT",
12+
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
13+
"maintainers": [
14+
{
15+
"name": "Payload",
16+
"email": "info@payloadcms.com",
17+
"url": "https://payloadcms.com"
18+
}
19+
],
20+
"sideEffects": false,
21+
"exports": {
22+
".": {
23+
"types": "./dist/index.d.ts",
24+
"default": "./dist/index.js"
25+
}
26+
},
27+
"main": "./dist/index.js",
28+
"types": "./dist/index.d.ts",
29+
"files": [
30+
"dist"
31+
],
32+
"scripts": {
33+
"build": "rimraf tsconfig.tsbuildinfo && pnpm build:types && pnpm build:swc",
34+
"build:debug": "pnpm build",
35+
"build:swc": "swc ./src -d ./dist --config-file .swcrc-build --strip-leading-paths",
36+
"build:types": "tsc --emitDeclarationOnly --outDir dist",
37+
"clean": "rimraf -g {dist,*.tsbuildinfo}",
38+
"lint": "eslint .",
39+
"lint:fix": "eslint . --fix",
40+
"prepublishOnly": "pnpm clean && pnpm turbo build"
41+
},
42+
"devDependencies": {
43+
"typescript": "5.7.3"
44+
},
45+
"engines": {
46+
"node": "^18.20.2 || >=20.9.0"
47+
},
48+
"publishConfig": {
49+
"exports": {
50+
".": {
51+
"types": "./dist/index.d.ts",
52+
"default": "./dist/index.js"
53+
}
54+
},
55+
"main": "./dist/index.js",
56+
"registry": "https://registry.npmjs.org/",
57+
"types": "./dist/index.d.ts"
58+
}
59+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const MyField = () => null
2+
export const MyLabel = () => null
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const Icon = () => null
2+
export const IconSmall = () => null
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const CustomView = () => null
2+
export default CustomView
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
declare type RawPayloadComponent = {
2+
clientProps?: object
3+
exportName?: string
4+
path: string
5+
serverProps?: object
6+
}
7+
8+
declare type PayloadComponent = false | RawPayloadComponent | string
9+
10+
declare type CustomComponent = PayloadComponent
11+
12+
declare interface FieldConfig {
13+
admin?: {
14+
components?: {
15+
Cell?: PayloadComponent
16+
Description?: PayloadComponent
17+
Field?: PayloadComponent
18+
Label?: PayloadComponent
19+
}
20+
}
21+
name: string
22+
type: string
23+
}
24+
25+
declare interface AdminConfig {
26+
components?: {
27+
actions?: CustomComponent[]
28+
graphics?: {
29+
Icon?: PayloadComponent
30+
Logo?: PayloadComponent
31+
}
32+
Nav?: PayloadComponent
33+
views?: Record<
34+
string,
35+
{
36+
Component?: PayloadComponent
37+
path?: string
38+
}
39+
>
40+
}
41+
}

0 commit comments

Comments
 (0)