sdxc

Type to search, or start from one of these:

@sdxc/jsdoc

Read JSDoc out of JavaScript and TypeScript source text into a JSON documentation model

npm add @sdxc/jsdoc
pnpm add @sdxc/jsdoc
yarn add @sdxc/jsdoc
bun add @sdxc/jsdoc
Installs with
@sdxc/result

Read JSDoc out of JavaScript and TypeScript source text into a JSON documentation model.

Installation

npm add @sdxc/jsdoc

extract() reports failures as a Result from @sdxc/result, which installs alongside this package.

Usage

Extract One Module

import { extract } from "@sdxc/jsdoc";
import { isFailure } from "@sdxc/result";

let source = `
/** Adds two numbers. */
export function add(a: number, b: number): number {
	return a + b;
}
`;

let result = extract(source, { path: "src/add.ts" });
if (isFailure(result)) throw result.error;

result.data.children[0]?.name; // "add"
result.data.children[0]?.signatures[0]?.returns; // "number"

Build The Document A Site Reads

Extract every module a package ships, then assemble them into one DocProject.

import type { DocModule } from "@sdxc/jsdoc";

import { extract, SCHEMA_VERSION } from "@sdxc/jsdoc";
import { isFailure } from "@sdxc/result";

let files = new Map([
	["src/index.ts", 'export { add } from "./add.js";'],
	[
		"src/add.ts",
		"/** Adds two numbers. */\nexport function add(a: number, b: number) {\n\treturn a + b;\n}",
	],
]);

let modules: DocModule[] = [];
for (let [path, source] of files) {
	let result = extract(source, { path });
	if (isFailure(result)) throw result.error;
	modules.push(result.data);
}

let project = { schema: SCHEMA_VERSION, name: "math", version: "1.4.0", modules };

Parse A Comment On Its Own

import { findTag, parseComment } from "@sdxc/jsdoc";

let comment = parseComment("/** Adds. @param a - The addend. @returns The sum. */");

comment.description; // "Adds."
findTag(comment, "returns")?.text; // "The sum."

API

extract(source: string, options?: ExtractOptions): Result<DocModule, ExtractError>

Reads one file's source text into its documentation module: the header comment, the symbols it exports, and the exports it forwards to other modules. It touches nothing outside the string it is given — no file system, no imports followed, no type checker — so the same text always yields the same document.

  • options.path: Path the text came from, which selects the dialect from its extension (.tsx and .jsx enable JSX) and is recorded on every node's source (default "module.ts")

  • options.id: Prefix for every node id, as <id>#<name> (default: path with its extension removed)

  • options.includeInternal: Keep symbols tagged @internal, dropped by default so a published site shows only what its readers can use (default false)

Returns a DocModule, or an ExtractError listing every syntax error with its position.

let result = extract(source, { path: "src/add.ts", includeInternal: true });

parseComment(raw: string): DocComment

Splits one JSDoc block into the prose before its first block tag and the tags after it. Accepts the comment with or without its /**/*/ markers, collapses aliases (@arg and @argument become param, @return becomes returns), and keeps fenced code inside an @example intact even when that code contains tags of its own.

parseComment("/** @param {string} name - The subject. */").tags;
// [{ tag: "param", name: "name", type: "string", text: "The subject." }]

findTag(comment: DocComment | null, tag: string): DocTag | null

First tag with the given canonical name, or null. Takes null for a symbol with no comment, so a renderer asks the same way everywhere.

findTags(comment: DocComment | null, tag: string): DocTag[]

Every tag with the given canonical name, in source order.

Every {@link}, {@linkcode} and {@linkplain} in a description or tag text, each with the raw text it occupies and the offset it starts at, so a renderer substitutes anchors without rescanning the markdown.

inlineLinks("See {@link parseComment|the parser}.");
// [{ raw: "{@link parseComment|the parser}", target: "parseComment", text: "the parser", index: 4 }]

SCHEMA_VERSION: number

The number to write into DocProject.schema. It rises whenever a field changes meaning or disappears, so a site recognizes a document written by an older version of this package.

ExtractError

The failure extract reports for text that will not parse, delivered inside a Failure. Its message leads with the first syntax error, since a later one is usually a consequence of it.

  • path: string - Path the caller passed for the source text

  • diagnostics: DocDiagnostic[] - Every syntax error found, in source order, each { message, line, column } with 1-based positions

