Files
Claude-Code-Workflow/.claude/skills/software-manual/scripts/swagger-runner.md
catlog22 7aa1cda367 feat: add issue discovery view for managing discovery sessions and findings
- Implemented main render function for the issue discovery view.
- Added data loading functions to fetch discoveries, details, findings, and progress.
- Created rendering functions for discovery list and detail sections.
- Introduced filtering and searching capabilities for findings.
- Implemented actions for exporting and dismissing findings.
- Added polling mechanism to track discovery progress.
- Included utility functions for HTML escaping and cleanup.
2025-12-28 17:21:07 +08:00

9.0 KiB

Swagger/OpenAPI Runner

Guide for generating backend API documentation from OpenAPI/Swagger specifications.

Overview

This script extracts and converts OpenAPI/Swagger specifications to Markdown format for inclusion in the software manual.

Detection Strategy

Check for Existing Specification

async function detectOpenAPISpec() {
  // Check for existing spec files
  const specPatterns = [
    'openapi.json',
    'openapi.yaml',
    'openapi.yml',
    'swagger.json',
    'swagger.yaml',
    'swagger.yml',
    '**/openapi*.json',
    '**/swagger*.json'
  ];

  for (const pattern of specPatterns) {
    const files = Glob(pattern);
    if (files.length > 0) {
      return { found: true, type: 'file', path: files[0] };
    }
  }

  // Check for swagger-jsdoc in dependencies
  const packageJson = JSON.parse(Read('package.json'));
  if (packageJson.dependencies?.['swagger-jsdoc'] ||
      packageJson.devDependencies?.['swagger-jsdoc']) {
    return { found: true, type: 'jsdoc' };
  }

  // Check for NestJS Swagger
  if (packageJson.dependencies?.['@nestjs/swagger']) {
    return { found: true, type: 'nestjs' };
  }

  // Check for runtime endpoint
  return { found: false, suggestion: 'runtime' };
}

Extraction Methods

Method A: From Existing Spec File

async function extractFromFile(specPath, workDir) {
  const outputDir = `${workDir}/api-docs/backend`;
  Bash({ command: `mkdir -p "${outputDir}"` });

  // Copy spec to output
  Bash({ command: `cp "${specPath}" "${outputDir}/openapi.json"` });

  // Convert to Markdown using widdershins
  const result = Bash({
    command: `npx widdershins "${specPath}" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'python:Python' 'bash:cURL'`,
    timeout: 60000
  });

  return { success: result.exitCode === 0, outputDir };
}

Method B: From swagger-jsdoc

async function extractFromJsDoc(workDir) {
  const outputDir = `${workDir}/api-docs/backend`;

  // Look for swagger definition file
  const defFiles = Glob('**/swagger*.js').concat(Glob('**/openapi*.js'));
  if (defFiles.length === 0) {
    return { success: false, error: 'No swagger definition found' };
  }

  // Generate spec
  const result = Bash({
    command: `npx swagger-jsdoc -d "${defFiles[0]}" -o "${outputDir}/openapi.json"`,
    timeout: 60000
  });

  if (result.exitCode !== 0) {
    return { success: false, error: result.stderr };
  }

  // Convert to Markdown
  Bash({
    command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'bash:cURL'`
  });

  return { success: true, outputDir };
}

Method C: From NestJS Swagger

async function extractFromNestJS(workDir) {
  const outputDir = `${workDir}/api-docs/backend`;

  // NestJS typically exposes /api-docs-json at runtime
  // We need to start the server temporarily

  // Start server in background
  const server = Bash({
    command: 'npm run start:dev',
    run_in_background: true,
    timeout: 30000
  });

  // Wait for server to be ready
  await waitForServer('http://localhost:3000', 30000);

  // Fetch OpenAPI spec
  const spec = await fetch('http://localhost:3000/api-docs-json');
  const specJson = await spec.json();

  // Save spec
  Write(`${outputDir}/openapi.json`, JSON.stringify(specJson, null, 2));

  // Stop server
  KillShell({ shell_id: server.task_id });

  // Convert to Markdown
  Bash({
    command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'bash:cURL'`
  });

  return { success: true, outputDir };
}

Method D: From Runtime Endpoint

