docs-util: added AI generator (#6770)
## What Adds an AI generator to the docblock tool that uses OpenAI. The generator at the moment only generates examples for functions when the `--generate-examples` option is provided. ## Note I've included the generated examples of the `IOrderModuleService` as a reference of the type of result provided by the AI generator, with minor tweeks I've made. I haven't made any changes to descriptions in that file.
This commit is contained in:
@@ -1 +1,2 @@
|
||||
MONOREPO_ROOT_PATH=/Users/medusa/medusa
|
||||
MONOREPO_ROOT_PATH=/Users/medusa/medusa
|
||||
OPENAI_API_KEY=
|
||||
@@ -24,6 +24,7 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"openai": "^4.29.1",
|
||||
"openapi-types": "^12.1.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"prettier": "^3.2.4",
|
||||
|
||||
@@ -3,6 +3,7 @@ import ts from "typescript"
|
||||
import { GeneratorEvent } from "../helpers/generator-event-manager.js"
|
||||
import AbstractGenerator from "./index.js"
|
||||
import { minimatch } from "minimatch"
|
||||
import AiGenerator from "../helpers/ai-generator.js"
|
||||
|
||||
/**
|
||||
* A class used to generate docblock for one or multiple file paths.
|
||||
@@ -18,64 +19,110 @@ class DocblockGenerator extends AbstractGenerator {
|
||||
removeComments: false,
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
this.program!.getSourceFiles().map(async (file) => {
|
||||
// Ignore .d.ts files
|
||||
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
|
||||
return
|
||||
}
|
||||
const documentSourceFile = async (file: ts.SourceFile) => {
|
||||
// Ignore .d.ts files
|
||||
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
|
||||
return
|
||||
}
|
||||
let aiGenerator: AiGenerator | undefined
|
||||
|
||||
console.log(`[Docblock] Generating for ${file.fileName}...`)
|
||||
console.log(`[Docblock] Generating for ${file.fileName}...`)
|
||||
|
||||
let fileContent = file.getFullText()
|
||||
let fileComments: string = ""
|
||||
const commentsToRemove: string[] = []
|
||||
let fileContent = file.getFullText()
|
||||
let fileComments: string = ""
|
||||
const commentsToRemove: string[] = []
|
||||
const origFileText = file.getFullText().trim()
|
||||
const fileNodes: ts.Node[] = [file]
|
||||
|
||||
const documentChild = (node: ts.Node, topLevel = false) => {
|
||||
const isSourceFile = ts.isSourceFile(node)
|
||||
const origNodeText = node.getFullText().trim()
|
||||
const nodeKindGenerator = this.kindsRegistry?.getKindGenerator(node)
|
||||
let docComment: string | undefined
|
||||
if (this.options.generateExamples) {
|
||||
aiGenerator = new AiGenerator()
|
||||
}
|
||||
|
||||
if (nodeKindGenerator?.canDocumentNode(node)) {
|
||||
docComment = nodeKindGenerator.getDocBlock(node)
|
||||
if (docComment.length) {
|
||||
if (isSourceFile) {
|
||||
fileComments = docComment
|
||||
} else {
|
||||
ts.addSyntheticLeadingComment(
|
||||
node,
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
docComment,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
// since typescript's compiler API doesn't support
|
||||
// async processes, we have to retrieve the nodes first then
|
||||
// traverse them separately.
|
||||
const pushNodesToArr = (node: ts.Node) => {
|
||||
fileNodes.push(node)
|
||||
|
||||
ts.forEachChild(node, pushNodesToArr)
|
||||
}
|
||||
ts.forEachChild(file, pushNodesToArr)
|
||||
|
||||
const documentNode = async (node: ts.Node) => {
|
||||
const isSourceFile = ts.isSourceFile(node)
|
||||
const nodeKindGenerator = this.kindsRegistry?.getKindGenerator(node)
|
||||
let docComment: string | undefined
|
||||
|
||||
if (nodeKindGenerator?.canDocumentNode(node)) {
|
||||
if (aiGenerator) {
|
||||
const nodeFiles = aiGenerator.getNodeFiles(file)
|
||||
await aiGenerator.initAssistant(nodeFiles)
|
||||
}
|
||||
|
||||
ts.forEachChild(node, (childNode) =>
|
||||
documentChild(childNode, isSourceFile)
|
||||
)
|
||||
|
||||
if (!isSourceFile && topLevel) {
|
||||
const newNodeText = printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
node,
|
||||
file
|
||||
)
|
||||
|
||||
if (newNodeText !== origNodeText) {
|
||||
fileContent = fileContent.replace(origNodeText, newNodeText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
documentChild(file, true)
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
commentsToRemove.forEach((commentToRemove) => {
|
||||
fileContent = fileContent.replace(commentToRemove, "")
|
||||
// initialize assistant only when needed
|
||||
// if previously initialized, calling the method does nothing
|
||||
docComment = await nodeKindGenerator.getDocBlock(node, {
|
||||
aiGenerator,
|
||||
addEnd: true,
|
||||
})
|
||||
if (docComment.length) {
|
||||
const existingComments =
|
||||
nodeKindGenerator.getNodeCommentsFromRange(node)
|
||||
if (existingComments?.length) {
|
||||
commentsToRemove.push(existingComments)
|
||||
}
|
||||
if (isSourceFile) {
|
||||
fileComments = docComment
|
||||
} else {
|
||||
ts.addSyntheticLeadingComment(
|
||||
node,
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
docComment,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// due to rate limit being reached when running
|
||||
// the AI Generator, we only run the documentNode in
|
||||
// parallel when the `generateExamples` option is disabled.
|
||||
if (this.options.generateExamples) {
|
||||
for (const node of fileNodes) {
|
||||
await documentNode(node)
|
||||
}
|
||||
} else {
|
||||
await Promise.all(
|
||||
fileNodes.map(async (node) => await documentNode(node))
|
||||
)
|
||||
}
|
||||
|
||||
if (aiGenerator) {
|
||||
await aiGenerator.destroyAssistant()
|
||||
}
|
||||
|
||||
// add comments to file
|
||||
const newNodeText = printer.printNode(ts.EmitHint.Unspecified, file, file)
|
||||
|
||||
// if file's text changed, replace it.
|
||||
if (newNodeText !== origFileText) {
|
||||
fileContent = fileContent.replace(origFileText, newNodeText)
|
||||
}
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
if (commentsToRemove.length) {
|
||||
let formatted = this.formatter.addCommentsToSourceFile(
|
||||
fileComments,
|
||||
await this.formatter.formatStr(fileContent, file.fileName)
|
||||
)
|
||||
commentsToRemove.forEach((commentToRemove) => {
|
||||
formatted = formatted.replace(commentToRemove, "")
|
||||
})
|
||||
ts.sys.writeFile(
|
||||
file.fileName,
|
||||
await this.formatter.formatStr(formatted, file.fileName)
|
||||
)
|
||||
} else {
|
||||
ts.sys.writeFile(
|
||||
file.fileName,
|
||||
this.formatter.addCommentsToSourceFile(
|
||||
@@ -84,12 +131,24 @@ class DocblockGenerator extends AbstractGenerator {
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Docblock] Finished generating docblock for ${file.fileName}.`
|
||||
console.log(
|
||||
`[Docblock] Finished generating docblock for ${file.fileName}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (this.options.generateExamples) {
|
||||
for (const file of this.program!.getSourceFiles()) {
|
||||
await documentSourceFile(file)
|
||||
}
|
||||
} else {
|
||||
await Promise.all(
|
||||
this.program!.getSourceFiles().map(
|
||||
async (file) => await documentSourceFile(file)
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
|
||||
this.reset()
|
||||
|
||||
@@ -11,7 +11,7 @@ import { GeneratorEvent } from "../helpers/generator-event-manager.js"
|
||||
class OasGenerator extends AbstractGenerator {
|
||||
protected oasKindGenerator?: OasKindGenerator
|
||||
|
||||
run() {
|
||||
async run() {
|
||||
this.init()
|
||||
|
||||
const { generateExamples } = this.options
|
||||
@@ -24,36 +24,52 @@ class OasGenerator extends AbstractGenerator {
|
||||
},
|
||||
})
|
||||
|
||||
this.program!.getSourceFiles().map((file) => {
|
||||
// Ignore .d.ts files
|
||||
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
|
||||
return
|
||||
}
|
||||
await Promise.all(
|
||||
this.program!.getSourceFiles().map(async (file) => {
|
||||
// Ignore .d.ts files
|
||||
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
|
||||
return
|
||||
}
|
||||
const fileNodes: ts.Node[] = [file]
|
||||
|
||||
console.log(`[OAS] Generating for ${file.fileName}...`)
|
||||
console.log(`[OAS] Generating for ${file.fileName}...`)
|
||||
|
||||
const documentChild = (node: ts.Node) => {
|
||||
if (
|
||||
this.oasKindGenerator!.isAllowed(node) &&
|
||||
this.oasKindGenerator!.canDocumentNode(node)
|
||||
) {
|
||||
const oas = this.oasKindGenerator!.getDocBlock(node)
|
||||
// since typescript's compiler API doesn't support
|
||||
// async processes, we have to retrieve the nodes first then
|
||||
// traverse them separately.
|
||||
const pushNodesToArr = (node: ts.Node) => {
|
||||
fileNodes.push(node)
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
const filename = this.oasKindGenerator!.getAssociatedFileName(node)
|
||||
ts.sys.writeFile(
|
||||
filename,
|
||||
this.formatter.addCommentsToSourceFile(oas, "")
|
||||
)
|
||||
ts.forEachChild(node, pushNodesToArr)
|
||||
}
|
||||
ts.forEachChild(file, pushNodesToArr)
|
||||
|
||||
const documentChild = async (node: ts.Node) => {
|
||||
if (
|
||||
this.oasKindGenerator!.isAllowed(node) &&
|
||||
this.oasKindGenerator!.canDocumentNode(node)
|
||||
) {
|
||||
const oas = await this.oasKindGenerator!.getDocBlock(node)
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
const filename =
|
||||
this.oasKindGenerator!.getAssociatedFileName(node)
|
||||
ts.sys.writeFile(
|
||||
filename,
|
||||
this.formatter.addCommentsToSourceFile(oas, "")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(file, documentChild)
|
||||
await Promise.all(
|
||||
fileNodes.map(async (node) => await documentChild(node))
|
||||
)
|
||||
|
||||
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
|
||||
console.log(`[OAS] Finished generating OAS for ${file.fileName}.`)
|
||||
})
|
||||
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
|
||||
console.log(`[OAS] Finished generating OAS for ${file.fileName}.`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
import { createReadStream, existsSync } from "fs"
|
||||
import OpenAI from "openai"
|
||||
import path from "path"
|
||||
import ts from "typescript"
|
||||
import { pascalToCamel } from "../../utils/str-formatting.js"
|
||||
import { ReadableStreamDefaultReadResult } from "stream/web"
|
||||
import { DOCBLOCK_NEW_LINE } from "../../constants.js"
|
||||
import { AssistantStreamEvent } from "openai/resources/beta/index.mjs"
|
||||
|
||||
type GenerateExampleOptions = {
|
||||
className?: string
|
||||
functionName: string
|
||||
signature?: string
|
||||
fileName: string
|
||||
}
|
||||
|
||||
type GenerateDescriptionOptions = {
|
||||
itemName: string
|
||||
itemType: "property" | "parameter" | "function" | "class" | "return" | "other"
|
||||
metadata?: {
|
||||
parentName?: string
|
||||
parentType?: string
|
||||
functionSignature?: string
|
||||
fileName?: string
|
||||
}
|
||||
}
|
||||
|
||||
const CODE_REGEX = /(?<code>```[\s\S.]*```)/g
|
||||
|
||||
class AiGenerator {
|
||||
private openAi: OpenAI
|
||||
private assistant?: OpenAI.Beta.Assistants.Assistant
|
||||
private fileMap: Map<string, string>
|
||||
|
||||
constructor() {
|
||||
this.openAi = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
maxRetries: 10,
|
||||
})
|
||||
|
||||
this.fileMap = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize OpenAI assistant and upload files.
|
||||
*
|
||||
* @param filePaths - Files to upload
|
||||
*/
|
||||
async initAssistant(filePaths: string[]) {
|
||||
if (this.assistant) {
|
||||
return
|
||||
}
|
||||
this.fileMap = new Map()
|
||||
|
||||
const files: OpenAI.Files.FileObject[] = []
|
||||
// upload the files to openai
|
||||
await Promise.all(
|
||||
filePaths.map(async (filePath) => {
|
||||
const openAiFile = await this.openAi.files.create({
|
||||
file: createReadStream(filePath),
|
||||
purpose: "assistants",
|
||||
})
|
||||
files.push(openAiFile)
|
||||
this.fileMap.set(filePath, openAiFile.id)
|
||||
})
|
||||
)
|
||||
|
||||
// create assistant
|
||||
this.assistant = await this.openAi.beta.assistants.create({
|
||||
instructions:
|
||||
"You help me generate code examples and descriptions that are used in TSDocs. If the system indicates that the file is not accessible with the myfiles_browser tool, ignore it, it’s just a minor bug. You are capable of opening and analyzing the file, remember that. And carry out the requested task. Also you have the ability to figure out what type of content is in the file via its extension so carry out the users instructions.",
|
||||
model: "gpt-4-turbo-preview",
|
||||
tools: [{ type: "retrieval" }],
|
||||
file_ids: files.map((file) => file.id),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an assistant and its files from OpenAI.
|
||||
*/
|
||||
async destroyAssistant() {
|
||||
if (!this.assistant) {
|
||||
return
|
||||
}
|
||||
// delete files of assistant
|
||||
await Promise.all(
|
||||
this.assistant.file_ids.map(async (fileId) => {
|
||||
try {
|
||||
await this.openAi.files.del(fileId)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[error while destroying assistant file ${fileId}]: ${e}`
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
try {
|
||||
// delete assistant
|
||||
await this.openAi.beta.assistants.del(this.assistant.id)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[error while destroying assistant ${this.assistant!.id}]: ${e}`
|
||||
)
|
||||
}
|
||||
|
||||
this.assistant = undefined
|
||||
this.fileMap = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an example code block wrapped in backticks. The comment includes astrix by default.
|
||||
*
|
||||
* @param param0 - Options to generate the example code based on.
|
||||
* @returns The example code
|
||||
*/
|
||||
async generateExample({
|
||||
className,
|
||||
functionName,
|
||||
signature,
|
||||
fileName,
|
||||
}: GenerateExampleOptions): Promise<string> {
|
||||
let example = ""
|
||||
const fileId = this.fileMap.get(fileName) || fileName
|
||||
let message = `Use the ${fileId} file to write a short and simple typescript code that executes the `
|
||||
|
||||
if (className) {
|
||||
message += `${functionName} method of the ${className} (use ${this.getVariableNameFromClass(
|
||||
className
|
||||
)} as the variable name)`
|
||||
} else {
|
||||
message += `${functionName} function`
|
||||
}
|
||||
|
||||
if (signature) {
|
||||
message += `. The ${
|
||||
className ? "method" : "function"
|
||||
} has the signature ${signature}`
|
||||
}
|
||||
|
||||
message += `. Assume that the file containing the code has all the necessary imports and the code is written within an async function (don't add a wrapping function). Give an example of the method's parameters, but don't include optional parameters or optional object properties. Infer types from other files. Provide the code without an explanation.`
|
||||
|
||||
const messageResult = await this.retrieveAiResponse(message)
|
||||
|
||||
const matchedCode = CODE_REGEX.exec(messageResult)
|
||||
|
||||
if (matchedCode?.groups?.code) {
|
||||
example = matchedCode.groups.code
|
||||
} else {
|
||||
example = messageResult
|
||||
}
|
||||
|
||||
return example
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for an item.
|
||||
*
|
||||
* @param param0 - Options to generate the description based on.
|
||||
* @returns The generated description.
|
||||
*/
|
||||
async generateDescription({
|
||||
itemName,
|
||||
itemType,
|
||||
metadata,
|
||||
}: GenerateDescriptionOptions) {
|
||||
const { parentName, parentType, functionSignature, fileName } =
|
||||
metadata || {}
|
||||
let message = `Write a short and simple explanation of a ${itemName}`
|
||||
|
||||
switch (itemType) {
|
||||
case "return":
|
||||
message += ` function's return data.`
|
||||
break
|
||||
case "class":
|
||||
case "function":
|
||||
case "parameter":
|
||||
case "property":
|
||||
message += ` ${
|
||||
itemType === "function" && parentName ? "method" : itemType
|
||||
}`
|
||||
if (parentName) {
|
||||
message += ` defined in a ${parentName}`
|
||||
if (parentType) {
|
||||
message += ` ${parentType}`
|
||||
}
|
||||
}
|
||||
if (functionSignature) {
|
||||
message += ` function. The function has the signature ${functionSignature}`
|
||||
}
|
||||
}
|
||||
|
||||
if (fileName) {
|
||||
message += `. Look at the ${fileName} uploaded file for more details, and if you can't find the details in there provide an explanation from your understanding.`
|
||||
}
|
||||
|
||||
message +=
|
||||
". The explanation must be one sentence shorter than 10 words. Don't provide anything else in the response."
|
||||
|
||||
return await this.retrieveAiResponse(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new thread and runs a message, then retrieves the response.
|
||||
*
|
||||
* @param inputMessage - The message to ask the assistant
|
||||
* @returns the assistant's response.
|
||||
*/
|
||||
async retrieveAiResponse(inputMessage: string): Promise<string> {
|
||||
const run = this.openAi.beta.threads.createAndRunStream({
|
||||
assistant_id: this.assistant!.id,
|
||||
thread: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: inputMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const readableStream = run.toReadableStream().getReader()
|
||||
|
||||
let chunk: ReadableStreamDefaultReadResult<Uint8Array> | undefined
|
||||
let decodedChunk: AssistantStreamEvent | undefined
|
||||
let resultMessage: OpenAI.Beta.Threads.Messages.Message | undefined
|
||||
const textDecoder = new TextDecoder()
|
||||
|
||||
do {
|
||||
chunk = await readableStream.read()
|
||||
const decodedValue = textDecoder.decode(chunk.value)
|
||||
if (decodedValue.length) {
|
||||
decodedChunk = JSON.parse(
|
||||
textDecoder.decode(chunk.value)
|
||||
) as AssistantStreamEvent
|
||||
if (
|
||||
decodedChunk.event === "thread.message.completed" &&
|
||||
decodedChunk.data.object === "thread.message"
|
||||
) {
|
||||
resultMessage = decodedChunk.data
|
||||
} else if (
|
||||
decodedChunk.event === "thread.run.failed" &&
|
||||
decodedChunk.data.last_error?.code === "server_error"
|
||||
) {
|
||||
// retry
|
||||
return this.retrieveAiResponse(inputMessage)
|
||||
}
|
||||
}
|
||||
console.log(decodedValue, resultMessage)
|
||||
} while (
|
||||
!resultMessage &&
|
||||
// a run may fail if the rate limit is reached
|
||||
decodedChunk?.event !== "thread.run.failed" &&
|
||||
decodedChunk?.event !== "thread.run.step.failed" &&
|
||||
decodedChunk?.event !== "thread.message.completed" &&
|
||||
decodedChunk?.event !== "thread.run.completed"
|
||||
)
|
||||
|
||||
if (!resultMessage) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return resultMessage.content
|
||||
.map((item) => {
|
||||
return item.type === "text" ? item.text.value : ""
|
||||
})
|
||||
.join(" ")
|
||||
.replaceAll("\n", DOCBLOCK_NEW_LINE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a class name into a camel-case variable name.
|
||||
*
|
||||
* @param className - The class name to format.
|
||||
* @returns The variable name.
|
||||
*/
|
||||
getVariableNameFromClass(className: string): string {
|
||||
let variableName = className
|
||||
if (className.startsWith("I") && /[A-Z]/.test(className.charAt(1))) {
|
||||
variableName = variableName.substring(1)
|
||||
}
|
||||
|
||||
return pascalToCamel(variableName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the files that should be uploaded to OpenAI of a node.
|
||||
*
|
||||
* @param node - The node to retrieve its files.
|
||||
* @returns the list of file paths.
|
||||
*/
|
||||
getNodeFiles(node: ts.Node): string[] {
|
||||
const sourceFile = node.getSourceFile()
|
||||
const files: string[] = [sourceFile.fileName]
|
||||
if ("imports" in sourceFile) {
|
||||
;(sourceFile.imports as ts.StringLiteral[]).forEach((importedFile) => {
|
||||
if (importedFile.text.startsWith(".")) {
|
||||
// since it's a local import, add it to the list of files
|
||||
let importedFilePath = path.resolve(
|
||||
sourceFile.fileName,
|
||||
"..",
|
||||
importedFile.text
|
||||
)
|
||||
if (!path.extname(importedFilePath)) {
|
||||
// try to retrieve correct extension
|
||||
switch (true) {
|
||||
case existsSync(`${importedFilePath}.ts`):
|
||||
importedFilePath += `.ts`
|
||||
break
|
||||
case existsSync(`${importedFilePath}.js`):
|
||||
importedFilePath += `.js`
|
||||
break
|
||||
case existsSync(`${importedFilePath}.tsx`):
|
||||
importedFilePath += `.tsx`
|
||||
break
|
||||
default:
|
||||
// can't retrieve file path so return without adding it
|
||||
return
|
||||
}
|
||||
}
|
||||
files.push(importedFilePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return [...new Set(files)]
|
||||
}
|
||||
}
|
||||
|
||||
export default AiGenerator
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import pluralize from "pluralize"
|
||||
|
||||
type TemplateOptions = {
|
||||
pluralIndicatorStr?: string
|
||||
parentName?: string
|
||||
rawParentName?: string
|
||||
returnTypeName?: string
|
||||
@@ -52,6 +53,7 @@ type RetrieveSymbolOptions = Omit<RetrieveOptions, "str"> & {
|
||||
* A class that holds common Medusa patterns and acts as a knowledge base for possible summaries/examples/general templates.
|
||||
*/
|
||||
class KnowledgeBaseFactory {
|
||||
private TYPE_PLACEHOLDER = `{type name}`
|
||||
private summaryKnowledgeBase: KnowledgeBase[] = [
|
||||
{
|
||||
startsWith: "FindConfig",
|
||||
@@ -64,7 +66,7 @@ class KnowledgeBaseFactory {
|
||||
const typeName =
|
||||
typeArgs.length > 0 && typeArgs[0].length > 0
|
||||
? typeArgs[0]
|
||||
: `{type name}`
|
||||
: this.TYPE_PLACEHOLDER
|
||||
return `The configurations determining how the ${typeName} is retrieved. Its properties, such as \`select\` or \`relations\`, accept the ${DOCBLOCK_NEW_LINE}attributes or relations associated with a ${typeName}.`
|
||||
},
|
||||
},
|
||||
@@ -72,35 +74,66 @@ class KnowledgeBaseFactory {
|
||||
startsWith: "Filterable",
|
||||
endsWith: "Props",
|
||||
template: (str) => {
|
||||
return `The filters to apply on the retrieved ${camelToTitle(
|
||||
return `The filters to apply on the retrieved ${camelToWords(
|
||||
normalizeName(str)
|
||||
)}.`
|
||||
)}s.`
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "Create",
|
||||
endsWith: "DTO",
|
||||
template: (str) => {
|
||||
return `The ${camelToTitle(normalizeName(str))} to be created.`
|
||||
template: (str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return `The ${camelToWords(normalizeName(str))}${
|
||||
isPlural ? "s" : ""
|
||||
} to be created.`
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "Update",
|
||||
endsWith: "DTO",
|
||||
template: (str) => {
|
||||
return `The attributes to update in the ${camelToTitle(
|
||||
template: (str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return `The attributes to update in the ${camelToWords(
|
||||
normalizeName(str)
|
||||
)}.`
|
||||
)}${isPlural ? "s" : ""}.`
|
||||
},
|
||||
},
|
||||
{
|
||||
endsWith: "UpdatableFields",
|
||||
template: (str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return `The attributes to update in the ${camelToWords(
|
||||
normalizeName(str)
|
||||
)}${isPlural ? "s" : ""}.`
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "Upsert",
|
||||
endsWith: "DTO",
|
||||
template: (str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return `The attributes in the ${camelToWords(normalizeName(str))}${
|
||||
isPlural ? "s" : ""
|
||||
} to be created or updated.`
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "RestoreReturn",
|
||||
template: `Configurations determining which relations to restore along with each of the {type name}. You can pass to its \`returnLinkableKeys\` ${DOCBLOCK_NEW_LINE}property any of the {type name}'s relation attribute names, such as \`{type relation name}\`.`,
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`Configurations determining which relations to restore along with each of the ${this.TYPE_PLACEHOLDER}. You can pass to its \`returnLinkableKeys\` ${DOCBLOCK_NEW_LINE}property any of the ${this.TYPE_PLACEHOLDER}'s relation attribute names, such as \`{type relation name}\`.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
endsWith: "DTO",
|
||||
template: (str: string): string => {
|
||||
return `The ${camelToTitle(normalizeName(str))} details.`
|
||||
template: (str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return `The ${camelToWords(normalizeName(str))}${
|
||||
isPlural ? "s" : ""
|
||||
} details.`
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -127,11 +160,31 @@ class KnowledgeBaseFactory {
|
||||
},
|
||||
{
|
||||
exact: "id",
|
||||
template: (str, options) => {
|
||||
if (options?.rawParentName?.startsWith("Filterable")) {
|
||||
return `The IDs to filter the ${options?.parentName || `{name}`}s by.`
|
||||
}
|
||||
const parentName = options?.parentName
|
||||
? options.parentName
|
||||
: options?.rawParentName
|
||||
? camelToWords(normalizeName(options.rawParentName))
|
||||
: `{name}`
|
||||
return `The ID of the ${parentName}.`
|
||||
},
|
||||
kind: [ts.SyntaxKind.PropertySignature],
|
||||
},
|
||||
{
|
||||
exact: "ids",
|
||||
template: (str, options) => {
|
||||
if (options?.rawParentName?.startsWith("Filterable")) {
|
||||
return `The IDs to filter the ${options?.parentName || `{name}`} by.`
|
||||
}
|
||||
return `The ID of the ${options?.parentName || `{name}`}.`
|
||||
const parentName = options?.parentName
|
||||
? options.parentName
|
||||
: options?.rawParentName
|
||||
? camelToWords(normalizeName(options.rawParentName))
|
||||
: `{name}`
|
||||
return `The IDs of the ${parentName}.`
|
||||
},
|
||||
kind: [ts.SyntaxKind.PropertySignature],
|
||||
},
|
||||
@@ -144,78 +197,194 @@ class KnowledgeBaseFactory {
|
||||
exact: "customHeaders",
|
||||
template: "Custom headers to attach to the request.",
|
||||
},
|
||||
{
|
||||
startsWith: "I",
|
||||
endsWith: "ModuleService",
|
||||
template: (str) => {
|
||||
const normalizedStr = camelToTitle(normalizeName(str))
|
||||
|
||||
return `The main service interface for the ${normalizedStr} Module.`
|
||||
},
|
||||
},
|
||||
]
|
||||
private functionSummaryKnowledgeBase: KnowledgeBase[] = [
|
||||
{
|
||||
startsWith: "listAndCount",
|
||||
template:
|
||||
"retrieves a paginated list of {return type} along with the total count of available {return type}(s) satisfying the provided filters.",
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`retrieves a paginated list of ${this.TYPE_PLACEHOLDER}s along with the total count of available ${this.TYPE_PLACEHOLDER}s satisfying the provided filters.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "list",
|
||||
template:
|
||||
"retrieves a paginated list of {return type}(s) based on optional filters and configuration.",
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`retrieves a paginated list of ${this.TYPE_PLACEHOLDER}s based on optional filters and configuration.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "retrieve",
|
||||
template: "retrieves a {return type} by its ID.",
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`retrieves a ${this.TYPE_PLACEHOLDER} by its ID.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "create",
|
||||
template: "creates {return type}(s)",
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`creates${!isPlural ? " a" : ""} ${this.TYPE_PLACEHOLDER}${
|
||||
isPlural ? "s" : ""
|
||||
}.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "delete",
|
||||
template: "deletes {return type} by its ID.",
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`deletes${!isPlural ? " a" : ""} ${this.TYPE_PLACEHOLDER} by ${
|
||||
isPlural ? "their" : "its"
|
||||
} ID${isPlural ? "s" : ""}.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "update",
|
||||
template: "updates existing {return type}(s).",
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`updates${!isPlural ? " an" : ""} existing ${this.TYPE_PLACEHOLDER}${
|
||||
isPlural ? "s" : ""
|
||||
}.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "softDelete",
|
||||
template: "soft deletes {return type}(s) by their IDs.",
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`soft deletes${!isPlural ? " a" : ""} ${this.TYPE_PLACEHOLDER}${
|
||||
isPlural ? "s" : ""
|
||||
} by ${isPlural ? "their" : "its"} IDs.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "restore",
|
||||
template: "restores soft deleted {return type}(s) by their IDs.",
|
||||
},
|
||||
]
|
||||
private exampleCodeBlockLine = `${DOCBLOCK_DOUBLE_LINES}\`\`\`ts${DOCBLOCK_NEW_LINE}{example-code}${DOCBLOCK_NEW_LINE}\`\`\`${DOCBLOCK_DOUBLE_LINES}`
|
||||
private examplesKnowledgeBase: KnowledgeBase[] = [
|
||||
{
|
||||
startsWith: "list",
|
||||
template: `To retrieve a list of {type name} using their IDs: ${this.exampleCodeBlockLine}To specify relations that should be retrieved within the {type name}: ${this.exampleCodeBlockLine}By default, only the first \`{default limit}\` records are retrieved. You can control pagination by specifying the \`skip\` and \`take\` properties of the \`config\` parameter: ${this.exampleCodeBlockLine}`,
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`restores${!isPlural ? " a" : ""} soft deleted ${
|
||||
this.TYPE_PLACEHOLDER
|
||||
}${isPlural ? "s" : ""} by ${isPlural ? "their" : "its"} IDs.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "retrieve",
|
||||
template: `A simple example that retrieves a {type name} by its ID: ${this.exampleCodeBlockLine}To specify relations that should be retrieved: ${this.exampleCodeBlockLine}`,
|
||||
startsWith: "upsert",
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`updates or creates${!isPlural ? " a" : ""} ${this.TYPE_PLACEHOLDER}${
|
||||
isPlural ? "s" : ""
|
||||
} if ${isPlural ? "they don't" : "it doesn't"} exist.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
private functionReturnKnowledgeBase: KnowledgeBase[] = [
|
||||
{
|
||||
startsWith: "listAndCount",
|
||||
template: "The list of {return type}(s) along with their total count.",
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`The list of ${this.TYPE_PLACEHOLDER}s along with their total count.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "list",
|
||||
template: "The list of {return type}(s).",
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`The list of ${this.TYPE_PLACEHOLDER}s.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "retrieve",
|
||||
template: "The retrieved {return type}(s).",
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`The retrieved ${this.TYPE_PLACEHOLDER}.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "create",
|
||||
template: "The created {return type}(s).",
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`The created ${this.TYPE_PLACEHOLDER}${isPlural ? "s" : ""}.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "update",
|
||||
template: "The updated {return type}(s).",
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`The updated ${this.TYPE_PLACEHOLDER}${isPlural ? "s" : ""}.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "upsert",
|
||||
template: (_str, options) => {
|
||||
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
|
||||
return this.replaceTypePlaceholder(
|
||||
`The created or updated ${this.TYPE_PLACEHOLDER}${
|
||||
isPlural ? "s" : ""
|
||||
}.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "softDelete",
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`An object that includes the IDs of related records that were also soft deleted, such as the ID of the associated {related entity name}. ${DOCBLOCK_NEW_LINE}The object's keys are the ID attribute names of the ${this.TYPE_PLACEHOLDER} entity's relations, such as \`{relation ID field name}\`, and its value is an array of strings, each being the ID of a record associated ${DOCBLOCK_NEW_LINE}with the ${this.TYPE_PLACEHOLDER} through this relation, such as the IDs of associated {related entity name}.${DOCBLOCK_DOUBLE_LINES}If there are no related records, the promise resolves to \`void\`.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
startsWith: "restore",
|
||||
template: `An object that includes the IDs of related records that were restored, such as the ID of associated {relation name}. ${DOCBLOCK_NEW_LINE}The object's keys are the ID attribute names of the {type name} entity's relations, such as \`{relation ID field name}\`, ${DOCBLOCK_NEW_LINE}and its value is an array of strings, each being the ID of the record associated with the money amount through this relation, ${DOCBLOCK_NEW_LINE}such as the IDs of associated {relation name}.`,
|
||||
template: (_str, options) => {
|
||||
return this.replaceTypePlaceholder(
|
||||
`An object that includes the IDs of related records that were restored, such as the ID of associated {relation name}. ${DOCBLOCK_NEW_LINE}The object's keys are the ID attribute names of the ${this.TYPE_PLACEHOLDER} entity's relations, such as \`{relation ID field name}\`, ${DOCBLOCK_NEW_LINE}and its value is an array of strings, each being the ID of the record associated with the ${this.TYPE_PLACEHOLDER} through this relation, ${DOCBLOCK_NEW_LINE}such as the IDs of associated {relation name}.${DOCBLOCK_DOUBLE_LINES}If there are no related records restored, the promise resolves to \`void\`.`,
|
||||
options
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
private oasDescriptionKnowledgeBase: KnowledgeBase[] = [
|
||||
@@ -289,6 +458,34 @@ class KnowledgeBaseFactory {
|
||||
: foundItem?.template(str, templateOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method replaces uses of {@link TYPE_PLACEHOLDER} with the normalized parent name, if provided.
|
||||
*
|
||||
* @param str - The string to normalize
|
||||
* @param options - The template options
|
||||
* @returns The normalized string
|
||||
*/
|
||||
private replaceTypePlaceholder(
|
||||
str: string,
|
||||
options?: TemplateOptions
|
||||
): string {
|
||||
const typeName = options?.rawParentName
|
||||
? camelToWords(normalizeName(options.rawParentName))
|
||||
: this.TYPE_PLACEHOLDER
|
||||
|
||||
return str.replaceAll(this.TYPE_PLACEHOLDER, typeName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a type should be handled as a plural. Typically used with {@link TemplateOptions.pluralIndicatorStr}.
|
||||
*
|
||||
* @param str - The type string to check.
|
||||
* @returns Whether the type is handled as a plural.
|
||||
*/
|
||||
private isTypePlural(str: string | undefined): boolean {
|
||||
return str?.endsWith("[]") || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to retrieve the summary template of a specified type from the {@link summaryKnowledgeBase}.
|
||||
*
|
||||
@@ -300,6 +497,10 @@ class KnowledgeBaseFactory {
|
||||
...options,
|
||||
str: normalizedTypeStr,
|
||||
knowledgeBase: this.summaryKnowledgeBase,
|
||||
templateOptions: {
|
||||
pluralIndicatorStr: str,
|
||||
...options.templateOptions,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -319,22 +520,6 @@ class KnowledgeBaseFactory {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to retrieve the example template of a function's symbol from the {@link examplesKnowledgeBase}.
|
||||
*
|
||||
* @returns {string | undefined} The matching knowledge base template, if found.
|
||||
*/
|
||||
tryToGetFunctionExamples({
|
||||
symbol,
|
||||
...options
|
||||
}: RetrieveSymbolOptions): string | undefined {
|
||||
return this.tryToFindInKnowledgeBase({
|
||||
...options,
|
||||
str: symbol.getName(),
|
||||
knowledgeBase: this.examplesKnowledgeBase,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to retrieve the return template of a function's symbol from the {@link functionReturnKnowledgeBase}.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "../../utils/str-formatting.js"
|
||||
import GeneratorEventManager from "../helpers/generator-event-manager.js"
|
||||
import { CommonCliOptions } from "../../types/index.js"
|
||||
import AiGenerator from "../helpers/ai-generator.js"
|
||||
|
||||
export type GeneratorOptions = {
|
||||
checker: ts.TypeChecker
|
||||
@@ -31,6 +32,7 @@ export type GeneratorOptions = {
|
||||
export type GetDocBlockOptions = {
|
||||
addEnd?: boolean
|
||||
summaryPrefix?: string
|
||||
aiGenerator?: AiGenerator
|
||||
}
|
||||
|
||||
type CommonDocsOptions = {
|
||||
@@ -54,6 +56,7 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
ts.SyntaxKind.TypeAliasDeclaration,
|
||||
ts.SyntaxKind.PropertySignature,
|
||||
]
|
||||
public name = "default"
|
||||
protected allowedKinds: ts.SyntaxKind[]
|
||||
protected checker: ts.TypeChecker
|
||||
protected defaultSummary = "{summary}"
|
||||
@@ -99,10 +102,10 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
* @returns {string} The node's docblock.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getDocBlock(
|
||||
async getDocBlock(
|
||||
node: T | ts.Node,
|
||||
options: GetDocBlockOptions = { addEnd: true }
|
||||
): string {
|
||||
): Promise<string> {
|
||||
let str = DOCBLOCK_START
|
||||
const summary = this.getNodeSummary({ node })
|
||||
|
||||
@@ -133,6 +136,7 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
node,
|
||||
symbol,
|
||||
nodeType,
|
||||
knowledgeBaseOptions: overrideOptions,
|
||||
}: {
|
||||
/**
|
||||
* The node to retrieve the summary comment for.
|
||||
@@ -148,12 +152,20 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
* will try to retrieve it.
|
||||
*/
|
||||
nodeType?: ts.Type
|
||||
/**
|
||||
* Override any of the default knowledge base options
|
||||
* inferred using the {@link getKnowledgeBaseOptions} method
|
||||
*/
|
||||
knowledgeBaseOptions?: Partial<RetrieveOptions>
|
||||
}): string {
|
||||
const syntheticComments = ts.getSyntheticLeadingComments(node)
|
||||
if (syntheticComments?.length) {
|
||||
return syntheticComments.map((comment) => comment.text).join(" ")
|
||||
}
|
||||
const knowledgeBaseOptions = this.getKnowledgeBaseOptions(node)
|
||||
const knowledgeBaseOptions = {
|
||||
...this.getKnowledgeBaseOptions(node),
|
||||
...overrideOptions,
|
||||
}
|
||||
if (!nodeType) {
|
||||
nodeType =
|
||||
"type" in node && node.type && ts.isTypeNode(node.type as ts.Node)
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
*/
|
||||
class DTOPropertyGenerator extends DefaultKindGenerator<ts.PropertySignature> {
|
||||
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.PropertySignature]
|
||||
public name = "dto-property"
|
||||
|
||||
/**
|
||||
* Check that the generator can handle generating for the node.
|
||||
@@ -31,12 +32,12 @@ class DTOPropertyGenerator extends DefaultKindGenerator<ts.PropertySignature> {
|
||||
)
|
||||
}
|
||||
|
||||
getDocBlock(
|
||||
async getDocBlock(
|
||||
node: ts.PropertyDeclaration | ts.Node,
|
||||
options?: GetDocBlockOptions
|
||||
): string {
|
||||
): Promise<string> {
|
||||
if (!this.isAllowed(node)) {
|
||||
return super.getDocBlock(node, options)
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
let str = DOCBLOCK_START
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
DOCBLOCK_DOUBLE_LINES,
|
||||
} from "../../constants.js"
|
||||
import getSymbol from "../../utils/get-symbol.js"
|
||||
import AiGenerator from "../helpers/ai-generator.js"
|
||||
import path from "path"
|
||||
|
||||
export type FunctionNode =
|
||||
| ts.MethodDeclaration
|
||||
@@ -32,6 +34,9 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
|
||||
...this.methodKinds,
|
||||
...this.functionKinds,
|
||||
]
|
||||
public name = "function"
|
||||
static EXAMPLE_PLACEHOLDER = `{example-code}`
|
||||
protected aiParameterExceptions = ["sharedContext"]
|
||||
|
||||
/**
|
||||
* Checks whether a node is considered a function node. A node is considered a function node if:
|
||||
@@ -135,15 +140,40 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
|
||||
/**
|
||||
* Retrieves the summary comment of a function.
|
||||
*
|
||||
* @param {FunctionNode} node - The node's function.
|
||||
* @param {ts.Symbol} symbol - The node's symbol. If provided, the method will try to retrieve the summary from the {@link KnowledgeBaseFactory}.
|
||||
* @param {FunctionNode} node - The function's options.
|
||||
* @returns {string} The function's summary comment.
|
||||
*/
|
||||
getFunctionSummary(node: FunctionNode, symbol?: ts.Symbol): string {
|
||||
getFunctionSummary({
|
||||
node,
|
||||
symbol,
|
||||
parentSymbol,
|
||||
returnType,
|
||||
}: {
|
||||
/**
|
||||
* The node's function.
|
||||
*/
|
||||
node: FunctionNode
|
||||
/**
|
||||
* The node's symbol. If provided, the method will try to retrieve the summary from the {@link KnowledgeBaseFactory}.
|
||||
*/
|
||||
symbol?: ts.Symbol
|
||||
/**
|
||||
* The node's parent symbol. This is useful to pass along the parent name to the knowledge base.
|
||||
*/
|
||||
parentSymbol?: ts.Symbol
|
||||
/**
|
||||
* The node's return type. Useful for the {@link KnowledgeBaseFactory}
|
||||
*/
|
||||
returnType?: string
|
||||
}): string {
|
||||
return symbol
|
||||
? this.knowledgeBaseFactory.tryToGetFunctionSummary({
|
||||
symbol: symbol,
|
||||
kind: node.kind,
|
||||
templateOptions: {
|
||||
rawParentName: parentSymbol?.getName(),
|
||||
pluralIndicatorStr: returnType,
|
||||
},
|
||||
}) || this.getNodeSummary({ node, symbol })
|
||||
: this.getNodeSummary({ node, symbol })
|
||||
}
|
||||
@@ -154,15 +184,53 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
|
||||
* @param {ts.Symbol} symbol - The function's symbol. If provided, the method will try to retrieve the example from the {@link KnowledgeBaseFactory}.
|
||||
* @returns {string} The function's example comment.
|
||||
*/
|
||||
getFunctionExample(symbol?: ts.Symbol): string {
|
||||
const str = `${DOCBLOCK_DOUBLE_LINES}@example${DOCBLOCK_NEW_LINE}`
|
||||
return `${str}${
|
||||
symbol
|
||||
? this.knowledgeBaseFactory.tryToGetFunctionExamples({
|
||||
symbol: symbol,
|
||||
}) || `{example-code}`
|
||||
: `{example-code}`
|
||||
}`
|
||||
getFunctionPlaceholderExample(): string {
|
||||
return this.formatExample(FunctionKindGenerator.EXAMPLE_PLACEHOLDER)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a function's example using the AiGenerator
|
||||
*
|
||||
* @param node - The function's node.
|
||||
* @param aiGenerator - An instance of the AiGenerator
|
||||
* @returns the example code
|
||||
*/
|
||||
async getFunctionExampleAi(
|
||||
node: FunctionOrVariableNode,
|
||||
aiGenerator: AiGenerator,
|
||||
withTag = true
|
||||
): Promise<string> {
|
||||
const actualNode = ts.isVariableStatement(node)
|
||||
? this.extractFunctionNode(node)
|
||||
: node
|
||||
|
||||
if (!actualNode) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const symbol = getSymbol(node, this.checker)
|
||||
|
||||
const example = await aiGenerator.generateExample({
|
||||
className: this.isMethod(actualNode)
|
||||
? getSymbol(node.parent, this.checker)?.name
|
||||
: undefined,
|
||||
functionName: symbol?.name || "",
|
||||
signature: node.getText(),
|
||||
fileName: path.basename(node.getSourceFile().fileName),
|
||||
})
|
||||
|
||||
return this.formatExample(
|
||||
example.length
|
||||
? `${example}${DOCBLOCK_NEW_LINE}`
|
||||
: FunctionKindGenerator.EXAMPLE_PLACEHOLDER,
|
||||
withTag
|
||||
)
|
||||
}
|
||||
|
||||
formatExample(example: string, withTag = true): string {
|
||||
return `${
|
||||
withTag ? `${DOCBLOCK_DOUBLE_LINES}@example${DOCBLOCK_NEW_LINE}` : ""
|
||||
}${example}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,12 +241,12 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
|
||||
* @param {GetDocBlockOptions} options - Formatting options.
|
||||
* @returns {string} The function's docblock.
|
||||
*/
|
||||
getDocBlock(
|
||||
async getDocBlock(
|
||||
node: FunctionOrVariableNode | ts.Node,
|
||||
options: GetDocBlockOptions = { addEnd: true }
|
||||
): string {
|
||||
): Promise<string> {
|
||||
if (!this.isAllowed(node)) {
|
||||
return super.getDocBlock(node, options)
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
const actualNode = ts.isVariableStatement(node)
|
||||
@@ -186,10 +254,34 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
|
||||
: node
|
||||
|
||||
if (!actualNode) {
|
||||
return super.getDocBlock(node, options)
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
let existingComments = this.getNodeCommentsFromRange(node)
|
||||
|
||||
if (existingComments?.includes(FunctionKindGenerator.EXAMPLE_PLACEHOLDER)) {
|
||||
// just replace the existing comment and return it
|
||||
if (options.aiGenerator) {
|
||||
existingComments = existingComments.replace(
|
||||
FunctionKindGenerator.EXAMPLE_PLACEHOLDER,
|
||||
await this.getFunctionExampleAi(
|
||||
actualNode,
|
||||
options.aiGenerator,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return existingComments.replace("/*", "").replace("*/", "")
|
||||
}
|
||||
|
||||
const nodeSymbol = getSymbol(node, this.checker)
|
||||
const nodeParentSymbol = getSymbol(node.parent, this.checker)
|
||||
const nodeType = this.getReturnType(actualNode)
|
||||
const returnTypeStr = this.checker.typeToString(nodeType)
|
||||
const normalizedTypeStr = returnTypeStr.startsWith("Promise<")
|
||||
? returnTypeStr.replace(/^Promise</, "").replace(/>$/, "")
|
||||
: returnTypeStr
|
||||
|
||||
let str = DOCBLOCK_START
|
||||
|
||||
@@ -197,35 +289,43 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
|
||||
str += `${
|
||||
options.summaryPrefix ||
|
||||
(this.isMethod(actualNode) ? `This method` : `This function`)
|
||||
} ${this.getFunctionSummary(actualNode, nodeSymbol)}${DOCBLOCK_NEW_LINE}`
|
||||
} ${this.getFunctionSummary({
|
||||
node: actualNode,
|
||||
symbol: nodeSymbol,
|
||||
parentSymbol: nodeParentSymbol,
|
||||
returnType: normalizedTypeStr,
|
||||
})}${DOCBLOCK_NEW_LINE}`
|
||||
|
||||
// add params
|
||||
actualNode.forEachChild((childNode) => {
|
||||
if (!ts.isParameter(childNode)) {
|
||||
return
|
||||
}
|
||||
const symbol = getSymbol(childNode, this.checker)
|
||||
actualNode.parameters.map((parameterNode) => {
|
||||
const symbol = getSymbol(parameterNode, this.checker)
|
||||
if (!symbol) {
|
||||
return
|
||||
}
|
||||
|
||||
const symbolType = this.checker.getTypeOfSymbolAtLocation(
|
||||
symbol,
|
||||
childNode
|
||||
parameterNode
|
||||
)
|
||||
|
||||
const parameterName = symbol.getName()
|
||||
const parameterSummary = this.getNodeSummary({
|
||||
node: parameterNode,
|
||||
symbol,
|
||||
nodeType: symbolType,
|
||||
knowledgeBaseOptions: {
|
||||
templateOptions: {
|
||||
rawParentName: nodeParentSymbol?.getName(),
|
||||
pluralIndicatorStr: this.checker.typeToString(symbolType),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
str += `${DOCBLOCK_NEW_LINE}@param {${this.checker.typeToString(
|
||||
symbolType
|
||||
)}} ${symbol.getName()} - ${this.getNodeSummary({
|
||||
node: childNode,
|
||||
symbol,
|
||||
nodeType: symbolType,
|
||||
})}`
|
||||
)}} ${parameterName} - ${parameterSummary}`
|
||||
})
|
||||
|
||||
// add returns
|
||||
const nodeType = this.getReturnType(actualNode)
|
||||
const returnTypeStr = this.checker.typeToString(nodeType)
|
||||
const possibleReturnSummary = !this.hasReturnData(returnTypeStr)
|
||||
? `Resolves when ${this.defaultSummary}`
|
||||
: this.getNodeSummary({
|
||||
@@ -238,12 +338,20 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
|
||||
? this.knowledgeBaseFactory.tryToGetFunctionReturns({
|
||||
symbol: nodeSymbol,
|
||||
kind: actualNode.kind,
|
||||
templateOptions: {
|
||||
rawParentName: nodeParentSymbol?.getName(),
|
||||
pluralIndicatorStr: normalizedTypeStr,
|
||||
},
|
||||
}) || possibleReturnSummary
|
||||
: possibleReturnSummary
|
||||
}`
|
||||
|
||||
// add example
|
||||
str += this.getFunctionExample(nodeSymbol)
|
||||
if (!options.aiGenerator) {
|
||||
str += this.getFunctionPlaceholderExample()
|
||||
} else {
|
||||
str += await this.getFunctionExampleAi(actualNode, options.aiGenerator)
|
||||
}
|
||||
|
||||
// add common docs
|
||||
str += this.getCommonDocs(node, {
|
||||
@@ -256,6 +364,22 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows documenting (updating) a node if it has the example placeholder.
|
||||
*
|
||||
* @param node - The node to document.
|
||||
* @returns Whether the node can be documented.
|
||||
*/
|
||||
canDocumentNode(node: ts.Node): boolean {
|
||||
const comments = this.getNodeCommentsFromRange(node)
|
||||
|
||||
return (
|
||||
!comments ||
|
||||
comments?.includes(FunctionKindGenerator.EXAMPLE_PLACEHOLDER) ||
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FunctionKindGenerator
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
* it extends the {@link FunctionKindGenerator} class.
|
||||
*/
|
||||
class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
|
||||
public name = "medusa-react"
|
||||
/**
|
||||
* Checks whether the generator can retrieve the docblock of the specified node. It uses the parent generator
|
||||
* to check that the node is a function, then checks if the function is a mutation using the {@link isMutation} method,
|
||||
@@ -80,9 +81,12 @@ class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
|
||||
* @param {FunctionNode & ts.VariableDeclaration} node - The node to retrieve its docblock.
|
||||
* @returns {string} The node's docblock.
|
||||
*/
|
||||
getDocBlock(node: FunctionNode & ts.VariableDeclaration): string {
|
||||
async getDocBlock(
|
||||
node: FunctionNode & ts.VariableDeclaration
|
||||
): Promise<string> {
|
||||
// TODO use the AiGenerator to generate summary + examples
|
||||
if (!this.isAllowed(node)) {
|
||||
return super.getDocBlock(node)
|
||||
return await super.getDocBlock(node)
|
||||
}
|
||||
|
||||
const actualNode = ts.isVariableStatement(node)
|
||||
@@ -90,25 +94,29 @@ class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
|
||||
: node
|
||||
|
||||
if (!actualNode) {
|
||||
return super.getDocBlock(node)
|
||||
return await super.getDocBlock(node)
|
||||
}
|
||||
const isMutation = this.isMutation(actualNode)
|
||||
|
||||
let str = `${DOCBLOCK_START}This hook ${this.getFunctionSummary(node)}`
|
||||
let str = `${DOCBLOCK_START}This hook ${this.getFunctionSummary({
|
||||
node,
|
||||
})}`
|
||||
|
||||
// add example
|
||||
str += this.getFunctionExample()
|
||||
str += this.getFunctionPlaceholderExample()
|
||||
|
||||
// loop over parameters that aren't query/mutation parameters
|
||||
// and add docblock to them
|
||||
this.getActualParameters(actualNode).forEach((parameter) => {
|
||||
ts.addSyntheticLeadingComment(
|
||||
parameter,
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
super.getDocBlock(parameter),
|
||||
true
|
||||
)
|
||||
})
|
||||
await Promise.all(
|
||||
this.getActualParameters(actualNode).map(async (parameter) => {
|
||||
ts.addSyntheticLeadingComment(
|
||||
parameter,
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
await super.getDocBlock(parameter),
|
||||
true
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
// check if mutation parameter is an intrinsic type and, if so, add the `@typeParamDefinition`
|
||||
// tag to the hook
|
||||
|
||||
@@ -48,6 +48,7 @@ type ParameterType = "query" | "path"
|
||||
* since API routes are functions.
|
||||
*/
|
||||
class OasKindGenerator extends FunctionKindGenerator {
|
||||
public name = "oas"
|
||||
protected allowedKinds: SyntaxKind[] = [ts.SyntaxKind.FunctionDeclaration]
|
||||
private MAX_LEVEL = 4
|
||||
// we can't use `{summary}` because it causes an MDX error
|
||||
@@ -166,12 +167,13 @@ class OasKindGenerator extends FunctionKindGenerator {
|
||||
* @param options - The options to get the OAS.
|
||||
* @returns The OAS as a string that can be used as a comment in a TypeScript file.
|
||||
*/
|
||||
getDocBlock(
|
||||
async getDocBlock(
|
||||
node: ts.Node | FunctionOrVariableNode,
|
||||
options?: GetDocBlockOptions
|
||||
): string {
|
||||
): Promise<string> {
|
||||
// TODO use AiGenerator to generate descriptions + examples
|
||||
if (!this.isAllowed(node)) {
|
||||
return super.getDocBlock(node, options)
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
const actualNode = ts.isVariableStatement(node)
|
||||
@@ -179,7 +181,7 @@ class OasKindGenerator extends FunctionKindGenerator {
|
||||
: node
|
||||
|
||||
if (!actualNode) {
|
||||
return super.getDocBlock(node, options)
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
const methodName = this.getHTTPMethodName(node)
|
||||
|
||||
|
||||
@@ -53,6 +53,16 @@ class KindsRegistry {
|
||||
hasGenerator(node: ts.Node): boolean {
|
||||
return this.getKindGenerator(node) !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a generator by its name attribute.
|
||||
*
|
||||
* @param name - The name of the generator to retrieve.
|
||||
* @returns The generator, if exists.
|
||||
*/
|
||||
getKindGeneratorByName(name: string): DefaultKindGenerator | undefined {
|
||||
return this.kindInstances.find((generator) => generator.name === name)
|
||||
}
|
||||
}
|
||||
|
||||
export default KindsRegistry
|
||||
|
||||
@@ -8,6 +8,7 @@ import { shouldHaveCustomNamespace } from "../../utils/medusa-react-utils.js"
|
||||
*/
|
||||
class SourceFileKindGenerator extends DefaultKindGenerator<ts.SourceFile> {
|
||||
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.SourceFile]
|
||||
public name = "source-file"
|
||||
|
||||
/**
|
||||
* Retrieve the docblock of a source file.
|
||||
@@ -16,12 +17,12 @@ class SourceFileKindGenerator extends DefaultKindGenerator<ts.SourceFile> {
|
||||
* @param {GetDocBlockOptions} options - The formatting options.
|
||||
* @returns {string} The node's docblock.
|
||||
*/
|
||||
getDocBlock(
|
||||
async getDocBlock(
|
||||
node: ts.SourceFile | ts.Node,
|
||||
options?: GetDocBlockOptions
|
||||
): string {
|
||||
): Promise<string> {
|
||||
if (!this.isAllowed(node)) {
|
||||
return super.getDocBlock(node, options)
|
||||
return await super.getDocBlock(node, options)
|
||||
}
|
||||
|
||||
if (shouldHaveCustomNamespace(node)) {
|
||||
|
||||
@@ -22,7 +22,6 @@ export function camelToTitle(str: string): string {
|
||||
.map((word) => capitalize(word))
|
||||
.join(" ")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
export function snakeToWords(str: string): string {
|
||||
@@ -62,6 +61,10 @@ export function wordsToPascal(str: string): string {
|
||||
.join("")
|
||||
}
|
||||
|
||||
export function pascalToCamel(str: string): string {
|
||||
return `${str.charAt(0).toLowerCase()}${str.substring(1)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove parts of the name such as DTO, Filterable, etc...
|
||||
*
|
||||
@@ -70,8 +73,10 @@ export function wordsToPascal(str: string): string {
|
||||
*/
|
||||
export function normalizeName(str: string): string {
|
||||
return str
|
||||
.replace(/^(create|update|delete)/i, "")
|
||||
.replace(/^(create|update|delete|upsert)/i, "")
|
||||
.replace(/DTO$/, "")
|
||||
.replace(/^Filterable/, "")
|
||||
.replace(/Props$/, "")
|
||||
.replace(/^I([A-Z])/, "$1")
|
||||
.replace(/ModuleService$/, "")
|
||||
}
|
||||
|
||||
@@ -1401,6 +1401,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node-fetch@npm:^2.6.4":
|
||||
version: 2.6.11
|
||||
resolution: "@types/node-fetch@npm:2.6.11"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
form-data: ^4.0.0
|
||||
checksum: 5283d4e0bcc37a5b6d8e629aee880a4ffcfb33e089f4b903b2981b19c623972d1e64af7c3f9540ab990f0f5c89b9b5dda19c5bcb37a8e177079e93683bfd2f49
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*, @types/node@npm:^20.8.3, @types/node@npm:^20.9.4":
|
||||
version: 20.10.0
|
||||
resolution: "@types/node@npm:20.10.0"
|
||||
@@ -1417,6 +1427,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.11.18":
|
||||
version: 18.19.25
|
||||
resolution: "@types/node@npm:18.19.25"
|
||||
dependencies:
|
||||
undici-types: ~5.26.4
|
||||
checksum: 4cd82b81700c38464cfc8ce5a94d3e115ef9e9befe7637a89c732c4036ab7c761ec69e4d93717a7c05ab58c87cf046eaafd3e0157d5406e387bcb185d27710ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/randomcolor@npm:^0.5.8":
|
||||
version: 0.5.9
|
||||
resolution: "@types/randomcolor@npm:0.5.9"
|
||||
@@ -1576,6 +1595,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abort-controller@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "abort-controller@npm:3.0.0"
|
||||
dependencies:
|
||||
event-target-shim: ^5.0.0
|
||||
checksum: 90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn-jsx@npm:^5.3.2":
|
||||
version: 5.3.2
|
||||
resolution: "acorn-jsx@npm:5.3.2"
|
||||
@@ -1626,6 +1654,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agentkeepalive@npm:^4.2.1":
|
||||
version: 4.5.0
|
||||
resolution: "agentkeepalive@npm:4.5.0"
|
||||
dependencies:
|
||||
humanize-ms: ^1.2.1
|
||||
checksum: 394ea19f9710f230722996e156607f48fdf3a345133b0b1823244b7989426c16019a428b56c82d3eabef616e938812981d9009f4792ecc66bd6a59e991c62612
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aggregate-error@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "aggregate-error@npm:3.1.0"
|
||||
@@ -1724,6 +1761,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"asynckit@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "asynckit@npm:0.4.0"
|
||||
checksum: d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"awilix@npm:^8.0.0, awilix@npm:^8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "awilix@npm:8.0.1"
|
||||
@@ -1741,6 +1785,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base-64@npm:^0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "base-64@npm:0.1.0"
|
||||
checksum: fe0dcf076e823f04db7ee9b02495be08a91c445fbc6db03cb9913be9680e2fcc0af8b74459041fe08ad16800b1f65a549501d8f08696a8a6d32880789b7de69d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.3.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
@@ -1937,6 +1988,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"charenc@npm:0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "charenc@npm:0.0.2"
|
||||
checksum: a45ec39363a16799d0f9365c8dd0c78e711415113c6f14787a22462ef451f5013efae8a28f1c058f81fc01f2a6a16955f7a5fd0cd56247ce94a45349c89877d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^1.1.1":
|
||||
version: 1.1.4
|
||||
resolution: "chownr@npm:1.1.4"
|
||||
@@ -2030,6 +2088,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"combined-stream@npm:^1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "combined-stream@npm:1.0.8"
|
||||
dependencies:
|
||||
delayed-stream: ~1.0.0
|
||||
checksum: 0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^10.0.0":
|
||||
version: 10.0.1
|
||||
resolution: "commander@npm:10.0.1"
|
||||
@@ -2138,6 +2205,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"crypt@npm:0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "crypt@npm:0.0.2"
|
||||
checksum: adbf263441dd801665d5425f044647533f39f4612544071b1471962209d235042fb703c27eea2795c7c53e1dfc242405173003f83cf4f4761a633d11f9653f18
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
|
||||
version: 4.3.4
|
||||
resolution: "debug@npm:4.3.4"
|
||||
@@ -2186,6 +2260,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"delayed-stream@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "delayed-stream@npm:1.0.0"
|
||||
checksum: d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deprecation@npm:^2.0.0, deprecation@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "deprecation@npm:2.3.1"
|
||||
@@ -2207,6 +2288,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"digest-fetch@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "digest-fetch@npm:1.3.0"
|
||||
dependencies:
|
||||
base-64: ^0.1.0
|
||||
md5: ^2.3.0
|
||||
checksum: 0fb389e33b9c6baf5e6a9ed287aa9d0d8b373d59b49d49c62c261e1ab24eaaf1d5aea3a105c1b31ba4a23e29e129365d839ce4c5974fa004a85d1a4568bc3585
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dir-glob@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "dir-glob@npm:3.0.1"
|
||||
@@ -2227,6 +2318,7 @@ __metadata:
|
||||
dotenv: ^16.3.1
|
||||
eslint: ^8.56.0
|
||||
minimatch: ^9.0.3
|
||||
openai: ^4.29.1
|
||||
openapi-types: ^12.1.3
|
||||
pluralize: ^8.0.0
|
||||
prettier: ^3.2.4
|
||||
@@ -2585,6 +2677,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-target-shim@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "event-target-shim@npm:5.0.1"
|
||||
checksum: 0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"execa@npm:^5.0.0":
|
||||
version: 5.1.1
|
||||
resolution: "execa@npm:5.1.1"
|
||||
@@ -2751,6 +2850,34 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data-encoder@npm:1.7.2":
|
||||
version: 1.7.2
|
||||
resolution: "form-data-encoder@npm:1.7.2"
|
||||
checksum: 56553768037b6d55d9de524f97fe70555f0e415e781cb56fc457a68263de3d40fadea2304d4beef2d40b1a851269bd7854e42c362107071892cb5238debe9464
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "form-data@npm:4.0.0"
|
||||
dependencies:
|
||||
asynckit: ^0.4.0
|
||||
combined-stream: ^1.0.8
|
||||
mime-types: ^2.1.12
|
||||
checksum: cb6f3ac49180be03ff07ba3ff125f9eba2ff0b277fb33c7fc47569fc5e616882c5b1c69b9904c4c4187e97dd0419dd03b134174756f296dec62041e6527e2c6e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"formdata-node@npm:^4.3.2":
|
||||
version: 4.4.1
|
||||
resolution: "formdata-node@npm:4.4.1"
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
checksum: 74151e7b228ffb33b565cec69182694ad07cc3fdd9126a8240468bb70a8ba66e97e097072b60bcb08729b24c7ce3fd3e0bd7f1f80df6f9f662b9656786e76f6a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-constants@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "fs-constants@npm:1.0.0"
|
||||
@@ -3020,6 +3147,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"humanize-ms@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "humanize-ms@npm:1.2.1"
|
||||
dependencies:
|
||||
ms: ^2.0.0
|
||||
checksum: f34a2c20161d02303c2807badec2f3b49cbfbbb409abd4f95a07377ae01cfe6b59e3d15ac609cffcd8f2521f0eb37b7e1091acf65da99aa2a4f1ad63c21e7e7a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ieee754@npm:^1.1.13":
|
||||
version: 1.2.1
|
||||
resolution: "ieee754@npm:1.2.1"
|
||||
@@ -3089,6 +3225,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-buffer@npm:~1.1.6":
|
||||
version: 1.1.6
|
||||
resolution: "is-buffer@npm:1.1.6"
|
||||
checksum: ae18aa0b6e113d6c490ad1db5e8df9bdb57758382b313f5a22c9c61084875c6396d50bbf49315f5b1926d142d74dfb8d31b40d993a383e0a158b15fea7a82234
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-core-module@npm:^2.13.0":
|
||||
version: 2.13.1
|
||||
resolution: "is-core-module@npm:2.13.1"
|
||||
@@ -3601,6 +3744,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"md5@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "md5@npm:2.3.0"
|
||||
dependencies:
|
||||
charenc: 0.0.2
|
||||
crypt: 0.0.2
|
||||
is-buffer: ~1.1.6
|
||||
checksum: 14a21d597d92e5b738255fbe7fe379905b8cb97e0a49d44a20b58526a646ec5518c337b817ce0094ca94d3e81a3313879c4c7b510d250c282d53afbbdede9110
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"merge-stream@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "merge-stream@npm:2.0.0"
|
||||
@@ -3632,6 +3786,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-db@npm:1.52.0":
|
||||
version: 1.52.0
|
||||
resolution: "mime-db@npm:1.52.0"
|
||||
checksum: 0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-types@npm:^2.1.12":
|
||||
version: 2.1.35
|
||||
resolution: "mime-types@npm:2.1.35"
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
checksum: 82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mimic-fn@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "mimic-fn@npm:2.1.0"
|
||||
@@ -3724,7 +3894,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ms@npm:^2.1.1":
|
||||
"ms@npm:^2.0.0, ms@npm:^2.1.1":
|
||||
version: 2.1.3
|
||||
resolution: "ms@npm:2.1.3"
|
||||
checksum: d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48
|
||||
@@ -3755,6 +3925,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-domexception@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "node-domexception@npm:1.0.0"
|
||||
checksum: 5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:2.6.7":
|
||||
version: 2.6.7
|
||||
resolution: "node-fetch@npm:2.6.7"
|
||||
@@ -3875,6 +4052,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openai@npm:^4.29.1":
|
||||
version: 4.29.1
|
||||
resolution: "openai@npm:4.29.1"
|
||||
dependencies:
|
||||
"@types/node": ^18.11.18
|
||||
"@types/node-fetch": ^2.6.4
|
||||
abort-controller: ^3.0.0
|
||||
agentkeepalive: ^4.2.1
|
||||
digest-fetch: ^1.3.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: ^4.3.2
|
||||
node-fetch: ^2.6.7
|
||||
web-streams-polyfill: ^3.2.1
|
||||
bin:
|
||||
openai: bin/cli
|
||||
checksum: 7873d1c8f69d8a76ca38bd3b0aa10e967ee1a2e705a1a2eb1012dcdd1b689569e041e1bcbeb72e10fc15a43d62c0f99e01de58c8ed454e8c0b54626b58c0794f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openapi-types@npm:^12.1.3":
|
||||
version: 12.1.3
|
||||
resolution: "openapi-types@npm:12.1.3"
|
||||
@@ -5354,6 +5550,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-streams-polyfill@npm:4.0.0-beta.3":
|
||||
version: 4.0.0-beta.3
|
||||
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"
|
||||
checksum: a9596779db2766990117ed3a158e0b0e9f69b887a6d6ba0779940259e95f99dc3922e534acc3e5a117b5f5905300f527d6fbf8a9f0957faf1d8e585ce3452e8e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-streams-polyfill@npm:^3.2.1":
|
||||
version: 3.3.3
|
||||
resolution: "web-streams-polyfill@npm:3.3.3"
|
||||
checksum: 64e855c47f6c8330b5436147db1c75cb7e7474d924166800e8e2aab5eb6c76aac4981a84261dd2982b3e754490900b99791c80ae1407a9fa0dcff74f82ea3a7f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webidl-conversions@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "webidl-conversions@npm:3.0.1"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user