From f9c1321d54d7a651fbe55dbb9e8bebc7e61d9aa7 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Sun, 20 Apr 2025 18:49:40 -0400 Subject: [PATCH 1/5] feat(generator): add `node.config.json` --- src/generators/index.mjs | 2 + .../node-config-schema/constants.mjs | 51 ++++++++ src/generators/node-config-schema/index.mjs | 118 ++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 src/generators/node-config-schema/constants.mjs create mode 100644 src/generators/node-config-schema/index.mjs 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..2a616779 --- /dev/null +++ b/src/generators/node-config-schema/constants.mjs @@ -0,0 +1,51 @@ +// Error Messages +export const ERRORS = { + missingCCandHFiles: + 'Both node_options.cc and node_options.h must be provided.', + headerTypeNotFound: + 'Header type for "{{headerKey}}" not found in the header file.', + missingTypeDefinition: + 'No type definition found for header type "{{headerType}}" in TYPE_DEFINITION_MAP.', +}; + +// 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+)/; + +// Basic JSON schema for node.config.json +export const BASIC_SCHEMA = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + additionalProperties: false, + properties: { + $schema: { + type: 'string', + }, + nodeOptions: { + additionalProperties: false, + properties: {}, + type: 'object', + }, + }, + type: 'object', +}; + +// Schema Definition Map for Data Types +export const TYPE_DEFINITION_MAP = { + 'std::vector': { + oneOf: [ + { type: 'string' }, // Single string case + { + items: { type: 'string', minItems: 1 }, // Array of strings, ensuring at least one item + type: 'array', + }, + ], + }, + uint64_t: { type: 'number' }, // 64-bit unsigned integer maps to a number + int64_t: { type: 'number' }, // 64-bit signed integer maps to a number + HostPort: { type: 'number' }, // HostPort is a number, like 4000 + 'std::string': { type: 'string' }, // String type + bool: { type: 'boolean' }, // Boolean type +}; diff --git a/src/generators/node-config-schema/index.mjs b/src/generators/node-config-schema/index.mjs new file mode 100644 index 00000000..76e73852 --- /dev/null +++ b/src/generators/node-config-schema/index.mjs @@ -0,0 +1,118 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { + ERRORS, + ADD_OPTION_REGEX, + BASIC_SCHEMA, + OPTION_HEADER_KEY_REGEX, + TYPE_DEFINITION_MAP, +} from './constants.mjs'; +import { join } from 'node:path'; + +/** + * 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) { + let ccFile, hFile; + + // Ensure input files are provided and capture the paths + for (const filePath of options.input) { + if (filePath.endsWith('node_options.cc')) { + ccFile = filePath; + } else if (filePath.endsWith('node_options.h')) { + hFile = filePath; + } + } + + // 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'); + + // Clone the BASIC_SCHEMA to avoid mutating the original schema object + /** @type {typeof BASIC_SCHEMA} */ + const schema = Object.assign({}, BASIC_SCHEMA); + 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 }) + ); + } + + const headerType = headerTypeMatch[1].trim(); + + // Ensure the headerType exists in the TYPE_DEFINITION_MAP + const typeDefinition = TYPE_DEFINITION_MAP[headerType]; + if (!typeDefinition) { + throw new Error( + formatErrorMessage(ERRORS.missingTypeDefinition, { headerType }) + ); + } + + // Add the option to the schema after removing the '--' prefix + nodeOptions.properties[option.replace('--', '')] = typeDefinition; + } + + 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; + }, +}; + +/** + * 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. + */ +function formatErrorMessage(message, params) { + return message.replace(/{{(\w+)}}/g, (_, key) => params[key] || `{{${key}}}`); +} From 27fe5d1158dd0202872d09b895740bc95bed5f75 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 21 Apr 2025 12:04:00 -0400 Subject: [PATCH 2/5] move schema to json, use function for type mapping --- .../node-config-schema/constants.mjs | 40 +------------ src/generators/node-config-schema/index.mjs | 56 +++++++++++++------ src/generators/node-config-schema/schema.json | 15 +++++ 3 files changed, 57 insertions(+), 54 deletions(-) create mode 100644 src/generators/node-config-schema/schema.json diff --git a/src/generators/node-config-schema/constants.mjs b/src/generators/node-config-schema/constants.mjs index 2a616779..7197dbe2 100644 --- a/src/generators/node-config-schema/constants.mjs +++ b/src/generators/node-config-schema/constants.mjs @@ -3,9 +3,8 @@ export const ERRORS = { missingCCandHFiles: 'Both node_options.cc and node_options.h must be provided.', headerTypeNotFound: - 'Header type for "{{headerKey}}" not found in the header file.', - missingTypeDefinition: - 'No type definition found for header type "{{headerType}}" in TYPE_DEFINITION_MAP.', + '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. @@ -14,38 +13,3 @@ export const ADD_OPTION_REGEX = // Regex pattern to match header keys in the Options class. export const OPTION_HEADER_KEY_REGEX = /Options::(\w+)/; - -// Basic JSON schema for node.config.json -export const BASIC_SCHEMA = { - $schema: 'https://json-schema.org/draft/2020-12/schema', - additionalProperties: false, - properties: { - $schema: { - type: 'string', - }, - nodeOptions: { - additionalProperties: false, - properties: {}, - type: 'object', - }, - }, - type: 'object', -}; - -// Schema Definition Map for Data Types -export const TYPE_DEFINITION_MAP = { - 'std::vector': { - oneOf: [ - { type: 'string' }, // Single string case - { - items: { type: 'string', minItems: 1 }, // Array of strings, ensuring at least one item - type: 'array', - }, - ], - }, - uint64_t: { type: 'number' }, // 64-bit unsigned integer maps to a number - int64_t: { type: 'number' }, // 64-bit signed integer maps to a number - HostPort: { type: 'number' }, // HostPort is a number, like 4000 - 'std::string': { type: 'string' }, // String type - bool: { type: 'boolean' }, // Boolean type -}; diff --git a/src/generators/node-config-schema/index.mjs b/src/generators/node-config-schema/index.mjs index 76e73852..c5f4c13e 100644 --- a/src/generators/node-config-schema/index.mjs +++ b/src/generators/node-config-schema/index.mjs @@ -2,9 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises'; import { ERRORS, ADD_OPTION_REGEX, - BASIC_SCHEMA, OPTION_HEADER_KEY_REGEX, - TYPE_DEFINITION_MAP, } from './constants.mjs'; import { join } from 'node:path'; @@ -48,10 +46,10 @@ export default { // Read the contents of the cc and h files const ccContent = await readFile(ccFile, 'utf-8'); const hContent = await readFile(hFile, 'utf-8'); + const schema = JSON.parse( + await readFile(new URL('./schema.json', import.meta.url)) + ); - // Clone the BASIC_SCHEMA to avoid mutating the original schema object - /** @type {typeof BASIC_SCHEMA} */ - const schema = Object.assign({}, BASIC_SCHEMA); const { nodeOptions } = schema.properties; // Process the cc content and match AddOption calls @@ -78,18 +76,10 @@ export default { ); } - const headerType = headerTypeMatch[1].trim(); - - // Ensure the headerType exists in the TYPE_DEFINITION_MAP - const typeDefinition = TYPE_DEFINITION_MAP[headerType]; - if (!typeDefinition) { - throw new Error( - formatErrorMessage(ERRORS.missingTypeDefinition, { headerType }) - ); - } - // Add the option to the schema after removing the '--' prefix - nodeOptions.properties[option.replace('--', '')] = typeDefinition; + nodeOptions.properties[option.replace('--', '')] = getTypeSchema( + headerTypeMatch[1].trim() + ); } nodeOptions.properties = Object.fromEntries( @@ -116,3 +106,37 @@ export default { 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. + */ +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 }) + ); + } +} 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" +} From be60acfd2aba0c50c8f3619a37e231ce895fb2c4 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Thu, 24 Apr 2025 16:12:01 -0400 Subject: [PATCH 3/5] resolve reviews --- src/generators/node-config-schema/index.mjs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/generators/node-config-schema/index.mjs b/src/generators/node-config-schema/index.mjs index c5f4c13e..c9d96666 100644 --- a/src/generators/node-config-schema/index.mjs +++ b/src/generators/node-config-schema/index.mjs @@ -5,6 +5,7 @@ import { OPTION_HEADER_KEY_REGEX, } from './constants.mjs'; import { join } from 'node:path'; +import schema from './schema.json' with { type: 'json ' }; /** * This generator generates the `node.config.json` schema. @@ -27,16 +28,13 @@ export default { * @throws {Error} If the required files node_options.cc or node_options.h are missing or invalid. */ async generate(_, options) { - let ccFile, hFile; - // Ensure input files are provided and capture the paths - for (const filePath of options.input) { - if (filePath.endsWith('node_options.cc')) { - ccFile = filePath; - } else if (filePath.endsWith('node_options.h')) { - hFile = filePath; - } - } + 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) { @@ -46,9 +44,6 @@ export default { // Read the contents of the cc and h files const ccContent = await readFile(ccFile, 'utf-8'); const hContent = await readFile(hFile, 'utf-8'); - const schema = JSON.parse( - await readFile(new URL('./schema.json', import.meta.url)) - ); const { nodeOptions } = schema.properties; From 14f3c5ffc71590d478265d397d3504a68c4e4702 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Thu, 24 Apr 2025 16:12:41 -0400 Subject: [PATCH 4/5] fixup! resolve reviews --- src/generators/node-config-schema/index.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generators/node-config-schema/index.mjs b/src/generators/node-config-schema/index.mjs index c9d96666..137fad5e 100644 --- a/src/generators/node-config-schema/index.mjs +++ b/src/generators/node-config-schema/index.mjs @@ -5,7 +5,7 @@ import { OPTION_HEADER_KEY_REGEX, } from './constants.mjs'; import { join } from 'node:path'; -import schema from './schema.json' with { type: 'json ' }; +import schema from './schema.json' with { type: 'json' }; /** * This generator generates the `node.config.json` schema. From 928b3801398cf67b25c2778896208d53e471a062 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Thu, 24 Apr 2025 16:44:55 -0400 Subject: [PATCH 5/5] resolve reviews --- src/generators/node-config-schema/index.mjs | 51 ++----------------- .../node-config-schema/utilities.mjs | 45 ++++++++++++++++ 2 files changed, 50 insertions(+), 46 deletions(-) create mode 100644 src/generators/node-config-schema/utilities.mjs diff --git a/src/generators/node-config-schema/index.mjs b/src/generators/node-config-schema/index.mjs index 137fad5e..47771d8a 100644 --- a/src/generators/node-config-schema/index.mjs +++ b/src/generators/node-config-schema/index.mjs @@ -1,11 +1,13 @@ import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + import { - ERRORS, ADD_OPTION_REGEX, + ERRORS, OPTION_HEADER_KEY_REGEX, } from './constants.mjs'; -import { join } from 'node:path'; import schema from './schema.json' with { type: 'json' }; +import { formatErrorMessage, getTypeSchema } from './utilities.mjs'; /** * This generator generates the `node.config.json` schema. @@ -55,6 +57,7 @@ export default { } 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; @@ -91,47 +94,3 @@ export default { return schema; }, }; - -/** - * 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. - */ -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. - */ -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 }) - ); - } -} 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 }) + ); + } +}