diff --git a/README.MD b/README.MD index 8b099b7..bd24f41 100644 --- a/README.MD +++ b/README.MD @@ -1,6 +1,6 @@ # JSDoc Builder -JSDoc Builder is a CLI tool for automatically generating JSDoc comments for JavaScript and TypeScript files. It parses functions and variables to infer parameter types, return types, and descriptions, and then inserts JSDoc comments directly into the source code. +JSDoc Builder is a CLI tool for automatically generating JSDoc comments for JavaScript, TypeScript, JSX, TSX, and Vue files. It parses functions and variables to infer parameter types, return types, and descriptions, and then inserts JSDoc comments directly into the source code. ## Features @@ -8,8 +8,16 @@ JSDoc Builder is a CLI tool for automatically generating JSDoc comments for Java - Function declarations - Arrow functions - TypeScript types and interfaces +- Supports multiple file formats: + - JavaScript (`.js`) + - TypeScript (`.ts`) + - JSX (`.jsx`) + - TSX (`.tsx`) + - Vue Single File Components (`.vue`) - Infers parameter and return types from TypeScript annotations. +- **AI-Powered Descriptions**: Use OpenAI to generate meaningful function descriptions automatically. - Outputs clean and structured JSDoc comments. +- Preserves Vue SFC structure (template, script, and style sections). ## Installation @@ -27,7 +35,7 @@ npm install jsdoc-builder --save-dev ## Usage -### CLI Command +### Basic Usage (Without AI) Run the following command to generate JSDoc comments for a file: @@ -35,7 +43,34 @@ Run the following command to generate JSDoc comments for a file: jsdoc-builder ``` -Replace `` with the path to the JavaScript or TypeScript file you want to process. +Replace `` with the path to the JavaScript, TypeScript, JSX, TSX, or Vue file you want to process. + +### AI-Powered Usage + +To enable AI-powered description generation using OpenAI: + +```bash +jsdoc-builder --ai --api-key YOUR_OPENAI_API_KEY +``` + +Or set the API key as an environment variable: + +```bash +export OPENAI_API_KEY=your_api_key_here +jsdoc-builder --ai +``` + +You can also specify a different model: + +```bash +jsdoc-builder --ai --model gpt-4 +``` + +### CLI Options + +- `--ai` - Enable AI-powered description generation +- `--api-key ` - OpenAI API key (can also be set via `OPENAI_API_KEY` environment variable) +- `--model ` - AI model to use (default: `gpt-3.5-turbo`) ### Example @@ -51,7 +86,7 @@ const multiply = (a: number, b: number): number => { }; ``` -#### Command: +#### Basic Command (Without AI): ```bash jsdoc-builder example.ts @@ -81,6 +116,136 @@ const multiply = (a: number, b: number): number => { }; ``` +#### AI-Powered Command: + +```bash +export OPENAI_API_KEY=your_api_key_here +jsdoc-builder example.ts --ai +``` + +#### AI-Generated Output: + +The AI will generate contextual descriptions based on the function code, parameter names, and types: + +```typescript +/** + * @description Adds two numbers and returns their sum + * @param {number} a + * @param {number} b + * @returns {number} + */ +function add(a: number, b: number) { + return a + b; +} + +/** + * @description Multiplies two numbers and returns the product + * @param {number} a + * @param {number} b + * @returns {number} + */ +const multiply = (a: number, b: number): number => { + return a * b; +}; +``` + +### JSX Example + +#### Input File (`example.jsx`): + +```jsx +function Component(props) { + return
{props.title}
; +} + +const ArrowComponent = (props) => { + return
{props.content}
; +}; +``` + +#### Command: + +```bash +jsdoc-builder example.jsx +``` + +#### Output File (`example.jsx`): + +```jsx +/** + * @description Press Your { Function Component } Description + * @param {any} props + * @returns {void} + */ +function Component(props) { + return
{props.title}
; +} + +/** + * @description Press Your { Function ArrowComponent } Description + * @param {any} props + * @returns {void} + */ +const ArrowComponent = (props) => { + return
{props.content}
; +}; +``` + +### Vue Example + +#### Input File (`example.vue`): + +```vue + + + +``` + +#### Command: + +```bash +jsdoc-builder example.vue +``` + +#### Output File (`example.vue`): + +```vue + + + +``` + ## API ### `generateJSDoc(filePath: string): void` diff --git a/example/example.jsx b/example/example.jsx new file mode 100644 index 0000000..424f3a6 --- /dev/null +++ b/example/example.jsx @@ -0,0 +1,17 @@ +/** +* @description Press Your { Function Component } Description +* @param {any} props +* @returns {void} +*/ +function Component(props) { + return
{props.title}
; +} +/** +* @description Press Your { Function ArrowComponent } Description +* @param {any} props +* @returns {void} +*/ +const ArrowComponent = (props) => { + return
{props.content}
; +}; +export default Component; diff --git a/example/example.tsx b/example/example.tsx new file mode 100644 index 0000000..3148d95 --- /dev/null +++ b/example/example.tsx @@ -0,0 +1,21 @@ +interface Props { + title: string; + count?: number; +} +/** +* @description Press Your { Function Component } Description +* @param {Props} props +* @returns {void} +*/ +function Component(props: Props) { + return
{props.title}
; +} +/** +* @description Press Your { Function ArrowComponent } Description +* @param {Props} props +* @returns {JSX.Element} +*/ +const ArrowComponent = (props: Props): JSX.Element => { + return
{props.title}
; +}; +export default Component; diff --git a/example/example.vue b/example/example.vue new file mode 100644 index 0000000..14502ca --- /dev/null +++ b/example/example.vue @@ -0,0 +1,36 @@ + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 68aefef..cbc427b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "commander": "^10.0.0", + "openai": "^6.9.1", "typescript": "^5.0.0" }, "bin": { @@ -149,6 +150,27 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/openai": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.9.1.tgz", + "integrity": "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/package.json b/package.json index e5bc5ea..71f81dc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "jsdoc-builder", "version": "0.0.6", "author": "dori", - "description": "Generate JSDoc comments for JavaScript and TypeScript files.", + "description": "Generate JSDoc comments for JavaScript, TypeScript, JSX, TSX, and Vue files.", "publishConfig": { "access": "public" }, @@ -29,6 +29,11 @@ "typescript-docs", "typescript-jsdoc", "javascript-documentation", + "jsx-documentation", + "tsx-documentation", + "vue-documentation", + "react-jsdoc", + "vue-jsdoc", "cli-tool", "developer-tools" ], @@ -39,6 +44,7 @@ }, "dependencies": { "commander": "^10.0.0", + "openai": "^6.9.1", "typescript": "^5.0.0" }, "devDependencies": { diff --git a/src/ai-service.ts b/src/ai-service.ts new file mode 100644 index 0000000..d09e18b --- /dev/null +++ b/src/ai-service.ts @@ -0,0 +1,110 @@ +import OpenAI from "openai"; + +export interface AIConfig { + apiKey?: string; + model?: string; + enabled: boolean; +} + +export class AIService { + private client: OpenAI | null = null; + private model: string; + + constructor(config: AIConfig) { + if (config.enabled && config.apiKey) { + this.client = new OpenAI({ + apiKey: config.apiKey, + }); + this.model = config.model || "gpt-3.5-turbo"; + } else { + this.client = null; + this.model = "gpt-3.5-turbo"; + } + } + + /** + * Generates a description for a function using AI + * @param functionName - The name of the function + * @param parameters - Array of parameter information + * @param returnType - The return type of the function + * @param functionCode - The actual function code for context + * @returns A meaningful description or a fallback description + */ + async generateDescription( + functionName: string, + parameters: { name: string; type: string }[], + returnType: string, + functionCode: string + ): Promise { + if (!this.client) { + return `Press Your { Function ${functionName} } Description`; + } + + try { + const prompt = this.buildPrompt( + functionName, + parameters, + returnType, + functionCode + ); + + const response = await this.client.chat.completions.create({ + model: this.model, + messages: [ + { + role: "system", + content: + "You are a helpful assistant that generates concise JSDoc descriptions for functions. Provide only the description text without any markdown formatting, code blocks, or JSDoc syntax. Keep it brief and professional.", + }, + { + role: "user", + content: prompt, + }, + ], + temperature: 0.7, + max_tokens: 150, + }); + + const description = response.choices[0]?.message?.content?.trim(); + return description || `Press Your { Function ${functionName} } Description`; + } catch (error) { + console.warn( + `Warning: Failed to generate AI description for ${functionName}:`, + error instanceof Error ? error.message : "Unknown error" + ); + return `Press Your { Function ${functionName} } Description`; + } + } + + private buildPrompt( + functionName: string, + parameters: { name: string; type: string }[], + returnType: string, + functionCode: string + ): string { + const paramList = + parameters.length > 0 + ? parameters + .map((p) => `${p.name}: ${p.type}`) + .join(", ") + : "none"; + + return `Generate a concise JSDoc description for the following function: + +Function Name: ${functionName} +Parameters: ${paramList} +Return Type: ${returnType} + +Function Code: +${functionCode} + +Provide only the description text (one or two sentences) without any additional formatting.`; + } + + /** + * Checks if AI is enabled and configured + */ + isEnabled(): boolean { + return this.client !== null; + } +} diff --git a/src/cli.ts b/src/cli.ts index 07507bd..dd9e635 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,12 +3,36 @@ import { program } from "commander"; import { generateJSDoc } from "./index"; program - .argument("", "The TypeScript or JavaScript file to process") + .argument("", "The TypeScript, JavaScript, JSX, TSX, or Vue file to process") .description( - "Generate JSDoc comments for the given TypeScript or JavaScript file" + "Generate JSDoc comments for the given TypeScript, JavaScript, JSX, TSX, or Vue file" ) - .action((file) => { - generateJSDoc(file); + .option("--ai", "Enable AI-powered description generation") + .option( + "--api-key ", + "OpenAI API key (can also be set via OPENAI_API_KEY environment variable)" + ) + .option( + "--model ", + "AI model to use (default: gpt-3.5-turbo)", + "gpt-3.5-turbo" + ) + .action(async (file, options) => { + const apiKey = options.apiKey || process.env.OPENAI_API_KEY; + const aiEnabled = options.ai && !!apiKey; + + if (options.ai && !apiKey) { + console.error( + "Error: AI is enabled but no API key provided. Set OPENAI_API_KEY environment variable or use --api-key option." + ); + process.exit(1); + } + + await generateJSDoc(file, { + aiEnabled, + apiKey, + model: options.model, + }); }); program.parse(process.argv); diff --git a/src/index.ts b/src/index.ts index e18677d..7f5c968 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,71 +1,175 @@ import * as ts from "typescript"; import * as fs from "fs"; +import * as path from "path"; +import { AIService, AIConfig } from "./ai-service"; + +export interface GenerateJSDocOptions { + aiEnabled?: boolean; + apiKey?: string; + model?: string; +} /** - * Parses a TypeScript or JavaScript file and adds JSDoc comments to functions. + * Parses a TypeScript, JavaScript, JSX, TSX, or Vue file and adds JSDoc comments to functions. * Skips functions that already have JSDoc comments. * @param filePath - The path of the file to process. + * @param options - Options for AI-powered generation */ -export function generateJSDoc(filePath: string): void { - const sourceCode = fs.readFileSync(filePath, "utf-8"); +export async function generateJSDoc( + filePath: string, + options: GenerateJSDocOptions = {} +): Promise { + const aiConfig: AIConfig = { + enabled: options.aiEnabled || false, + apiKey: options.apiKey, + model: options.model, + }; + + const aiService = new AIService(aiConfig); + + if (aiService.isEnabled()) { + console.log( + `AI-powered description generation enabled using model: ${ + options.model || "gpt-3.5-turbo" + }` + ); + } + + const ext = path.extname(filePath).toLowerCase(); + let sourceCode = fs.readFileSync(filePath, "utf-8"); + let isVueFile = false; + let vueTemplate = ""; + let vueScriptContent = ""; + let vueStyle = ""; + let scriptStartPos = 0; + + // Handle Vue SFC (Single File Component) + if (ext === ".vue") { + isVueFile = true; + const scriptMatch = sourceCode.match(/]*>([\s\S]*?)<\/script[^>]*>/i); + const templateMatch = sourceCode.match(/]*>([\s\S]*?)<\/template[^>]*>/i); + const styleMatch = sourceCode.match(/]*>([\s\S]*?)<\/style[^>]*>/i); + + if (scriptMatch) { + vueScriptContent = scriptMatch[1]; + scriptStartPos = scriptMatch.index! + scriptMatch[0].indexOf(scriptMatch[1]); + } + if (templateMatch) { + vueTemplate = templateMatch[0]; + } + if (styleMatch) { + vueStyle = styleMatch[0]; + } + + sourceCode = vueScriptContent || ""; + } + + // Determine the appropriate ScriptKind based on file extension + let scriptKind = ts.ScriptKind.TS; + if (ext === ".js" || ext === ".mjs" || ext === ".cjs") { + scriptKind = ts.ScriptKind.JS; + } else if (ext === ".jsx") { + scriptKind = ts.ScriptKind.JSX; + } else if (ext === ".tsx") { + scriptKind = ts.ScriptKind.TSX; + } else if (ext === ".vue") { + // For Vue files, treat the script section as JS + scriptKind = ts.ScriptKind.JS; + } + const sourceFile = ts.createSourceFile( filePath, sourceCode, ts.ScriptTarget.Latest, - true + true, + scriptKind ); - const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + // Collect all positions and JSDoc comments to insert + const insertions: { pos: number; comment: string }[] = []; - function visit(node: ts.Node, context: ts.TransformationContext): ts.Node { + async function processNodes(node: ts.Node): Promise { if (ts.isFunctionDeclaration(node) && node.name && !hasJSDoc(node)) { - const jsDoc = createJSDoc(node.name.text, node.parameters, node.type); - ts.addSyntheticLeadingComment( - node, - ts.SyntaxKind.MultiLineCommentTrivia, - jsDoc.comment, - true + const functionCode = node.getText(sourceFile); + const jsDoc = await createJSDoc( + node.name.text, + node.parameters, + node.type, + functionCode, + aiService ); - return node; + + // Get the actual start position (excluding leading trivia) + const pos = node.getStart(sourceFile); + insertions.push({ pos, comment: jsDoc.fullComment }); } if ( ts.isVariableDeclaration(node) && node.initializer && ts.isArrowFunction(node.initializer) && - !hasJSDoc(node.parent.parent) // VariableStatement에 JSDoc이 있는지 확인 + !hasJSDoc(node.parent.parent) ) { - const jsDoc = createJSDoc( + const functionCode = node.initializer.getText(sourceFile); + const jsDoc = await createJSDoc( node.name.getText(), node.initializer.parameters, - node.initializer.type + node.initializer.type, + functionCode, + aiService ); - ts.addSyntheticLeadingComment( - node.parent.parent, - ts.SyntaxKind.MultiLineCommentTrivia, - jsDoc.comment, - true - ); - return node; + // Get the actual start position of the variable statement + const pos = node.parent.parent.getStart(sourceFile); + insertions.push({ pos, comment: jsDoc.fullComment }); } - return ts.visitEachChild(node, (child) => visit(child, context), context); + // Recursively process children + const children = node.getChildren(sourceFile); + for (const child of children) { + await processNodes(child); + } } - const transformer: ts.TransformerFactory = (context) => { - return (sourceFile) => { - return ts.visitNode(sourceFile, (node) => - visit(node, context) - ) as ts.SourceFile; - }; - }; + await processNodes(sourceFile); + + // Sort insertions by position in reverse order (so we can insert without affecting positions) + insertions.sort((a, b) => b.pos - a.pos); - const result = ts.transform(sourceFile, [transformer]); - const transformedSourceFile = result.transformed[0] as ts.SourceFile; - const updatedCode = printer.printFile(transformedSourceFile); + let updatedCode = sourceCode; + for (const insertion of insertions) { + updatedCode = + updatedCode.substring(0, insertion.pos) + + insertion.comment + + updatedCode.substring(insertion.pos); + } + + // For Vue files, reconstruct the SFC structure + if (isVueFile) { + const originalFileContent = fs.readFileSync(filePath, "utf-8"); + const scriptMatch = originalFileContent.match(/]*>([\s\S]*?)<\/script[^>]*>/i); + + if (scriptMatch) { + const scriptTag = scriptMatch[0]; + const scriptOpenTag = scriptTag.substring(0, scriptTag.indexOf(scriptMatch[1])); + const scriptCloseTag = ""; + + // Reconstruct the Vue file + const parts = []; + if (vueTemplate) { + parts.push(vueTemplate); + } + parts.push(`${scriptOpenTag}${updatedCode}${scriptCloseTag}`); + if (vueStyle) { + parts.push(vueStyle); + } + + updatedCode = parts.join("\n\n"); + } + } fs.writeFileSync(filePath, updatedCode, "utf-8"); + console.log(`JSDoc comments have been added to ${filePath}`); } /** @@ -82,13 +186,17 @@ function hasJSDoc(node: ts.Node): boolean { * @param functionName - The name of the function. * @param parameters - The parameters of the function. * @param returnType - The return type of the function. + * @param functionCode - The function code for AI context. + * @param aiService - The AI service instance. * @returns A JSDoc comment as a string. */ -function createJSDoc( +async function createJSDoc( functionName: string, parameters: ts.NodeArray, - returnType?: ts.TypeNode -): { comment: string } { + returnType: ts.TypeNode | undefined, + functionCode: string, + aiService: AIService +): Promise<{ comment: string; fullComment: string }> { const paramTags = parameters.map((param) => ({ tagName: "param", name: param.name.getText(), @@ -100,11 +208,27 @@ function createJSDoc( type: returnType ? returnType.getText() : "void", }; + let description: string; + + if (aiService.isEnabled()) { + description = await aiService.generateDescription( + functionName, + paramTags.map((p) => ({ name: p.name, type: p.type })), + returnTag.type, + functionCode + ); + } else { + description = `Press Your { Function ${functionName} } Description`; + } + const commentText = [ - `* @description Press Your { Function ${functionName} } Description`, - ...paramTags.map((tag) => `* @param {${tag.type}} ${tag.name}`), - `* @returns {${returnTag.type}}`, + ` * @description ${description}`, + ...paramTags.map((tag) => ` * @param {${tag.type}} ${tag.name}`), + ` * @returns {${returnTag.type}}`, ].join("\n"); - return { comment: `*\n${commentText}\n` }; + const fullComment = `/**\n${commentText}\n */\n`; + + return { comment: `*\n${commentText}\n`, fullComment }; } +