- Prettier Plugin for TSDoc
A Prettier plugin that formats TSDoc comments consistently.
- Structural Formatting: Consistent leading
/**, aligned*, controlled blank lines - Tag Normalization: Normalize common tag spelling variants (e.g.,
@return→@returns) - Parameter Alignment: Align parameter descriptions across
@paramtags - Tag Ordering: Canonical ordering of TSDoc tags for improved readability
- Example Formatting: Automatic blank lines before
@exampletags - Markdown & Code Support: Format markdown and fenced code blocks within comments
- Release Tag Management:
- AST-aware insertion: Only add release tags to exported API constructs
- API Extractor compatible: Follows inheritance rules for class/interface members
- Automatic insertion of default release tags (
@internalby default) - Deduplication of duplicate release tags (
@public,@beta, etc.) - Preservation of existing release tags
- Legacy Migration Support: Automatic transformation of legacy Closure Compiler annotations to modern TSDoc syntax
- Multi-language Code Formatting: Enhanced support for TypeScript, JavaScript, HTML, CSS, and more
- Embedded Formatting Control: Toggle fenced code block formatting per project using Prettier or plugin-specific options
- Performance Optimized: Efficient parsing with telemetry and debug support
- Highly Configurable: 14+ configuration options via Prettier config
- TypeDoc/AEDoc Compatible: Support for extended tag sets beyond core TSDoc
npm install @castlabs/prettier-tsdoc-pluginAdd the plugin to your Prettier configuration:
{
"plugins": ["@castlabs/prettier-tsdoc-plugin"]
}Options can be configured at the top level of your Prettier configuration
(recommended) or nested under the tsdoc namespace (for backward
compatibility):
Recommended (flat config):
{
"plugins": ["@castlabs/prettier-tsdoc-plugin"],
"fencedIndent": "space",
"normalizeTagOrder": true,
"dedupeReleaseTags": true,
"splitModifiers": true,
"singleSentenceSummary": false,
"alignParamTags": false,
"defaultReleaseTag": "@internal",
"onlyExportedAPI": true,
"inheritanceAware": true,
"closureCompilerCompat": true,
"releaseTagStrategy": "keep-first"
}Alternative (nested config - backward compatible):
{
"plugins": ["@castlabs/prettier-tsdoc-plugin"],
"tsdoc": {
"fencedIndent": "space",
"normalizeTagOrder": true,
"dedupeReleaseTags": true,
"splitModifiers": true,
"singleSentenceSummary": false,
"alignParamTags": false,
"defaultReleaseTag": "@internal",
"onlyExportedAPI": true,
"inheritanceAware": true,
"closureCompilerCompat": true,
"releaseTagStrategy": "keep-first"
}
}Note: The flat config approach is recommended as it avoids Prettier warnings about unknown options. Both formats are supported and work identically.
| Option | Type | Default | Description |
|---|---|---|---|
fencedIndent |
"space" | "none" |
"space" |
Indentation style for fenced code blocks |
normalizeTagOrder |
boolean |
true |
Normalize tag order based on conventional patterns (see below) |
dedupeReleaseTags |
boolean |
true |
Deduplicate release tags (@public, @beta, etc.) |
splitModifiers |
boolean |
true |
Split modifiers to separate lines |
singleSentenceSummary |
boolean |
false |
Enforce single sentence summaries |
embeddedLanguageFormatting |
"auto" | "off" |
"auto" |
Control embedded code block formatting (auto uses Prettier, off trims only) |
alignParamTags |
boolean |
false |
Align parameter descriptions across @param tags |
defaultReleaseTag |
string |
"@internal" |
Default release tag when none exists (empty string to disable) |
onlyExportedAPI |
boolean |
true |
Only add release tags to exported API constructs (AST-aware) |
inheritanceAware |
boolean |
true |
Respect inheritance rules - skip tagging class/interface members |
closureCompilerCompat |
boolean |
true |
Enable legacy Closure Compiler annotation transformations |
extraTags |
string[] |
[] |
Additional custom tags to recognize |
normalizeTags |
Record<string, string> |
{} |
Custom tag spelling normalizations |
releaseTagStrategy |
"keep-first" | "keep-last" |
"keep-first" |
Strategy for release tag deduplication |
preserveExampleNewline |
boolean |
true |
Preserve newlines after @example tags (prevents content from being treated as title) |
By default the plugin formats fenced code blocks inside TSDoc comments using Prettier’s language-specific parsers. You can opt out when the extra formatting is undesirable:
"auto"(default) delegates supported code fences to Prettier and preserves the existing async formatter behavior."off"skips the embedded Prettier call and instead trims leading/trailing whitespace inside the fenced block, matching the legacy heuristic formatter.
Precedence rules:
tsdoc.embeddedLanguageFormattingtakes priority when specified.- The global Prettier option
embeddedLanguageFormattingis respected when the TSDoc override is omitted. Setting it to"off"disables embedded formatting across the board, including inside this plugin. - When neither is provided, the plugin defaults to
"auto".
The plugin also exposes the option at the top level, so CLI usage such as
prettier --plugin @castlabs/prettier-tsdoc-plugin --embedded-language-formatting off
will disable snippet formatting in TSDoc comments as well.
The plugin includes these built-in normalizations:
@return→@returns@prop→@property
You can add custom normalizations or override built-in ones using the
normalizeTags option.
When normalizeTagOrder is enabled (default: true), TSDoc tags are reordered
into a canonical structure for improved readability:
- Input Parameters:
@paramand@typeParamtags - Output:
@returnstag - Error Conditions:
@throwstags - Deprecation Notices:
@deprecatedtag - Cross-references:
@seetags - Release Tags:
@public,@internal,@beta, etc. - Examples:
@exampletags (always last, with automatic blank lines)
Before (mixed order):
/**
* A complex function.
* @see https://example.com
* @beta
* @throws {Error} If input is invalid
* @returns The result
* @param a The first number
* @deprecated Use newFunction instead
* @example
* ```ts
* complexFunction(1, 2);
* ```
*/After (canonical order):
/**
* A complex function.
*
* @param a - The first number
* @returns The result
* @throws {Error} If input is invalid
* @deprecated Use newFunction instead
* @see https://example.com
* @beta
*
* @example
* ```ts
* complexFunction(1, 2);
* ```
*/Note: When normalizeTagOrder is false, the original tag order is
preserved as much as possible, though TSDoc's parsing structure may still impose
some organization.
The preserveExampleNewline option controls how content after @example tags
is formatted. This is important because TypeDoc and other documentation
renderers treat text on the same line as @example as a title, while
content on subsequent lines is treated as the body.
With preserveExampleNewline: true (default):
Content on a new line after @example stays on a new line, preserving the
semantic distinction:
// Input:
/**
* @example
* This is content, not a title
*/
// Output (unchanged):
/**
* @example
* This is content, not a title
*/With preserveExampleNewline: false (legacy behavior):
The first line of content is pulled up to the same line as @example:
// Input:
/**
* @example
* This is content, not a title
*/
// Output:
/**
* @example This is content, not a title
*/Note: When a title is intentionally placed on the same line as @example in
the source, it will be preserved regardless of this setting:
// This is always preserved:
/**
* @example My Example Title
* This is the body content
*/The following tags are considered release tags and can be deduplicated:
@public@beta@alpha@internal@experimental
The plugin uses AST analysis to intelligently determine which comments need release tags, following API Extractor conventions:
- Only exported declarations receive default release tags
- Class/interface members inherit from their container's release tag
- Namespace members inherit from the namespace's release tag
- Non-exported code remains untagged (not part of public API)
Configuration Options:
{
"defaultReleaseTag": "@internal", // Default tag to add
}{
"defaultReleaseTag": "@public", // Use @public instead
}{
"defaultReleaseTag": "", // Disable feature (empty string)
}Note: To disable automatic release tag insertion, use an empty string "".
While null works in TypeScript/JavaScript config files, JSON configurations
require an empty string.
Example - AST-aware insertion for exported functions:
Input:
/**
* Exported helper function.
* @param value - Input value
* @returns Processed value
*/
export function helper(value: string): string {
return value.trim();
}
/**
* Internal helper (not exported).
* @param value - Input value
*/
function internal(value: string): void {
console.log(value);
}Output:
/**
* Exported helper function.
* @internal
* @param value - Input value
* @returns Processed value
*/
export function helper(value: string): string {
return value.trim();
}
/**
* Internal helper (not exported).
* @param value - Input value
*/
function internal(value: string): void {
console.log(value);
}Example - Existing tags are preserved:
Input:
/**
* Public API function.
* @public
* @param data - Input data
*/
function publicApi(data: any): void {}Output (no change):
/**
* Public API function.
* @public
* @param data - Input data
*/
function publicApi(data: any): void {}Example - Inheritance rules (class members inherit from class):
Input:
/**
* Widget class for the public API.
* @public
*/
export class Widget {
/**
* Method that inherits @public from class.
* @param value - Input value
*/
process(value: string): void {
// implementation
}
}Output (no change - method inherits @public from class):
/**
* Widget class for the public API.
* @public
*/
export class Widget {
/**
* Method that inherits @public from class.
* @param value - Input value
*/
process(value: string): void {
// implementation
}
}Phase 130 Feature - The plugin provides automatic transformation of legacy Google Closure Compiler annotations to modern TSDoc/JSDoc syntax, making it easy to migrate older JavaScript codebases to modern tooling.
{
"tsdoc": {
"closureCompilerCompat": true // Default: true - enabled by default
}
}The plugin automatically modernizes the following legacy annotations:
@export→@public@protected→@internal@private→@internal
@param {type} name→@param name@throws {Error}→@throws(when type is the only content)@this {type}→@this
@extends {BaseClass}→ (removed)@implements {IInterface}→ (removed)
Note: Only curly-brace syntax is removed. Modern TypeDoc overrides like
@extends BaseClass (without braces) are preserved.
@constructor→ (removed)@const→ (removed)@define→ (removed)@noalias→ (removed)@nosideeffects→ (removed)
@see http://example.com→@see {@link http://example.com}@see MyClass→@see {@link MyClass}(code constructs only)@see Also check the documentation→ (unchanged - descriptive text preserved)
{@code expression}→`expression`
@tutorial tutorialName→@document tutorialName- References: TypeDoc @document tag
@default value→@defaultValue value
@fileoverview description→ Summary content +@packageDocumentation- Special handling: Content is moved to summary, tag is placed at bottom
Example 1: Core Legacy Transformations
Before (Legacy Closure Compiler):
/**
* Creates a new widget with configuration.
* @constructor
* @param {string} id - The unique identifier for the widget.
* @param {object} [options] - Configuration options.
* @extends {BaseWidget}
* @implements {IWidget}
* @export
* @see MyOtherClass
* @see http://example.com/docs
*/After (Modern TSDoc):
/**
* Creates a new widget with configuration.
*
* @param id - The unique identifier for the widget.
* @param [options] - Configuration options.
* @public
* @see {@link MyOtherClass}
* @see {@link http://example.com/docs}
*/Example 2: New Tag Transformations
Before (Legacy with new transformations):
/**
* @fileoverview This module provides utility functions for data processing.
* It includes various helper methods for validation and transformation.
*/
/**
* Processes values with {@code let x = getValue();} syntax.
* @tutorial getting-started
* @default null
* @param value - The input value
*/
function processValue(value: string = null) {
// implementation
}After (Modern TSDoc):
/**
* This module provides utility functions for data processing.
* It includes various helper methods for validation and transformation.
*
* @packageDocumentation
*/
/**
* Processes values with `let x = getValue();` syntax.
*
* @param value - The input value
* @document getting-started
* @defaultValue null
*/
function processValue(value: string = null) {
// implementation
}The transformation engine uses intelligent pattern recognition to avoid false positives:
- Code blocks are protected: Transformations skip content inside ``` fenced blocks
- Prose detection:
@see First referenceis NOT transformed (common English words) - Code construct detection:
@see MyClassIS transformed (follows naming patterns) - Partial transformations:
@throws {Error} When invalidpreserves the description
Legacy transformations work seamlessly with all other plugin features:
- Tag ordering: Transformed tags participate in canonical ordering
- Release tag deduplication: Duplicate tags are removed after transformation
- Parameter alignment: Transformed
@paramtags align properly - Markdown formatting: Content formatting applies after transformation
To disable legacy transformations (e.g., for modern codebases):
{
"tsdoc": {
"closureCompilerCompat": false
}
}- Enable the plugin with default settings (
closureCompilerCompat: true) - Run Prettier on your legacy codebase - transformations happen automatically
- Review changes - the process is conservative and avoids false positives
- Commit transformed code - all legacy annotations are now modern TSDoc
- Optional: Disable
closureCompilerCompatonce migration is complete
Input:
/**
* Calculate the sum of two numbers.
* @param a - First number
* @return Second number result
*/
function add(a: number, b: number): number {
return a + b;
}Output:
/**
* Calculate the sum of two numbers.
* @param a - First number
* @returns Second number result
*/
function add(a: number, b: number): number {
return a + b;
}Input:
/**
* Process data with example:
* ```typescript
* const result=process({value:42});
* ```
*/
function process(data: any): any {
return data;
}Output:
/**
* Process data with example:
* ```typescript
* const result = process({ value: 42 });
* ```
*/
function process(data: any): any {
return data;
}Input:
/**
* Internal function.
* @public
* @param x - Value
* @public
* @beta
*/
function internalFn(x: number): void {}Output (with dedupeReleaseTags: true, releaseTagStrategy: "keep-first"):
/**
* Internal function.
* @public
* @param x - Value
* @beta
*/
function internalFn(x: number): void {}With alignParamTags: true:
Input:
/**
* Function with parameters.
* @param shortName - Short description
* @param veryLongParameterName - Long description that may wrap
* @param id - ID value
* @returns Result
*/
function example(
shortName: string,
veryLongParameterName: string,
id: number
): string {
return '';
}Output:
/**
* Function with parameters.
* @param shortName - Short description
* @param veryLongParameterName - Long description that may wrap
* @param id - ID value
* @returns Result
*/
function example(
shortName: string,
veryLongParameterName: string,
id: number
): string {
return '';
}- Small comments (< 100 chars): ~5ms average formatting time
- Medium comments (100-500 chars): ~15-20ms average formatting time
- Large comments (> 500 chars): ~40-50ms average formatting time
- Memory usage: Stable, no memory leaks detected
- Cache efficiency: Parser and configuration caching for optimal performance
- Use consistent configuration: Avoid changing TSDoc options frequently to benefit from parser caching
- Limit custom tags: Excessive
extraTagscan reduce parser cache efficiency - Consider comment size: Very large comments (> 1000 chars) may exceed 10ms formatting budget
- Enable markdown caching: Repeated markdown/code blocks are automatically cached
- Monitor with debug mode: Use
PRETTIER_TSDOC_DEBUG=1to track performance metrics
Set the PRETTIER_TSDOC_DEBUG=1 environment variable to enable debug telemetry:
PRETTIER_TSDOC_DEBUG=1 npx prettier --write "**/*.ts"This will log performance metrics including:
- Comments processed count
- Parse error frequency
- Average formatting time per comment
- Cache hit rates
- Memory usage patterns
Run the included benchmarks to measure performance on your system:
npm run benchmarkThis project uses Rollup to create a single bundled entry point. The build process includes:
- TypeScript compilation: TypeScript is compiled to JavaScript with source maps
- Bundling: All source files are bundled into a single
dist/index.jsfile - Source maps: Generated for debugging support
# Build the plugin bundle
npm run build
# Type checking only (no JavaScript output)
npm run typecheck
# Run tests
npm testThe build output consists of:
dist/index.js- Single bundled entry pointdist/index.js.map- Source map for debugging
The plugin is designed to be backward-compatible. New AST-aware features are enabled by default:
- AST-aware release tags: Enabled by default (
onlyExportedAPI: true). Set tofalsefor legacy behavior. - Inheritance awareness: Enabled by default (
inheritanceAware: true). Set tofalseto tag all constructs. - Default release tags: Enabled by default with
@internal. Set to empty string""to disable (ornullin TypeScript/JavaScript config files). - Parameter alignment: Disabled by default. Set
alignParamTags: trueto enable. - Tag normalization: Only built-in normalizations (
@return→@returns) are applied by default. - Configuration format: Use flat config (top-level options) instead of
nested
tsdoc: {}to avoid Prettier warnings about unknown options. Both formats work identically.
Symptom: [warn] Ignored unknown option { tsdoc: { ... } }
Solution: Use flat configuration (top-level options) instead of nested
tsdoc: {} format:
{
"plugins": ["@castlabs/prettier-tsdoc-plugin"],
"defaultReleaseTag": "@internal",
"onlyExportedAPI": true
}Both formats work identically, but flat config avoids Prettier warnings.
- Check comment syntax: Only
/** */comments are processed, not/* */or// - Check debug output: Use
PRETTIER_TSDOC_DEBUG=1to see which comments are being processed
- Large files: Comments > 1000 characters may take longer to format
- Custom tags: Excessive
extraTagscan impact performance - Debug mode: Use
PRETTIER_TSDOC_DEBUG=1to identify slow comments
- Tag normalization: Built-in normalizations are applied by default
- Legacy Closure Compiler transformations: Enabled by default
(
closureCompilerCompat: true) - AST-aware release tag insertion: Only exported declarations get default tags
- Inheritance rules: Class/interface members inherit from container
- Custom normalizations: Check your
normalizeTagsconfiguration
- Check export status: Only exported declarations get default tags with
onlyExportedAPI: true - Check inheritance: Class members inherit from class release tag
- Disable AST analysis: Set
onlyExportedAPI: falsefor legacy behavior - Debug AST analysis: Use
PRETTIER_TSDOC_DEBUG=1to see analysis results
To validate your configuration, use this TypeScript interface:
interface TSDocPluginOptions {
fencedIndent?: 'space' | 'none';
normalizeTagOrder?: boolean;
dedupeReleaseTags?: boolean;
splitModifiers?: boolean;
singleSentenceSummary?: boolean;
alignParamTags?: boolean;
defaultReleaseTag?: string | null;
onlyExportedAPI?: boolean;
inheritanceAware?: boolean;
closureCompilerCompat?: boolean;
extraTags?: string[];
normalizeTags?: Record<string, string>;
releaseTagStrategy?: 'keep-first' | 'keep-last';
}Phase 130 (Legacy Closure Compiler Support) - ✅ COMPLETED
All phases of the implementation plan have been completed successfully:
- ✅ Phase 1: Bootstrap
- ✅ Phase 2: Parser Detection
- ✅ Phase 3: Summary & Remarks
- ✅ Phase 4: Tags & Alignment
- ✅ Phase 5: Markdown & Codeblocks
- ✅ Phase 6: Configuration & Normalization
- ✅ Phase 7: Edge Cases & Performance
- ✅ Phase 8: Release Tags
- ✅ Phase 110: Newline and Tag Management
- ✅ Phase 130: Legacy Closure Compiler Support
See agents/context.md for the detailed specification.
MIT
{ "plugins": ["@castlabs/prettier-tsdoc-plugin"], "tsdoc": { "embeddedLanguageFormatting": "off", }, }