#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const YAML = require('yaml'); const interpolationVariableRegex = /^{{(.*?)}}$/ function findWorkspaceRoot(startDir) { let current = startDir; while (true) { if (fs.existsSync(path.join(current, 'workspace.yml'))) { return current; } const parent = path.dirname(current); if (parent === current) { throw new Error('workspace.yml not found from the provided start directory'); } current = parent; } } function parseYaml(filePath) { try { const text = fs.readFileSync(filePath, 'utf8'); const parsed = YAML.parse(text); return parsed || {}; } catch (error) { throw new Error(`Failed to parse YAML file ${filePath}: ${error.message}`); } } function readText(filePath) { return fs.readFileSync(filePath, 'utf8'); } function stripQuotes(value) { const trimmed = String(value).trim(); if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { return trimmed.slice(1, -1); } return trimmed; } function parseWorkspace(workspacePath) { const text = readText(workspacePath); const lines = text.split(/\r?\n/); const collections = []; let inCollections = false; let currentCollection = null; for (const line of lines) { const trimmed = line.trim(); if (!inCollections && trimmed === 'collections:') { inCollections = true; continue; } if (!inCollections) { continue; } if (!line.startsWith(' ') && !line.startsWith('\t') && trimmed) { break; } const nameMatch = line.match(/^\s*-\s+name:\s*(.+)$/); if (nameMatch) { currentCollection = { name: stripQuotes(nameMatch[1]) }; collections.push(currentCollection); continue; } const pathMatch = line.match(/^\s*path:\s*(.+)$/); if (pathMatch && currentCollection) { currentCollection.path = stripQuotes(pathMatch[1]); } } return { collections }; } function sanitizeVarName(value) { return String(value) .trim() .replace(/[{}]/g, '') .replace(/[^A-Za-z0-9_]/g, '_') .replace(/^([0-9])/, '_$1') || 'value'; } function collectPlaceholders(value) { if (typeof value !== 'string') { return []; } const placeholders = []; const regex = /\{\{([^{}]+)\}\}/g; let match; while ((match = regex.exec(value)) !== null) { placeholders.push(match[1]); } return placeholders; } function findBlock(lines, keyName) { for (let i = 0; i < lines.length; i += 1) { const trimmed = lines[i].trim(); if (trimmed !== keyName && !trimmed.startsWith(`${keyName}:`)) { continue; } const lineIndent = lines[i].match(/^\s*/)[0].length; const block = []; for (let j = i + 1; j < lines.length; j += 1) { const currentLine = lines[j]; const currentTrimmed = currentLine.trim(); if (!currentTrimmed) { block.push(currentLine); continue; } const currentIndent = currentLine.match(/^\s*/)[0].length; if (currentIndent <= lineIndent && !currentLine.startsWith(' ')) { break; } if (currentIndent <= lineIndent && currentTrimmed.startsWith('#')) { block.push(currentLine); continue; } if (currentIndent <= lineIndent) { break; } block.push(currentLine); } return block; } return []; } function parseRequestInfo(text) { const lines = text.split(/\r?\n/); const infoLines = findBlock(lines, 'info'); const nameMatch = infoLines.join('\n').match(/^\s*name:\s*(.+)$/m); return nameMatch ? stripQuotes(nameMatch[1]) : ''; } function parseHttpBlock(text) { const lines = text.split(/\r?\n/); const blockLines = findBlock(lines, 'http'); const blockText = blockLines.join('\n'); const methodMatch = blockText.match(/^\s*method:\s*(.+)$/m); const urlMatch = blockText.match(/^\s*url:\s*(.+)$/m); const params = []; const paramLines = blockLines.join('\n').split(/\r?\n/); let inParamsBlock = false; let paramsIndent = 0; let currentParam = null; for (const line of paramLines) { const trimmed = line.trim(); if (!inParamsBlock) { if (trimmed === 'params:') { inParamsBlock = true; paramsIndent = line.match(/^\s*/)[0].length; } continue; } if (!trimmed) { continue; } const indent = line.match(/^\s*/)[0].length; if (indent <= paramsIndent) { break; } const listItemMatch = line.match(/^\s*-\s+name:\s*(.+)$/); if (listItemMatch) { currentParam = { name: stripQuotes(listItemMatch[1]) }; params.push(currentParam); continue; } if (!currentParam) { continue; } const propertyMatch = line.match(/^\s*([A-Za-z0-9_]+):\s*(.+)$/); if (propertyMatch) { const [, key, value] = propertyMatch; currentParam[key] = stripQuotes(value); } } return { method: methodMatch ? stripQuotes(methodMatch[1]) : 'GET', url: urlMatch ? stripQuotes(urlMatch[1]) : '', params, }; } function formatVariableValue(value) { if (value === null || value === undefined) { return '""'; } if (typeof value === 'string') { if (value.trim() === '') { return '""'; } if (/\s/.test(value)) { return `"${value.replace(/"/g, '\\"')}"`; } return value; } return JSON.stringify(value); } function mergeRequestConfig(base, updates) { if (!updates || typeof updates !== 'object') { return base; } const merged = { ...(base || {}) }; if (updates.auth) { if (updates.auth === 'inherit' && merged.auth && typeof merged.auth === 'object') { merged.auth = merged.auth; } else if (typeof updates.auth === 'object') { merged.auth = updates.auth; } } if (Array.isArray(updates.variables)) { const variables = [...(Array.isArray(base.variables) ? base.variables : [])]; const byName = new Map(); for (const variable of variables) { if (variable && variable.name) { byName.set(String(variable.name), variable); } } for (const variable of updates.variables) { if (variable && variable.name) { byName.set(String(variable.name), variable); } } merged.variables = [...byName.values()]; } return merged; } function getRequestConfigForFile(yamlFile, sourceDir) { const resolved = []; const seenFiles = new Set(); const addFile = (filePath) => { if (!filePath || seenFiles.has(filePath)) { return; } seenFiles.add(filePath); if (!fs.existsSync(filePath)) { return; } try { const parsed = parseYaml(filePath); if (parsed && parsed.request && typeof parsed.request === 'object') { resolved.push(parsed.request); } } catch (error) { // Ignore files that cannot be parsed as YAML for request inheritance. } }; addFile(yamlFile); const dirChain = []; let currentDir = path.dirname(yamlFile); while (true) { dirChain.unshift(currentDir); if (currentDir === sourceDir) { break; } const parentDir = path.dirname(currentDir); if (parentDir === currentDir) { break; } currentDir = parentDir; } for (const dir of dirChain) { addFile(path.join(dir, 'opencollection.yml')); addFile(path.join(dir, 'folder.yml')); } return resolved.reduce((result, config) => mergeRequestConfig(result, config), {}); } function buildRequestContent(request, requestName, requestConfig = {}) { const lines = []; const variableDefinitions = []; const seenVariables = new Set(); const addVariable = (name, value) => { if (!name) { return; } const normalized = String(name).trim(); if (!normalized || seenVariables.has(normalized)) { return; } seenVariables.add(normalized); variableDefinitions.push({ name: normalized, value }); }; const configVariables = Array.isArray(requestConfig.variables) ? requestConfig.variables : []; for (const variable of configVariables) { if (variable && variable.name) { addVariable(variable.name, variable.value); } } let url = request.url || ''; if (url) { for (const placeholder of collectPlaceholders(url)) { addVariable(placeholder, 'YOUR_VALUE_HERE'); } url = url.replace(/:([A-Za-z0-9_]+)/g, (_, name) => `{{${name}}}`); } const queryParams = []; const headers = []; for (const header of Array.isArray(requestConfig.headers) ? requestConfig.headers : []) { if (!header || !header.name) { continue; } for (const placeholder of collectPlaceholders(String(header.value ?? ''))) { addVariable(placeholder, 'YOUR_VALUE_HERE'); } headers.push({ name: header.name, value: header.value ?? '' }); } for (const param of request.params || []) { const name = param.name || ''; const value = param.value || ''; const type = (param.type || 'query').toLowerCase(); const disabled = String(param.disabled).toLowerCase() === 'true'; if (disabled) { continue; } for (const placeholder of collectPlaceholders(String(value))) { addVariable(placeholder, 'YOUR_VALUE_HERE'); } if (type === 'header') { headers.push({ name, value }); } else { queryParams.push({ name, value }); } } if (requestConfig.auth) { if (requestConfig.auth.type === 'bearer') { for (const placeholder of collectPlaceholders(String(requestConfig.auth.token ?? ''))) { addVariable(placeholder, 'YOUR_VALUE_HERE'); } headers.push({ name: 'Authorization', value: `Bearer ${requestConfig.auth.token ?? ''}`, }); } else if (requestConfig.auth.type === 'basic') { const username = requestConfig.auth.username ?? ''; const password = requestConfig.auth.password ?? ''; for (const placeholder of collectPlaceholders(String(username))) { addVariable(placeholder, 'YOUR_VALUE_HERE'); } for (const placeholder of collectPlaceholders(String(password))) { addVariable(placeholder, 'YOUR_VALUE_HERE'); } // VSCode REST Client can manage username:password format directly! headers.push({ name: 'Authorization', value: `Basic ${username}:${password}`, }); } else { headers.push({ name: `UNKNOWN_${requestConfig.auth.type}`, value: `Basic ${requestConfig.auth.token}`, }); } } if (variableDefinitions.length > 0) { lines.push(`# Variables for ${requestName}`); for (const variable of variableDefinitions) { lines.push(`@${sanitizeVarName(variable.name)} = ${formatVariableValue(variable.value)}`); } lines.push(''); } const method = (request.method || 'GET').toUpperCase(); let requestUrl = url; for (const param of queryParams) { if (!param.name) { continue; } const separator = requestUrl.includes('?') ? '&' : '?'; requestUrl = `${requestUrl}${separator}${param.name}=${param.value}`; } lines.push(`${method} ${requestUrl}`); for (const header of headers) { lines.push(`${header.name}: ${header.value}`); } return lines.join('\n'); } function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } function walkYamlFiles(rootDir) { const results = []; const entries = fs.readdirSync(rootDir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.') || entry.name === 'node_modules') { continue; } const fullPath = path.join(rootDir, entry.name); if (entry.isDirectory()) { results.push(...walkYamlFiles(fullPath)); } else if (entry.isFile() && /\.ya?ml$/i.test(entry.name)) { results.push(fullPath); } } return results; } function writeEnvironmentTemplates(sourceDir, outputRoot) { const targets = []; const visit = (currentDir) => { const entries = fs.readdirSync(currentDir, { withFileTypes: true }); const hasEnvironmentsDir = entries.some((entry) => entry.isDirectory() && entry.name === 'environments'); if (hasEnvironmentsDir) { targets.push(currentDir); } for (const entry of entries) { if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') { continue; } visit(path.join(currentDir, entry.name)); } }; visit(sourceDir); for (const dir of targets) { const relativeDir = path.relative(sourceDir, dir); const targetDir = relativeDir && relativeDir !== '.' ? path.join(outputRoot, relativeDir) : outputRoot; ensureDir(targetDir); const environmentsDir = path.join(dir, 'environments'); if (!fs.existsSync(environmentsDir)) { continue; } const envFiles = fs.readdirSync(environmentsDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name)) .map((entry) => path.join(environmentsDir, entry.name)); const variableNames = []; const seenNames = new Set(); for (const envFile of envFiles) { const parsed = parseYaml(envFile); const variables = Array.isArray(parsed.variables) ? parsed.variables : []; for (const variable of variables) { if (!variable || !variable.name) { continue; } const name = String(variable.name).trim(); if (!name || seenNames.has(name)) { continue; } seenNames.add(name); variableNames.push(name); } } const templateContent = variableNames.length > 0 ? `${variableNames.map(v => `${v}={{${v}}}`).join('\n')}\n` : ''; fs.writeFileSync(path.join(targetDir, '.env.template'), templateContent, 'utf8'); } } function main() { const workspaceRoot = findWorkspaceRoot(__dirname); const workspaceFile = path.join(workspaceRoot, 'workspace.yml'); const workspace = parseWorkspace(workspaceFile); const collections = Array.isArray(workspace.collections) ? workspace.collections : []; if (collections.length === 0) { throw new Error('No collections found in workspace.yml'); } for (const collection of collections) { if (!collection || !collection.name || !collection.path) { continue; } const sourceDir = path.join(workspaceRoot, collection.path); if (!fs.existsSync(sourceDir)) { console.warn(`Skipping missing collection path: ${collection.path}`); continue; } const outputRoot = path.join(workspaceRoot, 'autodocs', 'http', collection.name); ensureDir(outputRoot); writeEnvironmentTemplates(sourceDir, outputRoot); const yamlFiles = walkYamlFiles(sourceDir); let processed = 0; for (const yamlFile of yamlFiles) { const content = readText(yamlFile); const requestName = parseRequestInfo(content) || path.basename(yamlFile, path.extname(yamlFile)); const httpBlock = parseHttpBlock(content); if (!httpBlock || !httpBlock.url) { continue; } const relativePath = path.relative(sourceDir, yamlFile); const parsedPath = path.parse(relativePath); const targetDir = path.join(outputRoot, parsedPath.dir); ensureDir(targetDir); const requestConfig = getRequestConfigForFile(yamlFile, sourceDir); const outputFile = path.join(targetDir, `${parsedPath.name}.http`); const requestContent = buildRequestContent(httpBlock, requestName, requestConfig); fs.writeFileSync(outputFile, `${requestContent}\n`, 'utf8'); processed += 1; } console.log(`Generated ${processed} .http file(s) for ${collection.name}`); } } try { main(); } catch (error) { console.error(error.message); process.exit(1); }