#!/usr/bin/env node import fs from 'fs'; import path from 'path'; import YAML from 'yaml'; import stripJsonComments from 'strip-json-comments'; import { fileURLToPath } from 'url'; const interpolationVariableRegex = /^{{(.*?)}}$/ const DEFAULT_ENV_VAR_VALUE = 'EDIT_VALUE_HERE' const VARIABLE_NAME_VALUE_SEPARATOR = '=' const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); 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 parsePlaceholderContent(content) { const trimmed = String(content).trim(); const dotenvMatch = trimmed.match(/^\$dotenv\s+(.+)$/i); if (dotenvMatch) { return { name: dotenvMatch[1].trim(), isDotenv: true }; } return { name: trimmed, isDotenv: false }; } function collectPlaceholders(value) { if (typeof value !== 'string') { return []; } const placeholders = []; const regex = /\{\{([^{}]+)\}\}/g; let match; while ((match = regex.exec(value)) !== null) { placeholders.push(parsePlaceholderContent(match[1]).name); } 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 parsed = YAML.parse(text) || {}; const http = parsed.http || {}; const headers = []; for (const header of Array.isArray(http.headers) ? http.headers : []) { if (!header || typeof header !== 'object') { continue; } headers.push({ name: String(header.name || '').trim(), value: String(header.value ?? '').trim(), }); } return { method: http.method || 'GET', url: http.url || '', params: Array.isArray(http.params) ? http.params : [], headers, body: http.body && typeof http.body === 'object' ? http.body : null, }; } function formatVariableValue(value, renderContext = {}) { if (value === null || value === undefined) { return "''"; } if (typeof value === 'string') { if (value.trim() === '') { return "''"; } const renderedValue = renderContext.renderValue ? renderContext.renderValue(value) : value; if (/\s/.test(renderedValue)) { return `"${renderedValue.replace(/"/g, '\\"')}"`; } return renderedValue; } return JSON.stringify(value); } export function mergeRequestConfig(base, updates) { if (!updates || typeof updates !== 'object') { return base; } const merged = { ...(base || {}) }; if (Object.prototype.hasOwnProperty.call(updates, 'auth')) { if (updates.auth === 'inherit') { if (merged.auth && typeof merged.auth === 'object') { merged.auth = merged.auth; } else { delete merged.auth; } } else if (typeof updates.auth === 'object' && updates.auth !== null) { merged.auth = updates.auth; } else { delete merged.auth; } } else { delete merged.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; } export 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); let requestConfig = null; if (parsed && parsed.request && typeof parsed.request === 'object') { requestConfig = parsed.request; } else if (parsed && typeof parsed === 'object') { const config = {}; if (Object.prototype.hasOwnProperty.call(parsed, 'auth')) { config.auth = parsed.auth; } else if (parsed.http && typeof parsed.http === 'object' && Object.prototype.hasOwnProperty.call(parsed.http, 'auth')) { config.auth = parsed.http.auth; } if (Array.isArray(parsed.variables)) { config.variables = parsed.variables; } if (Object.keys(config).length > 0) { requestConfig = config; } } if (requestConfig !== null) { resolved.push(requestConfig); } else if (path.resolve(filePath) === path.resolve(yamlFile)) { resolved.push({}); } } catch (error) { // Ignore files that cannot be parsed as YAML for request inheritance. } }; 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')); } addFile(yamlFile); return resolved.reduce((result, config) => mergeRequestConfig(result, config), {}); } export function buildRequestContent(request, requestName, requestConfig = {}, dotenvVariables = new Set()) { const lines = []; const variableDefinitions = []; const commentedVariableDefinitions = []; const parameterVariableDefinitions = []; const seenVariables = new Set(); const addVariable = (name, value) => { if (!name) { return; } const normalized = String(name).trim(); if (!normalized || seenVariables.has(normalized) || dotenvVariables.has(normalized)) { return; } seenVariables.add(normalized); variableDefinitions.push({ name: normalized, value }); }; const addParameterVariable = (name, value) => { if (!name) { return; } const normalized = String(name).trim(); if (!normalized || seenVariables.has(normalized) || dotenvVariables.has(normalized)) { return; } seenVariables.add(normalized); parameterVariableDefinitions.push({ name: normalized, value }); }; const addCommentedVariable = (name, value) => { if (!name) { return; } const normalized = String(name).trim(); if (!normalized) { // || seenVariables.has(normalized) || dotenvVariables.has(normalized)) { return; } // seenVariables.add(normalized); commentedVariableDefinitions.push({ name: normalized, value }); }; const addReferencedVariables = (value, fallbackValue = 'YOUR_VALUE_HERE') => { for (const placeholder of collectPlaceholders(String(value))) { addVariable(placeholder, fallbackValue); } }; const renderJsonValue = (value) => { if (typeof value === 'string') { return renderValue(value); } if (Array.isArray(value)) { return value.map((item) => renderJsonValue(item)); } if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value).map(([key, nestedValue]) => [key, renderJsonValue(nestedValue)]), ); } return value; }; const addParameterVariables = (name, value) => { addParameterVariable(name, value); }; const addCommentedVariables = (name, value) => { addCommentedVariable(name, value); }; const renderValue = (value) => { if (typeof value !== 'string') { return value; } return value.replace(/\{\{([^{}]+)\}\}/g, (match, inner) => { const placeholder = parsePlaceholderContent(inner); if (placeholder.isDotenv) { return match; } if (placeholder.name && dotenvVariables.has(placeholder.name)) { return `{{$dotenv ${placeholder.name}}}`; } return match; }); }; 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) { addReferencedVariables(url); url = url.replace(/:([A-Za-z0-9_]+)/g, (_, name) => `{{${name}}}`); url = renderValue(url); // Since Bruno puts enabled params in the URL, this avoids duplicate query params url = url.split('?')[0]; } const queryParams = []; const headers = []; const addHeader = (name, value) => { if (!name) { return; } addReferencedVariables(value ?? ''); headers.push({ name: String(name).trim(), value: renderValue(value ?? '') }); }; for (const header of Array.isArray(request.headers) ? request.headers : []) { if (!header || !header.name) { continue; } addHeader(header.name, header.value ?? ''); } for (const header of Array.isArray(requestConfig.headers) ? requestConfig.headers : []) { if (!header || !header.name) { continue; } addHeader(header.name, 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) { addCommentedVariables(name, value); continue; } addReferencedVariables(value); if (type === 'header') { headers.push({ name, value: renderValue(value) }); } else { queryParams.push({ name, value: `{{${name}}}` }); addParameterVariables(name, value); } } if (requestConfig.auth) { if (requestConfig.auth.type === 'bearer') { addReferencedVariables(requestConfig.auth.token ?? ''); addHeader('Authorization', `Bearer ${renderValue(requestConfig.auth.token ?? '')}`); } else if (requestConfig.auth.type === 'basic') { const username = requestConfig.auth.username ?? ''; const password = requestConfig.auth.password ?? ''; addReferencedVariables(username); addReferencedVariables(password); // VSCode REST Client can manage username:password format directly! addHeader('Authorization', `Basic ${renderValue(username)}:${renderValue(password)}`); } else { headers.push({ name: `UNKNOWN_${requestConfig.auth.type}`, value: `Basic ${requestConfig.auth.token}`, }); } } if (commentedVariableDefinitions.length > 0) { lines.push(`# Other variables for ${requestName}`); for (const variable of commentedVariableDefinitions) { lines.push(`# @${sanitizeVarName(variable.name)}${VARIABLE_NAME_VALUE_SEPARATOR}${formatVariableValue(variable.value, { renderValue })}`); } } if (parameterVariableDefinitions.length > 0) { lines.push(`# Parameter variables for ${requestName}`); for (const variable of parameterVariableDefinitions) { lines.push(`@${sanitizeVarName(variable.name)}${VARIABLE_NAME_VALUE_SEPARATOR}${formatVariableValue(variable.value, { renderValue })}`); } } let requestBody = ''; if (request.body && typeof request.body === 'object') { const bodyType = String(request.body.type || '').toLowerCase(); if (bodyType === 'json') { const jsonData = stripJsonComments(request.body.data, { whitespace: false }); if (jsonData !== undefined && jsonData !== null) { if (typeof jsonData === 'string') { requestBody = renderValue(jsonData); } else { const renderedData = Array.isArray(jsonData) ? jsonData.map((item) => renderJsonValue(item)) : renderJsonValue(jsonData); requestBody = JSON.stringify(renderedData, null, 2); } } addHeader('Content-Type', 'application/json'); } else if (bodyType === 'form-urlencoded') { const parts = []; for (const entry of Array.isArray(request.body.data) ? request.body.data : []) { if (!entry || !entry.name) { continue; } addReferencedVariables(entry.value ?? ''); parts.push(`${entry.name}=${renderValue(entry.value ?? '')}`); } requestBody = parts.join('&'); addHeader('Content-Type', 'application/x-www-form-urlencoded'); } } if (variableDefinitions.length > 0) { lines.push(`# Variables for ${requestName}`); for (const variable of variableDefinitions) { lines.push(`@${sanitizeVarName(variable.name)}${VARIABLE_NAME_VALUE_SEPARATOR}${formatVariableValue(variable.value, { renderValue })}`); } } 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(''); lines.push(`${method} ${requestUrl}`); for (const header of headers) { lines.push(`${header.name}: ${header.value}`); } if (requestBody) { lines.push(''); lines.push(requestBody); } 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 getDotenvVariablesForTargetDir(targetDir, outputRoot, dotenvVariablesByTarget) { const variables = new Set(); let currentDir = targetDir; while (true) { const vars = dotenvVariablesByTarget.get(currentDir); if (vars) { for (const variable of vars) { variables.add(variable); } } if (currentDir === outputRoot || path.dirname(currentDir) === currentDir) { break; } currentDir = path.dirname(currentDir); } return variables; } function writeEnvironmentTemplates(sourceDir, outputRoot) { const targets = []; const dotenvVariablesByTarget = new Map(); 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}=${DEFAULT_ENV_VAR_VALUE}`).join('\n')}\n` : ''; fs.writeFileSync(path.join(targetDir, '.env.template'), templateContent, 'utf8'); dotenvVariablesByTarget.set(targetDir, new Set(variableNames)); } return dotenvVariablesByTarget; } function writeJsFiles(sourceDir, outputRoot) { const targets = []; // const dotenvVariablesByTarget = new Map(); const visit = (currentDir) => { const entries = fs.readdirSync(currentDir, { withFileTypes: true }); const hasJsFiles = entries.some((entry) => entry.isFile() && /\.js$/i.test(entry.name)); if (hasJsFiles) { 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 jsFileDir = dir; if (!fs.existsSync(jsFileDir)) { continue; } const jsFiles = fs.readdirSync(jsFileDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && /\.js$/i.test(entry.name)) .map((entry) => [ path.join(jsFileDir, entry.name), path.join(targetDir, entry.name) ]); // const variableNames = []; // const seenNames = new Set(); // for (const jsFile of jsFiles) { // const parsed = parseYaml(jsFile); // 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}=${DEFAULT_ENV_VAR_VALUE}`).join('\n')}\n` : ''; // fs.writeFileSync(path.join(targetDir, '.env.template'), templateContent, 'utf8'); // dotenvVariablesByTarget.set(targetDir, new Set(variableNames)); for (const jsFile of jsFiles) { fs.copyFileSync(jsFile[0], jsFile[1]); } } return targets; } function cleanFolder(dir) { if (!fs.existsSync(dir)) return; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { // Ricorsione cleanFolder(fullPath); // Dopo la pulizia, se la cartella è vuota → cancellala const remaining = fs.readdirSync(fullPath); if (remaining.length === 0) { fs.rmdirSync(fullPath); } } else if (entry.isFile()) { // File da eliminare if ( entry.name.endsWith(".js") || entry.name.endsWith(".http") || entry.name.endsWith(".env.template") ) { fs.unlinkSync(fullPath); } } } } 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'); } const outputBaseRoot = path.join(workspaceRoot, 'autogen', 'httpyac'); cleanFolder(outputBaseRoot); 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(outputBaseRoot, collection.name); ensureDir(outputRoot); const writtenJsFiles = writeJsFiles(sourceDir, outputRoot); const dotenvVariablesByTarget = 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 dotenvVariables = getDotenvVariablesForTargetDir(targetDir, outputRoot, dotenvVariablesByTarget); const requestContent = buildRequestContent(httpBlock, requestName, requestConfig, dotenvVariables); fs.writeFileSync(outputFile, `${requestContent}\n`, 'utf8'); processed += 1; } console.log(`${collection.name.padEnd(33)} => generated ${processed.toString().padStart(3)} .http file(s) and ${writtenJsFiles.length.toString().padStart(3)} .js file(s)`); } } try { main(); } catch (error) { console.error(error.message); process.exit(1); }