diff --git a/src/generators/index.mjs b/src/generators/index.mjs index f4bb744a..000bc0d6 100644 --- a/src/generators/index.mjs +++ b/src/generators/index.mjs @@ -10,6 +10,7 @@ import addonVerify from './addon-verify/index.mjs'; import apiLinks from './api-links/index.mjs'; import oramaDb from './orama-db/index.mjs'; import astJs from './ast-js/index.mjs'; +import nodeConfigSchema from './node-config-schema/index.mjs'; export const publicGenerators = { 'json-simple': jsonSimple, @@ -21,6 +22,7 @@ export const publicGenerators = { 'addon-verify': addonVerify, 'api-links': apiLinks, 'orama-db': oramaDb, + 'node-config-schema': nodeConfigSchema, }; export const allGenerators = { diff --git a/src/generators/node-config-schema/constants.mjs b/src/generators/node-config-schema/constants.mjs new file mode 100644 index 00000000..7197dbe2 --- /dev/null +++ b/src/generators/node-config-schema/constants.mjs @@ -0,0 +1,15 @@ +// Error Messages +export const ERRORS = { + missingCCandHFiles: + 'Both node_options.cc and node_options.h must be provided.', + headerTypeNotFound: + 'A type for "{{headerKey}}" not found in the header file.', + missingTypeDefinition: 'No type schema found for "{{type}}".', +}; + +// Regex pattern to match calls to the AddOption function. +export const ADD_OPTION_REGEX = + /AddOption[\s\n\r]*\([\s\n\r]*"([^"]+)"(.*?)\);/gs; + +// Regex pattern to match header keys in the Options class. +export const OPTION_HEADER_KEY_REGEX = /Options::(\w+)/; diff --git a/src/generators/node-config-schema/index.mjs b/src/generators/node-config-schema/index.mjs new file mode 100644 index 00000000..47771d8a --- /dev/null +++ b/src/generators/node-config-schema/index.mjs @@ -0,0 +1,96 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { + ADD_OPTION_REGEX, + ERRORS, + OPTION_HEADER_KEY_REGEX, +} from './constants.mjs'; +import schema from './schema.json' with { type: 'json' }; +import { formatErrorMessage, getTypeSchema } from './utilities.mjs'; + +/** + * This generator generates the `node.config.json` schema. + * + * @typedef {Array} Input + * + * @type {GeneratorMetadata} + */ +export default { + name: 'node-config-schema', + + version: '1.0.0', + + description: 'Generates the node.config.json schema.', + + /** + * Generates the `node.config.json` schema. + * @param {unknown} _ - Unused parameter + * @param {Partial} options - Options containing the input file paths + * @throws {Error} If the required files node_options.cc or node_options.h are missing or invalid. + */ + async generate(_, options) { + // Ensure input files are provided and capture the paths + const ccFile = options.input.find(filePath => + filePath.endsWith('node_options.cc') + ); + const hFile = options.input.find(filePath => + filePath.endsWith('node_options.h') + ); + + // Error handling if either cc or h file is missing + if (!ccFile || !hFile) { + throw new Error(ERRORS.missingCCandHFiles); + } + + // Read the contents of the cc and h files + const ccContent = await readFile(ccFile, 'utf-8'); + const hContent = await readFile(hFile, 'utf-8'); + + const { nodeOptions } = schema.properties; + + // Process the cc content and match AddOption calls + for (const [, option, config] of ccContent.matchAll(ADD_OPTION_REGEX)) { + // If config doesn't include 'kAllowedInEnvvar', skip this option + if (!config.includes('kAllowedInEnvvar')) { + continue; + } + + const headerKey = config.match(OPTION_HEADER_KEY_REGEX)?.[1]; + + // If there's no header key, it's either a V8 option or a no-op + if (!headerKey) { + continue; + } + + // Try to find the corresponding header type in the hContent + const headerTypeMatch = hContent.match( + new RegExp(`\\s*(.+)\\s${headerKey}[^\\B_]`) + ); + + if (!headerTypeMatch) { + throw new Error( + formatErrorMessage(ERRORS.headerTypeNotFound, { headerKey }) + ); + } + + // Add the option to the schema after removing the '--' prefix + nodeOptions.properties[option.replace('--', '')] = getTypeSchema( + headerTypeMatch[1].trim() + ); + } + + nodeOptions.properties = Object.fromEntries( + Object.keys(nodeOptions.properties) + .sort() + .map(key => [key, nodeOptions.properties[key]]) + ); + + await writeFile( + join(options.output, 'node-config-schema.json'), + JSON.stringify(schema, null, 2) + '\n' + ); + + return schema; + }, +}; diff --git a/src/generators/node-config-schema/schema.json b/src/generators/node-config-schema/schema.json new file mode 100644 index 00000000..aedf7a46 --- /dev/null +++ b/src/generators/node-config-schema/schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string" + }, + "nodeOptions": { + "additionalProperties": false, + "properties": {}, + "type": "object" + } + }, + "type": "object" +} diff --git a/src/generators/node-config-schema/utilities.mjs b/src/generators/node-config-schema/utilities.mjs new file mode 100644 index 00000000..f29d977a --- /dev/null +++ b/src/generators/node-config-schema/utilities.mjs @@ -0,0 +1,45 @@ +import { ERRORS } from './constants.mjs'; + +/** + * Helper function to replace placeholders in error messages with dynamic values. + * @param {string} message - The error message with placeholders. + * @param {Object} params - The values to replace the placeholders. + * @returns {string} - The formatted error message. + */ +export function formatErrorMessage(message, params) { + return message.replace(/{{(\w+)}}/g, (_, key) => params[key] || `{{${key}}}`); +} + +/** + * Returns the JSON Schema definition for a given C++ type. + * + * @param {string} type - The type to get the schema for. + * @returns {object} JSON Schema definition for the given type. + */ +export function getTypeSchema(type) { + switch (type) { + case 'std::vector': + return { + oneOf: [ + { type: 'string' }, + { + type: 'array', + items: { type: 'string' }, + minItems: 1, + }, + ], + }; + case 'uint64_t': + case 'int64_t': + case 'HostPort': + return { type: 'number' }; + case 'std::string': + return { type: 'string' }; + case 'bool': + return { type: 'boolean' }; + default: + throw new Error( + formatErrorMessage(ERRORS.missingTypeDefinition, { type }) + ); + } +}