Skip to content

feat(generator): add node.config.json #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/generators/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,6 +22,7 @@ export const publicGenerators = {
'addon-verify': addonVerify,
'api-links': apiLinks,
'orama-db': oramaDb,
'node-config-schema': nodeConfigSchema,
};

export const allGenerators = {
Expand Down
15 changes: 15 additions & 0 deletions src/generators/node-config-schema/constants.mjs
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

@joyeecheung joyeecheung Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly that this parses the C++ source code to get the options? If so this feels like a misstep. The current C++ option parser puts an overhead at runtime to add the options. Ideally that should be refactored to add most of the options at compile time and let the parser be statically generated to eliminate the overhead and eliminate more static STL containers. Parsing the source code in an ad-hoc way in another tooling that would break the tests makes it harder to change the parser in the future, the more we do it the more the doc tooling can become a road block on performance optimisations of core which doesn't sound right. It would be better to use the binary to get the options instead of counting on the source code to be a particular shape.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @joyeecheung for the feedback! I feel the straightforward way is to simply use the existing code incorporated on this repository? Or should the generation of the node.config.json schema simply not be handled by the api-docs-tooling 🤔 any thoughts or alternatives are appreciated 🙇

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in turns of churn/volatility, using public APIs is better than using internal APIs, and using internal APIs is better than parsing the source. It might warrant a feature request for a public API of this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Joyee, that feels like a good path forward.


// Regex pattern to match header keys in the Options class.
export const OPTION_HEADER_KEY_REGEX = /Options::(\w+)/;
96 changes: 96 additions & 0 deletions src/generators/node-config-schema/index.mjs
Original file line number Diff line number Diff line change
@@ -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<ApiDocMetadataEntry>} Input
*
* @type {GeneratorMetadata<Input, string>}
*/
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<GeneratorOptions>} 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;
},
};
15 changes: 15 additions & 0 deletions src/generators/node-config-schema/schema.json
Original file line number Diff line number Diff line change
@@ -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"
}
45 changes: 45 additions & 0 deletions src/generators/node-config-schema/utilities.mjs
Original file line number Diff line number Diff line change
@@ -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<std::string>':
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 })
);
}
}
Loading