From 88517175321d628359255125a2172013d63d1e39 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Fri, 26 Jun 2026 14:50:23 +0200 Subject: [PATCH] refactor - add YAML module and remove python child process - add collection headers to each request - fix authorization header emission - create .env.template file with environment variables --- scripts/generate-http-docs.js | 742 ++++++++++++++++++++++------------ scripts/package-lock.json | 27 ++ scripts/package.json | 5 + 3 files changed, 519 insertions(+), 255 deletions(-) create mode 100644 scripts/package-lock.json create mode 100644 scripts/package.json diff --git a/scripts/generate-http-docs.js b/scripts/generate-http-docs.js index cee0999..36498a1 100644 --- a/scripts/generate-http-docs.js +++ b/scripts/generate-http-docs.js @@ -1,325 +1,557 @@ #!/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; + 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; } - const parent = path.dirname(current); - if (parent === current) { - throw new Error('workspace.yml not found from the provided start directory'); +} + +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}`); } - current = parent; - } } function readText(filePath) { - return fs.readFileSync(filePath, 'utf8'); + 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; + 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; + 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; + 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]); + } } - 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 }; + return { collections }; } function sanitizeVarName(value) { - return String(value) - .trim() - .replace(/[{}]/g, '') - .replace(/[^A-Za-z0-9_]/g, '_') - .replace(/^([0-9])/, '_$1') || '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; + 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; - } + 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); + 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 block; - } - return []; + 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]) : ''; + 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 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; + 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; + 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); + } } - 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, - }; + return { + method: methodMatch ? stripQuotes(methodMatch[1]) : 'GET', + url: urlMatch ? stripQuotes(urlMatch[1]) : '', + params, + }; } -function buildRequestContent(request, requestName) { - const lines = []; - const placeholders = new Set(); - - const addPlaceholders = (value) => { - for (const placeholder of collectPlaceholders(value)) { - placeholders.add(placeholder); - } - }; - - let url = request.url || ''; - if (url) { - addPlaceholders(url); - url = url.replace(/:([A-Za-z0-9_]+)/g, (_, name) => `{{${name}}}`); - } - - const queryParams = []; - const headers = []; - 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; +function formatVariableValue(value) { + if (value === null || value === undefined) { + return '""'; } - addPlaceholders(value); - if (type === 'header') { - headers.push({ name, value }); - } else { - queryParams.push({ name, value }); + if (typeof value === 'string') { + if (value.trim() === '') { + return '""'; + } + if (/\s/.test(value)) { + return `"${value.replace(/"/g, '\\"')}"`; + } + return value; } - } - if (placeholders.size > 0) { - lines.push(`# Variables for ${requestName}`); - for (const placeholder of [...placeholders].sort()) { - lines.push(`@${sanitizeVarName(placeholder)} = YOUR_VALUE_HERE`); + return JSON.stringify(value); +} + +function mergeRequestConfig(base, updates) { + if (!updates || typeof updates !== 'object') { + return base; } - lines.push(''); - } - const method = (request.method || 'GET').toUpperCase(); - let requestUrl = url; - for (const param of queryParams) { - if (!param.name) { - continue; + 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; + } } - const separator = requestUrl.includes('?') ? '&' : '?'; - requestUrl = `${requestUrl}${separator}${param.name}=${param.value}`; - } + 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; +} - lines.push(`${method} ${requestUrl}`); - for (const header of headers) { - lines.push(`${header.name}: ${header.value}`); - } - return lines.join('\n'); +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 }); + 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 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); + } } - 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.join('\n')}\n` : ''; + fs.writeFileSync(path.join(targetDir, '.env.template'), templateContent, 'utf8'); } - } - return results; } 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 : []; + 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; + if (collections.length === 0) { + throw new Error('No collections found in workspace.yml'); } - const sourceDir = path.join(workspaceRoot, collection.path); - if (!fs.existsSync(sourceDir)) { - console.warn(`Skipping missing collection path: ${collection.path}`); - continue; + 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}`); } - - const outputRoot = path.join(workspaceRoot, 'autodocs', 'http', collection.name); - ensureDir(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 outputFile = path.join(targetDir, `${parsedPath.name}.http`); - const requestContent = buildRequestContent(httpBlock, requestName); - fs.writeFileSync(outputFile, `${requestContent}\n`, 'utf8'); - processed += 1; - } - - console.log(`Generated ${processed} .http file(s) for ${collection.name}`); - } } try { - main(); + main(); } catch (error) { - console.error(error.message); - process.exit(1); + console.error(error.message); + process.exit(1); } diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 0000000..6f47507 --- /dev/null +++ b/scripts/package-lock.json @@ -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" + } + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..dab2d48 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "yaml": "^2.9.0" + } +}