Streamline management of API collections #1
+244
-12
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const YAML = require('yaml');
|
||||
|
||||
const interpolationVariableRegex = /^{{(.*?)}}$/
|
||||
|
||||
function findWorkspaceRoot(startDir) {
|
||||
let current = startDir;
|
||||
@@ -16,6 +19,16 @@ function findWorkspaceRoot(startDir) {
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
@@ -185,24 +198,147 @@ function parseHttpBlock(text) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildRequestContent(request, requestName) {
|
||||
const lines = [];
|
||||
const placeholders = new Set();
|
||||
function formatVariableValue(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '""';
|
||||
}
|
||||
|
||||
const addPlaceholders = (value) => {
|
||||
for (const placeholder of collectPlaceholders(value)) {
|
||||
placeholders.add(placeholder);
|
||||
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) {
|
||||
addPlaceholders(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 || '';
|
||||
@@ -213,7 +349,10 @@ function buildRequestContent(request, requestName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
addPlaceholders(value);
|
||||
for (const placeholder of collectPlaceholders(String(value))) {
|
||||
addVariable(placeholder, 'YOUR_VALUE_HERE');
|
||||
}
|
||||
|
||||
if (type === 'header') {
|
||||
headers.push({ name, value });
|
||||
} else {
|
||||
@@ -221,10 +360,42 @@ function buildRequestContent(request, requestName) {
|
||||
}
|
||||
}
|
||||
|
||||
if (placeholders.size > 0) {
|
||||
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 placeholder of [...placeholders].sort()) {
|
||||
lines.push(`@${sanitizeVarName(placeholder)} = YOUR_VALUE_HERE`);
|
||||
for (const variable of variableDefinitions) {
|
||||
lines.push(`@${sanitizeVarName(variable.name)} = ${formatVariableValue(variable.value)}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
@@ -267,6 +438,65 @@ function walkYamlFiles(rootDir) {
|
||||
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.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');
|
||||
@@ -290,6 +520,7 @@ function main() {
|
||||
|
||||
const outputRoot = path.join(workspaceRoot, 'autodocs', 'http', collection.name);
|
||||
ensureDir(outputRoot);
|
||||
writeEnvironmentTemplates(sourceDir, outputRoot);
|
||||
|
||||
const yamlFiles = walkYamlFiles(sourceDir);
|
||||
let processed = 0;
|
||||
@@ -307,8 +538,9 @@ function main() {
|
||||
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);
|
||||
const requestContent = buildRequestContent(httpBlock, requestName, requestConfig);
|
||||
fs.writeFileSync(outputFile, `${requestContent}\n`, 'utf8');
|
||||
processed += 1;
|
||||
}
|
||||
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "scripts",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user