Types

ExtractOptions

Options accepted by extract(), documented above alongside the function.

DocProject

The document a site reads. The caller assembles it from the modules it extracted, in whatever order it wants them presented.

interface DocProject {
	schema: number;
	name: string;
	version: string | null;
	modules: DocModule[];
}

DocModule

One extracted file.

interface DocModule {
	id: string;
	path: string;
	comment: DocComment | null;
	children: DocNode[];
	reExports: DocReExport[];
}

DocNode

One documented symbol. Members of classes, interfaces, enums and namespaces are the same shape under children, so one recursive renderer covers the whole tree.

interface DocNode {
	id: string;
	name: string;
	kind: DocKind;
	comment: DocComment | null;
	source: DocSource;
	type: string | null;
	signatures: DocSignature[];
	typeParameters: DocTypeParameter[];
	extends: string[];
	implements: string[];
	children: DocNode[];
	flags: DocFlags;
}

DocKind is one of function, class, interface, type-alias, variable, enum, enum-member, namespace, property, method, accessor or constructor.

DocComment and DocTag

interface DocComment {
	description: string;
	tags: DocTag[];
}

interface DocTag {
	tag: string;
	name: string | null;
	type: string | null;
	text: string;
}

DocSignature, DocParameter and DocTypeParameter

A symbol with one signature documents itself, so DocSignature.comment is filled in only for an overloaded symbol, where each signature keeps the comment written above it.

interface DocSignature {
	comment: DocComment | null;
	typeParameters: DocTypeParameter[];
	parameters: DocParameter[];
	returns: string | null;
}

interface DocParameter {
	name: string;
	type: string | null;
	description: string | null;
	optional: boolean;
	rest: boolean;
	default: string | null;
}

DocReExport

An export forwarded from another module, for the caller to resolve against the modules it holds. kind is named for export { a } from, all for export *, or namespace for export * as ns.

interface DocReExport {
	kind: "named" | "all" | "namespace";
	module: string;
	name: string | null;
	exported: string | null;
	typeOnly: boolean;
}

DocFlags carries default, optional, readonly, static, abstract, async, deprecated, internal and visibility, always present so a template reads flags.deprecated without guarding. DocSource is { path, line, column } with 1-based positions. DocLink is { raw, target, text, index }. DocDiagnostic is { message, line, column }.

Pattern: Resolving A Barrel

An index.ts that only re-exports produces no symbols of its own. Extract every module a package ships, then follow reExports to decide what each entry point publishes and under which name.

import type { DocModule, DocNode } from "@sdxc/jsdoc";

function published(byPath: Map<string, DocModule>, module: DocModule): DocNode[] {
	return module.reExports.flatMap((forwarded) => {
		let target = byPath.get(resolve(module.path, forwarded.module));
		if (!target) return [];
		if (forwarded.kind === "all") return published(byPath, target);
		return target.children
			.filter((node) => node.name === forwarded.name)
			.map((node) => ({ ...node, name: forwarded.exported ?? node.name }));
	});
}

Pattern: Linking Symbols To Each Other

{@link} targets are written as names, not ids. Index the tree by name once, then turn each link into an href the site can route to.

import type { DocModule } from "@sdxc/jsdoc";

import { inlineLinks } from "@sdxc/jsdoc";

function linkify(modules: DocModule[], text: string): string {
	let ids = new Map(
		modules.flatMap((module) => module.children.map((node) => [node.name, node.id])),
	);

	let out = text;
	for (let link of inlineLinks(text).reverse()) {
		let id = ids.get(link.target);
		let label = link.text ?? link.target;
		let replacement = id ? `[${label}](#${encodeURIComponent(id)})` : label;
		out = out.slice(0, link.index) + replacement + out.slice(link.index + link.raw.length);
	}
	return out;
}

Walking the links in reverse keeps every earlier offset valid while the text is rewritten.

Pattern: Grouping A Page By Kind

DocNode.kind is the only thing a page needs to lay symbols out, and the tree is uniform, so the same grouping works for a module and for the members of a class.

import type { DocKind, DocNode } from "@sdxc/jsdoc";

function byKind(nodes: DocNode[]): Map<DocKind, DocNode[]> {
	let groups = new Map<DocKind, DocNode[]>();
	for (let node of nodes) groups.set(node.kind, [...(groups.get(node.kind) ?? []), node]);
	return groups;
}