Skip to content
Open
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 @@ -15,6 +15,7 @@
{ "rule": "object-shorthand", "severity": "off", "fixable": true }
],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.tsserver.pluginPaths": ["${workspaceFolder}/tools/ts-plugin-inherit-doc"],
// Load .git-blame-ignore-revs file
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
"@payloadcms/ts-plugin-inherit-doc": "workspace:*",
"@playwright/test": "1.54.1",
"@sentry/nextjs": "^8.33.1",
"@sentry/node": "^8.33.1",
Expand Down
105 changes: 49 additions & 56 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions tools/ts-plugin-inherit-doc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# TypeScript JSDoc Inheritance Plugin

A TypeScript Language Service Plugin that enables documentation inheritance via `@inheritDoc` tag.

## Usage

1. Build the plugin:

```bash
cd tools/ts-plugin-inherit-doc
pnpm install
pnpm build
```

2. Add to your `tsconfig.json`:

```json
{
"compilerOptions": {
"plugins": [{ "name": "@payloadcms/ts-plugin-inherit-doc" }]
}
}
```

3. Use in your code:

```javascript
/**
* Represents a user in the system
* @typedef {Object} User
* @property {string} id - Unique identifier
* @property {string} name - User's full name
*/

/**
* @typedef {Object} UserResponse
* @property {User} user - @inheritDoc User
* @property {string} token - Auth token
*/
```

When you hover over `user` property, you'll see the inherited documentation from `User`.

## Notes

- The plugin rebuilds the documentation cache on each hover (optimization needed for production)
- Only works with `@typedef` JSDoc declarations
- VSCode requires TypeScript workspace version to use plugins
20 changes: 20 additions & 0 deletions tools/ts-plugin-inherit-doc/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/** @type {import('jest').Config} */
const customJestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'\\.(css|scss)$': '<rootDir>/helpers/mocks/emptyModule.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/test/helpers/mocks/fileMock.js',
'^(\\.{1,2}/.*)\\.js$': '$1',
},
testEnvironment: 'node',
testMatch: ['<rootDir>/**/*spec.ts'],
testTimeout: 160000,
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},
verbose: true,
}

export default customJestConfig
25 changes: 25 additions & 0 deletions tools/ts-plugin-inherit-doc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@payloadcms/ts-plugin-inherit-doc",
"version": "1.0.0",
"description": "TypeScript Language Service Plugin for JSDoc documentation inheritance",
"keywords": [
"typescript",
"plugin",
"jsdoc"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest",
"watch": "tsc --watch"
},
"dependencies": {
"typescript": "^5.7.2"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^22.15.30",
"jest": "^29.7.0"
}
}
156 changes: 156 additions & 0 deletions tools/ts-plugin-inherit-doc/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as ts from 'typescript/lib/tsserverlibrary'

// eslint-disable-next-line @typescript-eslint/no-require-imports
const pluginFactory = require('./index')

describe('ts-plugin-inherit-doc', () => {
let tempDir: string
let testFilePath: string

beforeEach(() => {
// Create temporary directory for test files
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ts-plugin-test-'))
testFilePath = path.join(tempDir, 'test.ts')
})

afterEach(() => {
// Clean up
fs.rmSync(tempDir, { recursive: true, force: true })
})

function createLanguageService(fileContent: string): ts.LanguageService {
fs.writeFileSync(testFilePath, fileContent)

const servicesHost: ts.LanguageServiceHost = {
getScriptFileNames: () => [testFilePath],
getScriptVersion: () => '1',
getScriptSnapshot: (fileName) => {
if (fileName === testFilePath) {
const text = fs.readFileSync(fileName, 'utf8')
return ts.ScriptSnapshot.fromString(text)
}
return undefined
},
getCurrentDirectory: () => tempDir,
getCompilationSettings: () => ({
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.CommonJS,
}),
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
readDirectory: ts.sys.readDirectory,
directoryExists: ts.sys.directoryExists,
getDirectories: ts.sys.getDirectories,
}

const languageService = ts.createLanguageService(servicesHost)

// Initialize plugin
const plugin = pluginFactory({ typescript: ts })
const pluginInfo: ts.server.PluginCreateInfo = {
languageService,
languageServiceHost: servicesHost,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
project: {} as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serverHost: {} as any,
config: {},
}

return plugin.create(pluginInfo)
}

it('plugin adds debug marker to all hovers', () => {
const content = `
type MyType = {
prop: string
}
`
const ls = createLanguageService(content)
const position = content.indexOf('prop')
const quickInfo = ls.getQuickInfoAtPosition(testFilePath, position)

expect(quickInfo).toBeDefined()
expect(quickInfo!.documentation).toBeDefined()
})

it('inherits documentation from referenced type', () => {
const content = `
/**
* Base type with documentation
*/
type BaseType = {
/**
* A property
*/
prop: string
}

type DerivedType = {
/**
* @inheritDoc BaseType
*/
field: BaseType
}
`
const ls = createLanguageService(content)
const position = content.indexOf('field:')
const quickInfo = ls.getQuickInfoAtPosition(testFilePath, position)

expect(quickInfo).toBeDefined()
const docText = quickInfo!.documentation!.map((p) => p.text).join('')
expect(docText).toContain('Base type with documentation')
expect(docText).toContain('prop: A property')
})

it('shows debug message when type not found', () => {
const content = `
type MyType = {
/**
* @inheritDoc NonExistentType
*/
field: string
}
`
const ls = createLanguageService(content)
const position = content.indexOf('field:')
const quickInfo = ls.getQuickInfoAtPosition(testFilePath, position)

expect(quickInfo).toBeDefined()
const docText = quickInfo!.documentation!.map((p) => p.text).join('')
expect(docText).toContain('NonExistentType')
expect(docText).toContain('not found')
})

it('works with interface declarations', () => {
const content = `
/**
* Interface documentation
*/
interface MyInterface {
/**
* Method docs
*/
method(): void
}

type MyType = {
/**
* @inheritDoc MyInterface
*/
field: MyInterface
}
`
const ls = createLanguageService(content)
const position = content.lastIndexOf('field:')
const quickInfo = ls.getQuickInfoAtPosition(testFilePath, position)

expect(quickInfo).toBeDefined()
const docText = quickInfo!.documentation!.map((p) => p.text).join('')
expect(docText).toContain('Interface documentation')
})
})
Loading
Loading