async function extractFromRuntime(workDir, serverUrl = 'http://localhost:3000') {
  const outputDir = `${workDir}/api-docs/backend`;

  // Common OpenAPI endpoint paths
  const endpointPaths = [
    '/api-docs-json',
    '/swagger.json',
    '/openapi.json',
    '/docs/json',
    '/api/v1/docs.json'
  ];

  let specJson = null;

  for (const path of endpointPaths) {
    try {
      const response = await fetch(`${serverUrl}${path}`);
      if (response.ok) {
        specJson = await response.json();
        break;
      }
    } catch (e) {
      continue;
    }
  }

  if (!specJson) {
    return { success: false, error: 'Could not fetch OpenAPI spec from server' };
  }

  // Save and convert
  Write(`${outputDir}/openapi.json`, JSON.stringify(specJson, null, 2));

  Bash({
    command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md"`
  });

  return { success: true, outputDir };
}

Installation

Required Tools

# For OpenAPI to Markdown conversion
npm install -g widdershins

# Or as dev dependency
npm install --save-dev widdershins

# For generating from JSDoc comments
npm install --save-dev swagger-jsdoc

Configuration

widdershins Options

npx widdershins openapi.json \
  -o api-reference.md \
  --language_tabs 'javascript:JavaScript' 'python:Python' 'bash:cURL' \
  --summary \
  --omitHeader \
  --resolve \
  --expandBody
Option Description
--language_tabs Code example languages
--summary Use summary as operation heading
--omitHeader Don't include title header
--resolve Resolve $ref references
--expandBody Show full request body

swagger-jsdoc Definition

Example swagger-def.js:

module.exports = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'MyApp API',
      version: '1.0.0',
      description: 'API documentation for MyApp'
    },
    servers: [
      { url: 'http://localhost:3000/api/v1' }
    ]
  },
  apis: ['./src/routes/*.js', './src/controllers/*.js']
};

Output Format

Generated Markdown Structure

# MyApp API

## Overview

Base URL: `http://localhost:3000/api/v1`

## Authentication

This API uses Bearer token authentication.

---

## Projects

### List Projects

`GET /projects`

Returns a list of all projects.

**Parameters**

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| status | query | string | false | Filter by status |
| page | query | integer | false | Page number |

**Responses**

| Status | Description |
|--------|-------------|
| 200 | Successful response |
| 401 | Unauthorized |

**Example Request**

```javascript
fetch('/api/v1/projects?status=active')
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "data": [
    { "id": "1", "name": "Project 1" }
  ],
  "pagination": {
    "page": 1,
    "total": 10
  }
}

## Integration

### Main Runner

```javascript
async function runSwaggerExtraction(workDir) {
  const detection = await detectOpenAPISpec();

  if (!detection.found) {
    console.log('No OpenAPI spec detected. Skipping backend API docs.');
    return { success: false, skipped: true };
  }

  let result;

  switch (detection.type) {
    case 'file':
      result = await extractFromFile(detection.path, workDir);
      break;
    case 'jsdoc':
      result = await extractFromJsDoc(workDir);
      break;
    case 'nestjs':
      result = await extractFromNestJS(workDir);
      break;
    default:
      result = await extractFromRuntime(workDir);
  }

  if (result.success) {
    // Post-process the Markdown
    await postProcessApiDocs(result.outputDir);
  }

  return result;
}

async function postProcessApiDocs(outputDir) {
  const mdFile = `${outputDir}/api-reference.md`;
  let content = Read(mdFile);

  // Remove widdershins header
  content = content.replace(/^---[\s\S]*?---\n/, '');

  // Add custom styling hints
  content = content.replace(/^(#{1,3} .+)$/gm, '$1\n');

  Write(mdFile, content);
}

Troubleshooting

Common Issues

"widdershins: command not found"

npm install -g widdershins
# Or use npx
npx widdershins openapi.json -o api.md

"Error parsing OpenAPI spec"

# Validate spec first
npx @redocly/cli lint openapi.json

# Fix common issues
npx @redocly/cli bundle openapi.json -o fixed.json

"Server not responding"

Ensure the development server is running and accessible:

# Check if server is running
curl http://localhost:3000/health

# Check OpenAPI endpoint
curl http://localhost:3000/api-docs-json

Manual Fallback

If automatic extraction fails, document APIs manually:

  1. List all route files: Glob('**/routes/*.js')
  2. Extract route definitions using regex
  3. Build documentation structure manually
async function manualApiExtraction(workDir) {
  const routeFiles = Glob('src/routes/*.js').concat(Glob('src/routes/*.ts'));
  const endpoints = [];

  for (const file of routeFiles) {
    const content = Read(file);
    const routes = content.matchAll(/router\.(get|post|put|delete|patch)\(['"]([^'"]+)['"]/g);

    for (const match of routes) {
      endpoints.push({
        method: match[1].toUpperCase(),
        path: match[2],
        file: file
      });
    }
  }

  return endpoints;
}