- 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"]
}All options are configured under the tsdoc namespace in your Prettier
configuration:
{
  "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,
    "extraTags": [],
    "normalizeTags": {
      "@return": "@returns",
      "@prop": "@property"
    },
    "releaseTagStrategy": "keep-first"
  }
}| 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 | null | 
"@internal" | 
Default release tag when none exists (null 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 | 
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 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:
{
  "tsdoc": {
    "defaultReleaseTag": "@internal", // Default tag to add
    "defaultReleaseTag": "@public", // Use @public instead
    "defaultReleaseTag": null // Disable feature
  }
}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 tonullto disable. - Parameter alignment: Disabled by default. Set 
alignParamTags: trueto enable. - Tag normalization: Only built-in normalizations (
@return→@returns) are applied by default. 
- 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", }, }