UNPKG

@openpkg-ts/sdk

Version:

TypeScript package specification SDK

1,470 lines (1,452 loc) 72.2 kB
// 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