Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"css.validate": false,
"scss.validate": false,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
// Load .git-blame-ignore-revs file
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
// Essentially disables bun test buttons
Expand Down
126 changes: 126 additions & 0 deletions docs/typescript/ts-plugin.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
title: TypeScript Plugin (Experimental)
label: TypeScript Plugin
order: 30
desc: IDE support for PayloadComponent import paths – inline validation, autocomplete, and go-to-definition.
keywords: headless cms, typescript, documentation, ide, plugin, autocomplete, validation, import paths
---

<Banner type="warning">
**Experimental** — This plugin is experimental and may change in future
releases. Please [report any
issues](https://github.com/payloadcms/payload/issues) you encounter.
</Banner>

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:

- **Path validation** — red squigglies when a component path doesn't resolve to a real file
- **Export validation** — errors when the named export doesn't exist in the target module, with "Did you mean?" suggestions
- **Autocomplete** — file and directory suggestions while typing the path, and export name suggestions after `#`
- **Go-to-definition** — Ctrl/Cmd+click on a component path string to jump to the component's source

## Installation

Install the plugin as a dev dependency:

```bash
pnpm add -D @payloadcms/typescript-plugin
```

Then add it to the `plugins` array in your `tsconfig.json`:

```json
{
"compilerOptions": {
"plugins": [{ "name": "next" }, { "name": "@payloadcms/typescript-plugin" }]
}
}
```

### VS Code / Cursor Setup

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.

To switch: open the command palette (`Cmd+Shift+P`) and run **"TypeScript: Select TypeScript Version"**, then choose **"Use Workspace Version"**.

For teams, add the following to `.vscode/settings.json` so everyone is prompted to switch:

```json
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
```

After selecting the workspace version, restart the TypeScript server (`Cmd+Shift+P` → "TypeScript: Restart TS Server").

<Banner type="info">
TypeScript Language Service Plugins only run inside your IDE. They do not
affect `tsc` compilation or your build output.
</Banner>

## Supported Path Conventions

The plugin supports all of Payload's component path formats:

| Format | Example | Resolution |
| ----------------------- | ---------------------------------- | ------------------------------ |
| Absolute (from baseDir) | `'/components/MyField#MyField'` | Resolved relative to `baseDir` |
| Relative | `'./components/MyField#MyField'` | Resolved relative to `baseDir` |
| tsconfig alias | `'@/components/MyField#MyField'` | Resolved via tsconfig `paths` |
| Package import | `'@payloadcms/ui/rsc#MyComponent'` | Resolved via node_modules |
| Default export | `'/components/MyField'` | Uses `default` export |

Both the **string form** and the **object form** are supported:

```ts
// String form
{
admin: {
components: {
Field: '/components/MyField#MyField',
}
}
}

// Object form
{
admin: {
components: {
Field: {
path: '/components/MyField',
exportName: 'MyField',
}
}
}
}
```

## Configuration

### Base Directory

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.

If your project uses a non-standard layout, you can override `baseDir` in the plugin config:

```json
{
"compilerOptions": {
"plugins": [
{
"name": "@payloadcms/typescript-plugin",
"baseDir": "./src"
}
]
}
}
```

The `baseDir` path is relative to the `tsconfig.json` location.

## How It Works

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.

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.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"lint:fix": "turbo run lint:fix --log-order=grouped --continue --filter \"!blank\" --filter \"!website\" --filter \"!ecommerce\"",
"lint:scss": "stylelint \"packages/ui/src/**/*.scss\"",
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
"prepare": "husky",
"prepare": "husky && pnpm turbo build --filter @payloadcms/typescript-plugin",
"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 ..",
"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 ..",
"publish-prerelease": "pnpm --filter releaser publish-prerelease",
Expand Down Expand Up @@ -163,6 +163,7 @@
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
"@payloadcms/typescript-plugin": "workspace:*",
"@playwright/test": "1.58.2",
"@sentry/nextjs": "^8.33.1",
"@sentry/node": "^8.33.1",
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/versions/payloadPackageList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const PAYLOAD_PACKAGE_LIST = [
'@payloadcms/plugin-seo',
'@payloadcms/plugin-stripe',
'@payloadcms/plugin-zapier',
'@payloadcms/typescript-plugin',
'@payloadcms/richtext-lexical',
'@payloadcms/richtext-slate',
'@payloadcms/sdk',
Expand Down
18 changes: 18 additions & 0 deletions packages/typescript-plugin/.swcrc-build
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"exclude": [
"/**/*.spec.ts"
],
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": false,
"dts": true
}
},
"module": {
"type": "commonjs"
}
}
59 changes: 59 additions & 0 deletions packages/typescript-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "@payloadcms/typescript-plugin",
"version": "3.77.0",
"description": "TypeScript Language Service Plugin for Payload CMS - validates PayloadComponent import paths and provides autocomplete",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/typescript-plugin"
},
"license": "MIT",
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
"maintainers": [
{
"name": "Payload",
"email": "info@payloadcms.com",
"url": "https://payloadcms.com"
}
],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "rimraf tsconfig.tsbuildinfo && pnpm build:types && pnpm build:swc",
"build:debug": "pnpm build",
"build:swc": "swc ./src -d ./dist --config-file .swcrc-build --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf -g {dist,*.tsbuildinfo}",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"devDependencies": {
"typescript": "5.7.3"
},
"engines": {
"node": "^18.20.2 || >=20.9.0"
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MyField = () => null
export const MyLabel = () => null
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const Icon = () => null
export const IconSmall = () => null
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const CustomView = () => null
export default CustomView
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
declare type RawPayloadComponent = {
clientProps?: object
exportName?: string
path: string
serverProps?: object
}

declare type PayloadComponent = false | RawPayloadComponent | string

declare type CustomComponent = PayloadComponent

declare interface FieldConfig {
admin?: {
components?: {
Cell?: PayloadComponent
Description?: PayloadComponent
Field?: PayloadComponent
Label?: PayloadComponent
}
}
name: string
type: string
}

declare interface AdminConfig {
components?: {
actions?: CustomComponent[]
graphics?: {
Icon?: PayloadComponent
Logo?: PayloadComponent
}
Nav?: PayloadComponent
views?: Record<
string,
{
Component?: PayloadComponent
path?: string
}
>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const NavIcon = () => null
export const NavIconSmall = () => null
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const Badge = () => null
export const BadgeIcon = () => null
export default Badge
Loading