@openpkg-ts/sdk
Version:
TypeScript package specification SDK
1,470 lines (1,452 loc) • 72.2 kB
JavaScript
// src/analysis/run-analysis.ts
import * as fs2 from "node:fs";
import * as path4 from "node:path";
// src/ts-module.ts
import * as tsNamespace from "typescript";
var resolvedTypeScriptModule = (() => {
const candidate = tsNamespace;
if (candidate.ScriptTarget === undefined && typeof candidate.default !== "undefined") {
return candidate.default;
}
return candidate;
})();
var ts = resolvedTypeScriptModule;
// src/analysis/context.ts
import * as path2 from "node:path";
// src/options.ts
var DEFAULT_OPTIONS = {
includePrivate: false,
followImports: true
};
function normalizeOpenPkgOptions(options = {}) {
return {
...DEFAULT_OPTIONS,
...options
};
}
// src/analysis/program.ts
import * as path from "node:path";
var DEFAULT_COMPILER_OPTIONS = {
target: ts.ScriptTarget.Latest,
module: ts.ModuleKind.CommonJS,
lib: ["lib.es2021.d.ts"],
allowJs: true,
declaration: true,
moduleResolution: ts.ModuleResolutionKind.NodeJs
};
function createProgram({
entryFile,
baseDir = path.dirname(entryFile),
content
}) {
const configPath = ts.findConfigFile(baseDir, ts.sys.fileExists, "tsconfig.json");
let compilerOptions = { ...DEFAULT_COMPILER_OPTIONS };
if (configPath) {
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath));
compilerOptions = { ...compilerOptions, ...parsedConfig.options };
}
const compilerHost = ts.createCompilerHost(compilerOptions, true);
let inMemorySource;
if (content !== undefined) {
inMemorySource = ts.createSourceFile(entryFile, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
const originalGetSourceFile = compilerHost.getSourceFile.bind(compilerHost);
compilerHost.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
if (fileName === entryFile) {
return inMemorySource;
}
return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
};
}
const program = ts.createProgram([entryFile], compilerOptions, compilerHost);
const sourceFile = inMemorySource ?? program.getSourceFile(entryFile);
return {
program,
compilerHost,
compilerOptions,
sourceFile,
configPath
};
}
// src/analysis/context.ts
function createAnalysisContext({
entryFile,
packageDir,
content,
options
}) {
const baseDir = packageDir ?? path2.dirname(entryFile);
const normalizedOptions = normalizeOpenPkgOptions(options);
const programResult = createProgram({ entryFile, baseDir, content });
if (!programResult.sourceFile) {
throw new Error(`Could not load ${entryFile}`);
}
return {
entryFile,
baseDir,
program: programResult.program,
checker: programResult.program.getTypeChecker(),
sourceFile: programResult.sourceFile,
compilerOptions: programResult.compilerOptions,
compilerHost: programResult.compilerHost,
options: normalizedOptions,
configPath: programResult.configPath
};
}
// src/analysis/spec-builder.ts
import * as fs from "node:fs";
import * as path3 from "node:path";
import { SCHEMA_URL } from "@openpkg-ts/spec";
// src/utils/type-utils.ts
function collectReferencedTypes(type, typeChecker, referencedTypes, visitedTypes = new Set) {
if (visitedTypes.has(type))
return;
visitedTypes.add(type);
const symbol = type.getSymbol();
if (symbol) {
const symbolName = symbol.getName();
if (!symbolName.startsWith("__") && !isBuiltInType(symbolName)) {
referencedTypes.add(symbolName);
}
}
if (type.isIntersection()) {
for (const intersectionType of type.types) {
collectReferencedTypes(intersectionType, typeChecker, referencedTypes, visitedTypes);
}
}
if (type.isUnion()) {
for (const unionType of type.types) {
collectReferencedTypes(unionType, typeChecker, referencedTypes, visitedTypes);
}
}
if (type.flags & ts.TypeFlags.Object) {
const objectType = type;
if (objectType.objectFlags & ts.ObjectFlags.Reference) {
const typeRef = objectType;
if (typeRef.typeArguments) {
for (const typeArg of typeRef.typeArguments) {
collectReferencedTypes(typeArg, typeChecker, referencedTypes, visitedTypes);
}
}
}
}
}
function collectReferencedTypesFromNode(node, typeChecker, referencedTypes) {
if (ts.isTypeReferenceNode(node)) {
const typeNameText = node.typeName.getText();
const symbol = typeChecker.getSymbolAtLocation(node.typeName);
const name = symbol?.getName() ?? typeNameText;
if (!isBuiltInType(name)) {
referencedTypes.add(name);
}
node.typeArguments?.forEach((arg) => collectReferencedTypesFromNode(arg, typeChecker, referencedTypes));
return;
}
if (ts.isExpressionWithTypeArguments(node)) {
const expressionText = node.expression.getText();
const symbol = typeChecker.getSymbolAtLocation(node.expression);
const name = symbol?.getName() ?? expressionText;
if (!isBuiltInType(name)) {
referencedTypes.add(name);
}
node.typeArguments?.forEach((arg) => collectReferencedTypesFromNode(arg, typeChecker, referencedTypes));
return;
}
if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
node.types.forEach((typeNode) => collectReferencedTypesFromNode(typeNode, typeChecker, referencedTypes));
return;
}
if (ts.isArrayTypeNode(node)) {
collectReferencedTypesFromNode(node.elementType, typeChecker, referencedTypes);
return;
}
if (ts.isParenthesizedTypeNode(node)) {
collectReferencedTypesFromNode(node.type, typeChecker, referencedTypes);
return;
}
if (ts.isTypeLiteralNode(node)) {
node.members.forEach((member) => {
if (ts.isPropertySignature(member) && member.type) {
collectReferencedTypesFromNode(member.type, typeChecker, referencedTypes);
}
if (ts.isMethodSignature(member)) {
member.typeParameters?.forEach((param) => {
param.constraint && collectReferencedTypesFromNode(param.constraint, typeChecker, referencedTypes);
});
member.parameters.forEach((param) => {
if (param.type) {
collectReferencedTypesFromNode(param.type, typeChecker, referencedTypes);
}
});
if (member.type) {
collectReferencedTypesFromNode(member.type, typeChecker, referencedTypes);
}
}
if (ts.isCallSignatureDeclaration(member) && member.type) {
collectReferencedTypesFromNode(member.type, typeChecker, referencedTypes);
}
if (ts.isIndexSignatureDeclaration(member) && member.type) {
collectReferencedTypesFromNode(member.type, typeChecker, referencedTypes);
}
});
return;
}
if (ts.isTypeOperatorNode(node)) {
collectReferencedTypesFromNode(node.type, typeChecker, referencedTypes);
return;
}
if (ts.isIndexedAccessTypeNode(node)) {
collectReferencedTypesFromNode(node.objectType, typeChecker, referencedTypes);
collectReferencedTypesFromNode(node.indexType, typeChecker, referencedTypes);
return;
}
if (ts.isLiteralTypeNode(node)) {
return;
}
node.forEachChild((child) => {
if (ts.isTypeNode(child)) {
collectReferencedTypesFromNode(child, typeChecker, referencedTypes);
}
});
}
function isBuiltInType(name) {
const builtIns = [
"string",
"number",
"boolean",
"bigint",
"symbol",
"undefined",
"null",
"any",
"unknown",
"never",
"void",
"object",
"Array",
"Promise",
"Map",
"Set",
"WeakMap",
"WeakSet",
"Date",
"RegExp",
"Error",
"Function",
"Object",
"String",
"Number",
"Boolean",
"BigInt",
"Symbol",
"Uint8Array",
"Int8Array",
"Uint16Array",
"Int16Array",
"Uint32Array",
"Int32Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
"Uint8ClampedArray",
"ArrayBuffer",
"ArrayBufferLike",
"DataView",
"Uint8ArrayConstructor",
"ArrayBufferConstructor",
"JSON",
"Math",
"Reflect",
"Proxy",
"Intl",
"globalThis",
"__type"
];
return builtIns.includes(name);
}
// src/utils/parameter-utils.ts
var BUILTIN_TYPE_SCHEMAS = {
Date: { type: "string", format: "date-time" },
RegExp: { type: "object", description: "RegExp" },
Error: { type: "object" },
Promise: { type: "object" },
Map: { type: "object" },
Set: { type: "object" },
WeakMap: { type: "object" },
WeakSet: { type: "object" },
Function: { type: "object" },
ArrayBuffer: { type: "string", format: "binary" },
ArrayBufferLike: { type: "string", format: "binary" },
DataView: { type: "string", format: "binary" },
Uint8Array: { type: "string", format: "byte" },
Uint16Array: { type: "string", format: "byte" },
Uint32Array: { type: "string", format: "byte" },
Int8Array: { type: "string", format: "byte" },
Int16Array: { type: "string", format: "byte" },
Int32Array: { type: "string", format: "byte" },
Float32Array: { type: "string", format: "byte" },
Float64Array: { type: "string", format: "byte" },
BigInt64Array: { type: "string", format: "byte" },
BigUint64Array: { type: "string", format: "byte" }
};
function isObjectLiteralType(type) {
if (!(type.getFlags() & ts.TypeFlags.Object)) {
return false;
}
const objectFlags = type.objectFlags;
return (objectFlags & ts.ObjectFlags.ObjectLiteral) !== 0;
}
function isPureRefSchema(value) {
return Object.keys(value).length === 1 && "$ref" in value;
}
function withDescription(schema, description) {
if (isPureRefSchema(schema)) {
return {
allOf: [schema],
description
};
}
return {
...schema,
description
};
}
function propertiesToSchema(properties, description) {
const schema = {
type: "object",
properties: {}
};
const required = [];
for (const prop of properties) {
const propType = prop.type;
let propSchema;
if (typeof propType === "string") {
if (["string", "number", "boolean", "bigint", "null"].includes(propType)) {
propSchema = { type: propType === "bigint" ? "string" : propType };
} else {
propSchema = { type: propType };
}
} else if (propType && typeof propType === "object") {
propSchema = propType;
} else {
propSchema = { type: "any" };
}
if (prop.description && typeof propSchema === "object") {
propSchema = withDescription(propSchema, prop.description);
}
schema.properties[prop.name] = propSchema;
if (!prop.optional) {
required.push(prop.name);
}
}
if (required.length > 0) {
schema.required = required;
}
if (description) {
return withDescription(schema, description);
}
return schema;
}
function buildSchemaFromTypeNode(node, typeChecker, typeRefs, referencedTypes, functionDoc, parentParamName) {
if (ts.isParenthesizedTypeNode(node)) {
return buildSchemaFromTypeNode(node.type, typeChecker, typeRefs, referencedTypes, functionDoc ?? null, parentParamName);
}
if (ts.isIntersectionTypeNode(node)) {
const schemas = node.types.map((type) => buildSchemaFromTypeNode(type, typeChecker, typeRefs, referencedTypes, functionDoc, parentParamName));
return { allOf: schemas };
}
if (ts.isUnionTypeNode(node)) {
const schemas = node.types.map((type) => buildSchemaFromTypeNode(type, typeChecker, typeRefs, referencedTypes, functionDoc, parentParamName));
return { anyOf: schemas };
}
if (ts.isArrayTypeNode(node)) {
return {
type: "array",
items: buildSchemaFromTypeNode(node.elementType, typeChecker, typeRefs, referencedTypes, functionDoc, parentParamName)
};
}
if (ts.isTypeLiteralNode(node)) {
const properties = {};
const required = [];
for (const member of node.members) {
if (!ts.isPropertySignature(member) || !member.name) {
continue;
}
const propName = member.name.getText();
let schema2 = "any";
if (member.type) {
const memberType = typeChecker.getTypeFromTypeNode(member.type);
const formatted = formatTypeReference(memberType, typeChecker, typeRefs, referencedTypes);
if (typeof formatted === "string") {
if (formatted === "any") {
schema2 = buildSchemaFromTypeNode(member.type, typeChecker, typeRefs, referencedTypes, functionDoc, parentParamName);
} else {
schema2 = { type: formatted };
}
} else {
schema2 = formatted;
}
} else {
schema2 = { type: "any" };
}
const description = getDocDescriptionForProperty(functionDoc, parentParamName, propName);
if (typeof schema2 === "object" && description) {
schema2 = withDescription(schema2, description);
}
properties[propName] = schema2;
if (!member.questionToken) {
required.push(propName);
}
}
const schema = {
type: "object",
properties
};
if (required.length > 0) {
schema.required = required;
}
return schema;
}
if (ts.isTypeReferenceNode(node)) {
const typeName = node.typeName.getText();
if (typeName === "Array") {
return { type: "array" };
}
const builtInSchema = BUILTIN_TYPE_SCHEMAS[typeName];
if (builtInSchema) {
return { ...builtInSchema };
}
if (isBuiltInType(typeName)) {
return { type: "object" };
}
if (!typeRefs.has(typeName)) {
typeRefs.set(typeName, typeName);
}
referencedTypes?.add(typeName);
return { $ref: `#/types/${typeName}` };
}
if (ts.isLiteralTypeNode(node)) {
if (ts.isStringLiteral(node.literal)) {
return { enum: [node.literal.text] };
}
if (ts.isNumericLiteral(node.literal)) {
return { enum: [Number(node.literal.text)] };
}
}
if (ts.isIntersectionTypeNode(node)) {
const schemas = node.types.map((typeNode) => buildSchemaFromTypeNode(typeNode, typeChecker, typeRefs, referencedTypes, functionDoc, parentParamName));
if (schemas.some((schema) => ("$ref" in schema) && Object.keys(schema).length === 1)) {
const refs = schemas.filter((schema) => ("$ref" in schema) && Object.keys(schema).length === 1);
const nonRefs = schemas.filter((schema) => !(("$ref" in schema) && Object.keys(schema).length === 1));
if (refs.length === schemas.length) {
return refs[0];
}
if (nonRefs.length > 0) {
const merged = nonRefs.reduce((acc, schema) => ({ ...acc, ...schema }), {});
return merged;
}
}
return {
allOf: schemas
};
}
return { type: node.getText() };
}
function getDocDescriptionForProperty(functionDoc, parentParamName, propName) {
if (!functionDoc) {
return;
}
let match = functionDoc.params.find((p) => p.name === `${parentParamName}.${propName}`);
if (!match) {
match = functionDoc.params.find((p) => p.name.endsWith(`.${propName}`));
}
return match?.description;
}
function schemaIsAny(schema) {
if (typeof schema === "string") {
return schema === "any";
}
if ("type" in schema && schema.type === "any" && Object.keys(schema).length === 1) {
return true;
}
return false;
}
function schemasAreEqual(left, right) {
if (typeof left !== typeof right) {
return false;
}
if (typeof left === "string" && typeof right === "string") {
return left === right;
}
if (left == null || right == null) {
return left === right;
}
const normalize = (value) => {
if (Array.isArray(value)) {
return value.map((item) => normalize(item));
}
if (value && typeof value === "object") {
const sortedEntries = Object.entries(value).map(([key, val]) => [key, normalize(val)]).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
return Object.fromEntries(sortedEntries);
}
return value;
};
return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
}
function formatTypeReference(type, typeChecker, typeRefs, referencedTypes, visitedAliases) {
const visited = visitedAliases ?? new Set;
const aliasSymbol = type.aliasSymbol;
let aliasName;
let aliasAdded = false;
if (aliasSymbol) {
aliasName = aliasSymbol.getName();
if (visited.has(aliasName)) {
return { $ref: `#/types/${aliasName}` };
}
if (typeRefs.has(aliasName)) {
return { $ref: `#/types/${aliasName}` };
}
if (referencedTypes && !isBuiltInType(aliasName)) {
referencedTypes.add(aliasName);
return { $ref: `#/types/${aliasName}` };
}
visited.add(aliasName);
aliasAdded = true;
}
try {
const typeString = typeChecker.typeToString(type);
const primitives = [
"string",
"number",
"boolean",
"bigint",
"symbol",
"any",
"unknown",
"void",
"undefined",
"null",
"never"
];
if (primitives.includes(typeString)) {
if (typeString === "bigint") {
return { type: "string", format: "bigint" };
}
if (typeString === "undefined" || typeString === "null") {
return { type: "null" };
}
if (typeString === "void" || typeString === "never") {
return { type: "null" };
}
return { type: typeString };
}
if (type.isUnion()) {
const unionType = type;
const parts = unionType.types.map((t) => formatTypeReference(t, typeChecker, typeRefs, referencedTypes, visited));
return {
anyOf: parts
};
}
if (type.isIntersection()) {
const intersectionType = type;
const parts = intersectionType.types.map((t) => formatTypeReference(t, typeChecker, typeRefs, referencedTypes, visited));
const normalized = parts.flatMap((part) => {
if (typeof part === "string") {
return [{ type: part }];
}
if (part && typeof part === "object" && "allOf" in part) {
return Array.isArray(part.allOf) ? part.allOf : [part];
}
return [part];
});
if (normalized.length === 1) {
return normalized[0];
}
return {
allOf: normalized
};
}
const symbol = type.getSymbol();
if (symbol) {
const symbolName = symbol.getName();
if (symbolName.startsWith("__")) {
if (type.getFlags() & ts.TypeFlags.Object) {
const properties = type.getProperties();
if (properties.length > 0) {
const objSchema = {
type: "object",
properties: {}
};
const required = [];
for (const prop of properties) {
const propType = typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration);
const propName = prop.getName();
objSchema.properties[propName] = formatTypeReference(propType, typeChecker, typeRefs, referencedTypes, visited);
if (!(prop.flags & ts.SymbolFlags.Optional)) {
required.push(propName);
}
}
if (required.length > 0) {
objSchema.required = required;
}
return objSchema;
}
}
return { type: "object" };
}
if (typeRefs.has(symbolName)) {
return { $ref: `#/types/${symbolName}` };
}
if (symbolName === "Array") {
return { type: "array" };
}
const builtInSchema = BUILTIN_TYPE_SCHEMAS[symbolName];
if (builtInSchema) {
return { ...builtInSchema };
}
if (referencedTypes && !isBuiltInType(symbolName)) {
referencedTypes.add(symbolName);
return { $ref: `#/types/${symbolName}` };
}
if (isBuiltInType(symbolName)) {
return { type: "object" };
}
return { $ref: `#/types/${symbolName}` };
}
if (type.isLiteral()) {
if (typeString.startsWith('"') && typeString.endsWith('"')) {
const literalValue = typeString.slice(1, -1);
return { enum: [literalValue] };
}
return { enum: [Number(typeString)] };
}
const typePattern = /^(\w+)(\s*\|\s*undefined)?$/;
const match = typeString.match(typePattern);
if (match) {
const [, typeName, hasUndefined] = match;
if (typeRefs.has(typeName) || !isBuiltInType(typeName)) {
if (hasUndefined) {
return {
anyOf: [{ $ref: `#/types/${typeName}` }, { type: "null" }]
};
}
return { $ref: `#/types/${typeName}` };
}
}
return { type: typeString };
} finally {
if (aliasAdded && aliasName) {
visited.delete(aliasName);
}
}
}
function structureParameter(param, paramDecl, paramType, typeChecker, typeRefs, functionDoc, paramDoc, referencedTypes) {
const paramName = param.getName();
const fallbackName = paramName === "__0" || ts.isObjectBindingPattern(paramDecl.name) || ts.isArrayBindingPattern(paramDecl.name) ? "object" : paramName;
if (paramType.isIntersection()) {
const properties = [];
const intersectionType = paramType;
for (const subType of intersectionType.types) {
const symbol2 = subType.getSymbol();
const _typeString = typeChecker.typeToString(subType);
const isAnonymousObject = isObjectLiteralType(subType) || symbol2?.getName().startsWith("__");
if (isAnonymousObject) {
for (const prop of subType.getProperties()) {
const propType = typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration);
let description = "";
if (functionDoc) {
let docParam = functionDoc.params.find((p) => p.name === `${paramName}.${prop.getName()}`);
if (!docParam && paramName === "__0") {
docParam = functionDoc.params.find((p) => p.name.endsWith(`.${prop.getName()}`));
}
if (docParam) {
description = docParam.description;
}
}
properties.push({
name: prop.getName(),
type: formatTypeReference(propType, typeChecker, typeRefs, referencedTypes),
description,
optional: !!(prop.flags & ts.SymbolFlags.Optional)
});
}
} else if (symbol2) {
const _symbolName = symbol2.getName();
if (!isBuiltInType(_symbolName)) {
for (const prop of subType.getProperties()) {
const propType = typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration);
properties.push({
name: prop.getName(),
type: formatTypeReference(propType, typeChecker, typeRefs, referencedTypes),
description: "",
optional: !!(prop.flags & ts.SymbolFlags.Optional)
});
}
}
}
}
const actualName = fallbackName;
return {
name: actualName,
required: !typeChecker.isOptionalParameter(paramDecl),
description: paramDoc?.description || "",
schema: propertiesToSchema(properties)
};
}
if (paramType.isUnion()) {
const unionType = paramType;
const objectOptions = [];
let hasNonObjectTypes = false;
for (const subType of unionType.types) {
const symbol2 = subType.getSymbol();
if (isObjectLiteralType(subType) || symbol2?.getName().startsWith("__")) {
const properties = [];
for (const prop of subType.getProperties()) {
const propType = typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration);
properties.push({
name: prop.getName(),
type: formatTypeReference(propType, typeChecker, typeRefs, referencedTypes),
description: "",
optional: !!(prop.flags & ts.SymbolFlags.Optional)
});
}
if (properties.length > 0) {
objectOptions.push({ properties });
}
} else {
hasNonObjectTypes = true;
}
}
if (objectOptions.length > 0 && !hasNonObjectTypes) {
const readableName2 = fallbackName;
return {
name: readableName2,
required: !typeChecker.isOptionalParameter(paramDecl),
description: paramDoc?.description || "",
schema: {
oneOf: objectOptions.map((opt) => propertiesToSchema(opt.properties))
}
};
}
}
const symbol = paramType.getSymbol();
if ((symbol?.getName().startsWith("__") || isObjectLiteralType(paramType)) && paramType.getProperties().length > 0) {
const properties = [];
for (const prop of paramType.getProperties()) {
const propType = typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration);
properties.push({
name: prop.getName(),
type: formatTypeReference(propType, typeChecker, typeRefs, referencedTypes),
description: "",
optional: !!(prop.flags & ts.SymbolFlags.Optional)
});
}
const readableName2 = fallbackName;
return {
name: readableName2,
required: !typeChecker.isOptionalParameter(paramDecl),
description: paramDoc?.description || "",
schema: propertiesToSchema(properties)
};
}
if (paramType.flags & ts.TypeFlags.Any && paramDecl.type && paramDecl.name && ts.isObjectBindingPattern(paramDecl.name)) {
const actualName = fallbackName;
const schema2 = buildSchemaFromTypeNode(paramDecl.type, typeChecker, typeRefs, referencedTypes, functionDoc ?? null, param.getName());
return {
name: actualName,
required: !typeChecker.isOptionalParameter(paramDecl),
description: paramDoc?.description || "",
schema: schema2
};
}
const typeRef = formatTypeReference(paramType, typeChecker, typeRefs, referencedTypes);
let schema;
if (typeof typeRef === "string") {
if ([
"string",
"number",
"boolean",
"null",
"undefined",
"any",
"unknown",
"never",
"void"
].includes(typeRef)) {
schema = { type: typeRef };
} else {
schema = { type: typeRef };
}
} else {
schema = typeRef;
}
if (paramDecl.type) {
const astSchema = buildSchemaFromTypeNode(paramDecl.type, typeChecker, typeRefs, referencedTypes, functionDoc ?? null, param.getName());
if (schemaIsAny(schema)) {
schema = astSchema;
} else if (!(("type" in schema) && schema.type === "any") && !(typeof schema === "object" && isPureRefSchema(schema)) && Object.keys(astSchema).length > 0 && !schemasAreEqual(schema, astSchema)) {
schema = {
allOf: [schema, astSchema]
};
}
}
const readableName = fallbackName;
return {
name: readableName,
required: !typeChecker.isOptionalParameter(paramDecl),
description: paramDoc?.description || "",
schema
};
}
// src/utils/tsdoc-utils.ts
function parseJSDocComment(symbol, _typeChecker, sourceFileOverride) {
const node = symbol.valueDeclaration || symbol.declarations?.[0];
if (!node)
return null;
const sourceFile = sourceFileOverride || node.getSourceFile();
const commentRanges = ts.getLeadingCommentRanges(sourceFile.text, node.pos);
if (!commentRanges || commentRanges.length === 0) {
return null;
}
const lastComment = commentRanges[commentRanges.length - 1];
const commentText = sourceFile.text.substring(lastComment.pos, lastComment.end);
return parseJSDocText(commentText);
}
function parseJSDocText(commentText) {
const tags = [];
const result = {
description: "",
params: [],
examples: []
};
const cleanedText = commentText.replace(/^\/\*\*\s*/, "").replace(/\s*\*\/$/, "").replace(/^\s*\* ?/gm, "");
const lines = cleanedText.split(/\n/);
let currentTag = "";
let currentContent = [];
const pushDescription = (line) => {
const processed = replaceInlineLinks(line, tags).trimEnd();
if (processed.trim()) {
result.description = result.description ? `${result.description}
${processed}` : processed;
}
};
for (const line of lines) {
const tagMatch = line.match(/^@(\w+)(?:\s+(.*))?$/);
if (tagMatch) {
if (currentTag) {
processTag(result, tags, currentTag, currentContent.join(String.fromCharCode(10)));
}
currentTag = tagMatch[1];
currentContent = tagMatch[2] ? [tagMatch[2]] : [];
} else if (currentTag) {
currentContent.push(line);
} else {
if (line.trim()) {
pushDescription(line);
}
}
}
if (currentTag) {
processTag(result, tags, currentTag, currentContent.join(String.fromCharCode(10)));
}
if (result.examples && result.examples.length === 0) {
delete result.examples;
}
if (tags.length > 0) {
result.tags = tags;
}
return result;
}
function processTag(result, tags, tag, content) {
switch (tag) {
case "param":
case "parameter": {
const paramMatch = content.match(/^(?:\{([^}]+)\}\s+)?(\S+)(?:\s+-\s+)?(.*)$/);
if (paramMatch) {
const [, type, name, description] = paramMatch;
const processedDescription = replaceInlineLinks(description || "", tags);
result.params.push({
name: name || "",
description: processedDescription || "",
type
});
}
break;
}
case "returns":
case "return": {
result.returns = replaceInlineLinks(content, tags);
break;
}
case "example": {
const example = replaceInlineLinks(content.trim(), tags).trim();
if (example) {
if (!result.examples) {
result.examples = [];
}
result.examples.push(example);
}
break;
}
case "see": {
const parts = content.split(",").map((part) => part.trim()).filter(Boolean);
for (const part of parts) {
const linkTargets = extractLinkTargets(part);
if (linkTargets.length > 0) {
for (const target of linkTargets) {
tags.push({ name: "link", text: target });
tags.push({ name: "see", text: target });
}
} else {
tags.push({ name: "see", text: part });
}
}
break;
}
case "link": {
const { target } = parseLinkBody(content.trim());
if (target) {
tags.push({ name: "link", text: target });
}
break;
}
default: {
replaceInlineLinks(content, tags);
}
}
}
function replaceInlineLinks(text, tags, tagName = "link") {
return text.replace(/\{@link\s+([^}]+)\}/g, (_match, body) => {
const { target, label } = parseLinkBody(body);
if (target) {
tags.push({ name: tagName, text: target });
}
return label || target || "";
});
}
function extractLinkTargets(text) {
const targets = [];
text.replace(/\{@link\s+([^}]+)\}/g, (_match, body) => {
const { target } = parseLinkBody(body);
if (target) {
targets.push(target);
}
return "";
});
return targets;
}
function parseLinkBody(raw) {
const trimmed = raw.trim();
if (!trimmed) {
return { target: "" };
}
const pipeIndex = trimmed.indexOf("|");
if (pipeIndex >= 0) {
const target2 = trimmed.slice(0, pipeIndex).trim();
const label2 = trimmed.slice(pipeIndex + 1).trim();
return { target: target2, label: label2 };
}
const parts = trimmed.split(/\s+/);
const target = parts.shift() ?? "";
const label = parts.join(" ").trim();
return { target, label: label || undefined };
}
function extractDestructuredParams(parsedDoc, paramName) {
const destructuredParams = new Map;
const paramPrefix = `${paramName}.`;
for (const param of parsedDoc.params) {
if (param.name.startsWith(paramPrefix)) {
const propertyName = param.name.substring(paramPrefix.length);
destructuredParams.set(propertyName, param.description);
} else if (param.name.includes(".") && paramName === "__0") {
const [_prefix, propertyName] = param.name.split(".", 2);
if (propertyName) {
destructuredParams.set(propertyName, param.description);
}
}
}
return destructuredParams;
}
function getParameterDocumentation(param, paramDecl, typeChecker) {
const result = {
description: ""
};
const funcNode = paramDecl.parent;
if (ts.isFunctionDeclaration(funcNode) || ts.isFunctionExpression(funcNode)) {
const funcSymbol = typeChecker.getSymbolAtLocation(funcNode.name || funcNode);
if (funcSymbol) {
const parsedDoc = parseJSDocComment(funcSymbol, typeChecker);
if (parsedDoc) {
const paramName = param.getName();
const paramDoc = parsedDoc.params.find((p) => p.name === paramName || p.name.split(".")[0] === paramName);
if (paramDoc) {
result.description = paramDoc.description;
}
const destructuredProps = extractDestructuredParams(parsedDoc, paramName);
if (destructuredProps.size > 0) {
result.destructuredProperties = Array.from(destructuredProps.entries()).map(([name, description]) => ({
name,
description
}));
}
}
}
}
return result;
}
// src/analysis/ast-utils.ts
function getJSDocComment(symbol, typeChecker) {
const comments = symbol.getDocumentationComment(typeChecker);
return ts.displayPartsToString(comments);
}
function getSourceLocation(node) {
const sourceFile = node.getSourceFile();
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
return {
file: sourceFile.fileName,
line: line + 1
};
}
// src/analysis/serializers/classes.ts
function serializeClass(declaration, symbol, context) {
const { checker, typeRegistry } = context;
const typeRefs = typeRegistry.getTypeRefs();
const referencedTypes = typeRegistry.getReferencedTypes();
const members = serializeClassMembers(declaration, checker, typeRefs, referencedTypes);
const parsedDoc = parseJSDocComment(symbol, context.checker);
const description = parsedDoc?.description ?? getJSDocComment(symbol, context.checker);
const exportEntry = {
id: symbol.getName(),
name: symbol.getName(),
kind: "class",
description,
source: getSourceLocation(declaration),
members: members.length > 0 ? members : undefined,
tags: parsedDoc?.tags
};
const typeDefinition = {
id: symbol.getName(),
name: symbol.getName(),
kind: "class",
description,
source: getSourceLocation(declaration),
members: members.length > 0 ? members : undefined,
tags: parsedDoc?.tags
};
return {
exportEntry,
typeDefinition
};
}
function serializeClassMembers(declaration, checker, typeRefs, referencedTypes) {
const members = [];
for (const member of declaration.members) {
if (!member.name && !ts.isConstructorDeclaration(member)) {
continue;
}
if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {
const memberName = member.name?.getText();
if (!memberName)
continue;
const memberSymbol = member.name ? checker.getSymbolAtLocation(member.name) : undefined;
const memberType = memberSymbol ? checker.getTypeOfSymbolAtLocation(memberSymbol, member) : member.type ? checker.getTypeFromTypeNode(member.type) : checker.getTypeAtLocation(member);
collectReferencedTypes(memberType, checker, referencedTypes);
const schema = formatTypeReference(memberType, checker, typeRefs, referencedTypes);
const flags = {};
const isOptionalSymbol = memberSymbol != null && (memberSymbol.flags & ts.SymbolFlags.Optional) !== 0;
if (member.questionToken || isOptionalSymbol) {
flags.optional = true;
}
if (member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ReadonlyKeyword)) {
flags.readonly = true;
}
if (member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)) {
flags.static = true;
}
members.push({
id: memberName,
name: memberName,
kind: "property",
visibility: getMemberVisibility(member.modifiers),
schema,
description: memberSymbol ? getJSDocComment(memberSymbol, checker) : undefined,
flags: Object.keys(flags).length > 0 ? flags : undefined
});
continue;
}
if (ts.isMethodDeclaration(member)) {
const memberName = member.name?.getText() ?? "method";
const memberSymbol = member.name ? checker.getSymbolAtLocation(member.name) : undefined;
const methodDoc = memberSymbol ? parseJSDocComment(memberSymbol, checker) : null;
const signature = checker.getSignatureFromDeclaration(member);
const signatures = signature ? [
serializeSignature(signature, checker, typeRefs, referencedTypes, methodDoc, memberSymbol)
] : undefined;
members.push({
id: memberName,
name: memberName,
kind: "method",
visibility: getMemberVisibility(member.modifiers),
signatures,
description: memberSymbol ? getJSDocComment(memberSymbol, checker) : undefined,
flags: getMethodFlags(member)
});
continue;
}
if (ts.isConstructorDeclaration(member)) {
const ctorSymbol = checker.getSymbolAtLocation(member);
const ctorDoc = ctorSymbol ? parseJSDocComment(ctorSymbol, checker) : null;
const signature = checker.getSignatureFromDeclaration(member);
const signatures = signature ? [serializeSignature(signature, checker, typeRefs, referencedTypes, ctorDoc, ctorSymbol)] : undefined;
members.push({
id: "constructor",
name: "constructor",
kind: "constructor",
visibility: getMemberVisibility(member.modifiers),
signatures,
description: ctorSymbol ? getJSDocComment(ctorSymbol, checker) : undefined
});
continue;
}
if (ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) {
const memberName = member.name?.getText();
if (!memberName)
continue;
const memberSymbol = checker.getSymbolAtLocation(member.name);
const accessorType = ts.isGetAccessorDeclaration(member) ? checker.getTypeAtLocation(member) : member.parameters.length > 0 ? checker.getTypeAtLocation(member.parameters[0]) : checker.getTypeAtLocation(member);
collectReferencedTypes(accessorType, checker, referencedTypes);
const schema = formatTypeReference(accessorType, checker, typeRefs, referencedTypes);
members.push({
id: memberName,
name: memberName,
kind: "accessor",
visibility: getMemberVisibility(member.modifiers),
schema,
description: memberSymbol ? getJSDocComment(memberSymbol, checker) : undefined
});
}
}
return members;
}
function serializeSignature(signature, checker, typeRefs, referencedTypes, doc, symbol) {
return {
parameters: signature.getParameters().map((param) => {
const paramDecl = param.valueDeclaration;
const paramType = paramDecl?.type != null ? checker.getTypeFromTypeNode(paramDecl.type) : checker.getTypeAtLocation(paramDecl);
collectReferencedTypes(paramType, checker, referencedTypes);
const paramDoc = paramDecl ? getParameterDocumentation(param, paramDecl, checker) : undefined;
return structureParameter(param, paramDecl, paramType, checker, typeRefs, doc, paramDoc, referencedTypes);
}),
returns: {
schema: formatTypeReference(signature.getReturnType(), checker, typeRefs, referencedTypes),
description: doc?.returns || ""
},
description: doc?.description || (symbol ? getJSDocComment(symbol, checker) : undefined)
};
}
function getMemberVisibility(modifiers) {
if (!modifiers)
return;
if (modifiers.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword)) {
return "private";
}
if (modifiers.some((mod) => mod.kind === ts.SyntaxKind.ProtectedKeyword)) {
return "protected";
}
if (modifiers.some((mod) => mod.kind === ts.SyntaxKind.PublicKeyword)) {
return "public";
}
return;
}
function getMethodFlags(member) {
const flags = {};
if (member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)) {
flags.static = true;
}
if (member.asteriskToken) {
flags.generator = true;
}
if (member.questionToken) {
flags.optional = true;
}
return Object.keys(flags).length > 0 ? flags : undefined;
}
// src/analysis/serializers/enums.ts
function serializeEnum(declaration, symbol, context) {
const parsedDoc = parseJSDocComment(symbol, context.checker);
const description = parsedDoc?.description ?? getJSDocComment(symbol, context.checker);
const exportEntry = {
id: symbol.getName(),
name: symbol.getName(),
kind: "enum",
description,
source: getSourceLocation(declaration),
tags: parsedDoc?.tags
};
const typeDefinition = {
id: symbol.getName(),
name: symbol.getName(),
kind: "enum",
members: getEnumMembers(declaration),
description,
source: getSourceLocation(declaration),
tags: parsedDoc?.tags
};
return {
exportEntry,
typeDefinition
};
}
function getEnumMembers(enumDecl) {
return enumDecl.members.map((member) => ({
id: member.name?.getText() || "",
name: member.name?.getText() || "",
value: member.initializer ? member.initializer.getText() : undefined,
description: ""
}));
}
// src/analysis/serializers/functions.ts
function serializeCallSignatures(signatures, symbol, context, parsedDoc) {
if (signatures.length === 0) {
return [];
}
const { checker, typeRegistry } = context;
const typeRefs = typeRegistry.getTypeRefs();
const referencedTypes = typeRegistry.getReferencedTypes();
const functionDoc = parsedDoc ?? (symbol ? parseJSDocComment(symbol, checker) : null);
return signatures.map((signature) => {
const parameters = signature.getParameters().map((param) => {
const paramDecl = param.declarations?.find(ts.isParameter);
const paramType = paramDecl ? paramDecl.type != null ? checker.getTypeFromTypeNode(paramDecl.type) : checker.getTypeAtLocation(paramDecl) : checker.getTypeOfSymbolAtLocation(param, symbol?.declarations?.[0] ?? signature.declaration ?? param.declarations?.[0] ?? param.valueDeclaration);
collectReferencedTypes(paramType, checker, referencedTypes);
if (paramDecl?.type) {
collectReferencedTypesFromNode(paramDecl.type, checker, referencedTypes);
}
if (paramDecl && ts.isParameter(paramDecl)) {
const paramDoc = getParameterDocumentation(param, paramDecl, checker);
return structureParameter(param, paramDecl, paramType, checker, typeRefs, functionDoc, paramDoc, referencedTypes);
}
return {
name: param.getName(),
required: !(param.flags & ts.SymbolFlags.Optional),
description: "",
schema: formatTypeReference(paramType, checker, typeRefs, referencedTypes)
};
});
const returnType = signature.getReturnType();
if (returnType) {
collectReferencedTypes(returnType, checker, referencedTypes);
}
return {
parameters,
returns: {
schema: returnType ? formatTypeReference(returnType, checker, typeRefs, referencedTypes) : { type: "void" },
description: functionDoc?.returns || ""
},
description: functionDoc?.description || undefined
};
});
}
function serializeFunctionExport(declaration, symbol, context) {
const { checker } = context;
const signature = checker.getSignatureFromDeclaration(declaration);
const funcSymbol = checker.getSymbolAtLocation(declaration.name || declaration);
const parsedDoc = parseJSDocComment(symbol, checker);
const description = parsedDoc?.description ?? getJSDocComment(symbol, checker);
return {
id: symbol.getName(),
name: symbol.getName(),
kind: "function",
signatures: signature ? serializeCallSignatures([signature], funcSymbol ?? symbol, context, parsedDoc) : [],
description,
source: getSourceLocation(declaration),
examples: parsedDoc?.examples,
tags: parsedDoc?.tags
};
}
// src/analysis/serializers/interfaces.ts
function serializeInterface(declaration, symbol, context) {
const parsedDoc = parseJSDocComment(symbol, context.checker);
const description = parsedDoc?.description ?? getJSDocComment(symbol, context.checker);
const exportEntry = {
id: symbol.getName(),
name: symbol.getName(),
kind: "interface",
description,
source: getSourceLocation(declaration),
tags: parsedDoc?.tags
};
const schema = interfaceToSchema(declaration, context.checker, context.typeRegistry.getTypeRefs(), context.typeRegistry.getReferencedTypes());
const typeDefinition = {
id: symbol.getName(),
name: symbol.getName(),
kind: "interface",
schema,
description,
source: getSourceLocation(declaration),
tags: parsedDoc?.tags
};
return {
exportEntry,
typeDefinition
};
}
function interfaceToSchema(iface, typeChecker, typeRefs, referencedTypes) {
const schema = {
type: "object",
properties: {}
};
const required = [];
for (const prop of iface.members.filter(ts.isPropertySignature)) {
const propName = prop.name?.getText() || "";
if (prop.type) {
const propType = typeChecker.getTypeAtLocation(prop.type);
collectReferencedTypes(propType, typeChecker, referencedTypes);
}
schema.properties[propName] = prop.type ? formatTypeReference(typeChecker.getTypeAtLocation(prop.type), typeChecker, typeRefs, referencedTypes) : { type: "any" };
if (!prop.questionToken) {
required.push(propName);
}
}
if (required.length > 0) {
schema.required = required;
}
return schema;
}
// src/analysis/serializers/type-aliases.ts
function serializeTypeAlias(declaration, symbol, context) {
const { checker, typeRegistry } = context;
const typeRefs = typeRegistry.getTypeRefs();
const referencedTypes = typeRegistry.getReferencedTypes();
const parsedDoc = parseJSDocComment(symbol, checker);
const description = parsedDoc?.description ?? getJSDocComment(symbol, checker);
const exportEntry = {
id: symbol.getName(),
name: symbol.getName(),
kind: "type",
type: typeToRef(declaration.type, checker, typeRefs, referencedTypes),
description,
source: getSourceLocation(declaration),
tags: parsedDoc?.tags
};
const aliasType = checker.getTypeAtLocation(declaration.type);
const aliasName = symbol.getName();
const existingRef = typeRefs.get(aliasName);
if (existingRef) {
typeRefs.delete(aliasName);
}
const aliasSchema = formatTypeReference(aliasType, checker, typeRefs, undefined);
if (existingRef) {
typeRefs.set(aliasName, existingRef);
}
const typeDefinition = {
id: symbol.getName(),
name: symbol.getName(),
kind: "type",
description,
source: getSourceLocation(declaration),
tags: parsedDoc?.tags
};
if (typeof aliasSchema === "string") {
typeDefinition.type = aliasSchema;
} else if (aliasSchema && Object.keys(aliasSchema).length > 0) {
typeDefinition.schema = aliasSchema;
} else {
typeDefinition.type = declaration.type.getText();
}
return {
exportEntry,
typeDefinition
};
}
function typeToRef(node, typeChecker, typeRefs, referencedTypes) {
const type = typeChecker.getTypeAtLocation(node);
collectReferencedTypes(type, typeChecker, referencedTypes);
return formatTypeReference(type, typeChecker, typeRefs, referencedTypes);
}
// src/analysis/serializers/variables.ts
function serializeVariable(declaration, symbol, context) {
const { checker, typeRegistry } = context;
const variableType = checker.getTypeAtLocation(declaration.name ?? declaration);
const callSignatures = variableType.getCallSignatures();
const parsedDoc = parseJSDocComment(symbol, checker);
const description = parsedDoc?.description ?? getJSDocComment(symbol, checker);
if (callSignatures.length > 0) {
return {
id: symbol.getName(),
name: symbol.getName(),
kind: "function",
signatures: serializeCallSignatures(callSignatures, symbol, context, parsedDoc),
description,
source: getSourceLocation(declaration.initializer ?? declaration),
examples: parsedDoc?.examples,
tags: parsedDoc?.tags
};
}
const typeRefs = typeRegistry.getTypeRefs();
const referencedTypes = typeRegistry.getReferencedTypes();
return {
id: symbol.getName(),
name: symbol.getName(),
kind: "variable",
type: typeToRef2(declaration, checker, typeRefs, referencedTypes),
description,
source: getSourceLocation(declaration),
tags: parsedDoc?.tags
};
}
function typeToRef2(node, typeChecker, typeRefs, referencedTypes) {
const type = typeChecker.getTypeAtLocation(node);
collectReferencedTypes(type, typeChecker, referencedTypes);
return formatTypeReference(type, typeChecker, typeRefs, referencedTypes);
}
// src/analysis/type-registry.ts
class TypeRegistry {
typeRefs = new Map;
typeDefinitions = new Map;
referencedTypes = new Set;
registerExportedType(name, id = name) {
if (!this.typeRefs.has(name)) {
this.typeRefs.set(name, id);
}
}
hasType(name) {
return this.typeDefinitions.has(name);
}
registerTypeDefinition(definition) {
if (this.typeDefinitions.has(definition.name)) {
return false;
}
this.typeDefinitions.set(definition.name, definition);
if (!this.typeRefs.has(definition.name)) {
this.typeRefs.set(definition.name, definition.id);
}
return true;
}
getTypeRefs() {
return this.typeRefs;
}
getTypeDefinitions() {
return Array.from(this.typeDefinitions.values());
}
getReferencedTypes() {
return this.referencedTypes;
}
isKnownType(name) {
if (this.typeDefinitions.has(name)) {
return true;
}
const ref = this.typeRefs.get(name);
if (ref === undefined) {
return false;
}
if (ref !== name) {
return this.typeDefinitions.has(ref);
}
return false;
}
}
// src/analysis/spec-builder.ts
function buildOpenPkgSpec(context, resolveExternalTypes) {
const { baseDir, checker: typeC