Skip to content

Commit d8da28b

Browse files
committed
feat(generator): add node.config.json
1 parent b6cf072 commit d8da28b

File tree

3 files changed

+195
-0
lines changed

3 files changed

+195
-0
lines changed

src/generators/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import addonVerify from './addon-verify/index.mjs';
1010
import apiLinks from './api-links/index.mjs';
1111
import oramaDb from './orama-db/index.mjs';
1212
import astJs from './ast-js/index.mjs';
13+
import nodeConfigSchema from './node-config-schema/index.mjs';
1314

1415
export const publicGenerators = {
1516
'json-simple': jsonSimple,
@@ -21,6 +22,7 @@ export const publicGenerators = {
2122
'addon-verify': addonVerify,
2223
'api-links': apiLinks,
2324
'orama-db': oramaDb,
25+
'node-config-schema': nodeConfigSchema,
2426
};
2527

2628
export const allGenerators = {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Constants and Error Messages
2+
3+
// Error messages related to missing files or invalid input
4+
export const ERRORS = {
5+
missingCCandHFiles:
6+
'Both node_options.cc and node_options.h must be provided.',
7+
headerTypeNotFound:
8+
'Header type for "{{headerKey}}" not found in the header file.',
9+
missingTypeDefinition:
10+
'No type definition found for header type "{{headerType}}" in TYPE_DEFINITION_MAP.',
11+
};
12+
13+
// Regular Expressions for Option Matching
14+
15+
/**
16+
* Regex pattern to match calls to AddOption function.
17+
* - Captures the option name (inside quotes) and its parameters (if any).
18+
*/
19+
export const ADD_OPTION_REGEX =
20+
/AddOption[\s\n\r]*\([\s\n\r]*"([^"]+)"(.*?)\);/gs;
21+
22+
/**
23+
* Regex pattern to match header keys in the Options class.
24+
* - Captures the key name following 'Options::', e.g., Options::key_name.
25+
*/
26+
export const OPTION_HEADER_KEY_REGEX = /Options::(\w+)/;
27+
28+
// JSON Schema Definitions
29+
30+
/**
31+
* JSON schema for validating the configuration structure.
32+
* - Ensures no additional properties beyond the defined ones are allowed.
33+
* - Defines properties such as $schema and nodeOptions.
34+
*/
35+
export const BASIC_SCHEMA = {
36+
$schema: 'https://json-schema.org/draft/2020-12/schema',
37+
additionalProperties: false, // Disallows extra properties outside the defined schema
38+
properties: {
39+
// The $schema property, which must always be a string
40+
$schema: {
41+
type: 'string',
42+
},
43+
// The nodeOptions property, which must be an object with no additional properties
44+
nodeOptions: {
45+
additionalProperties: false,
46+
properties: {}, // Can be extended if more node options are needed in the future
47+
type: 'object',
48+
},
49+
},
50+
type: 'object', // Defines that the root of the schema is an object
51+
};
52+
53+
// Schema Definition Map for Data Types
54+
/**
55+
* Maps C++ data types to JSON schema definitions.
56+
* - Defines mappings for types like std::vector<std::string>, uint64_t, etc.
57+
* - Ensures correct data structure for each type.
58+
*/
59+
export const TYPE_DEFINITION_MAP = {
60+
'std::vector<std::string>': {
61+
oneOf: [
62+
{ type: 'string' }, // Single string case
63+
{
64+
items: { type: 'string', minItems: 1 }, // Array of strings, ensuring at least one item
65+
type: 'array',
66+
},
67+
],
68+
},
69+
uint64_t: { type: 'number' }, // 64-bit unsigned integer maps to a number
70+
int64_t: { type: 'number' }, // 64-bit signed integer maps to a number
71+
HostPort: { type: 'number' }, // HostPort is a number, like 4000
72+
'std::string': { type: 'string' }, // String type
73+
bool: { type: 'boolean' }, // Boolean type
74+
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { readFile, writeFile } from 'node:fs/promises';
2+
import {
3+
ERRORS,
4+
ADD_OPTION_REGEX,
5+
BASIC_SCHEMA,
6+
OPTION_HEADER_KEY_REGEX,
7+
TYPE_DEFINITION_MAP,
8+
} from './constants.mjs';
9+
import { join } from 'node:path';
10+
11+
/**
12+
* This generator generates the `node.config.json` schema.
13+
*
14+
* @typedef {Array<ApiDocMetadataEntry>} Input
15+
*
16+
* @type {GeneratorMetadata<Input, string>}
17+
*/
18+
export default {
19+
name: 'node-config-schema',
20+
21+
version: '1.0.0',
22+
23+
description: 'Generates the node.config.json schema.',
24+
25+
/**
26+
* Generates the `node.config.json` schema.
27+
* @param {unknown} _ - Unused parameter
28+
* @param {Partial<GeneratorOptions>} options - Options containing the input file paths
29+
* @throws {Error} If the required files node_options.cc or node_options.h are missing or invalid.
30+
*/
31+
async generate(_, options) {
32+
let ccFile, hFile;
33+
34+
// Ensure input files are provided and capture the paths
35+
for (const filePath of options.input) {
36+
if (filePath.endsWith('node_options.cc')) {
37+
ccFile = filePath;
38+
} else if (filePath.endsWith('node_options.h')) {
39+
hFile = filePath;
40+
}
41+
}
42+
43+
// Error handling if either cc or h file is missing
44+
if (!ccFile || !hFile) {
45+
throw new Error(ERRORS.missingCCandHFiles);
46+
}
47+
48+
// Read the contents of the cc and h files
49+
const ccContent = await readFile(ccFile, 'utf-8');
50+
const hContent = await readFile(hFile, 'utf-8');
51+
52+
// Clone the BASIC_SCHEMA to avoid mutating the original schema object
53+
/** @type {typeof BASIC_SCHEMA} */
54+
const schema = Object.assign({}, BASIC_SCHEMA);
55+
const { nodeOptions } = schema.properties;
56+
57+
// Process the cc content and match AddOption calls
58+
for (const [, option, config] of ccContent.matchAll(ADD_OPTION_REGEX)) {
59+
// If config doesn't include 'kAllowedInEnvvar', skip this option
60+
if (!config.includes('kAllowedInEnvvar')) {
61+
continue;
62+
}
63+
64+
const headerKey = config.match(OPTION_HEADER_KEY_REGEX)?.[1];
65+
// If there's no header key, it's either a V8 option or a no-op
66+
if (!headerKey) {
67+
continue;
68+
}
69+
70+
// Try to find the corresponding header type in the hContent
71+
const headerTypeMatch = hContent.match(
72+
new RegExp(`\\s*(.+)\\s${headerKey}[^\\B_]`)
73+
);
74+
75+
// Error handling if header type is not found
76+
if (!headerTypeMatch) {
77+
throw new Error(
78+
formatErrorMessage(ERRORS.headerTypeNotFound, { headerKey })
79+
);
80+
}
81+
82+
const headerType = headerTypeMatch[1].trim();
83+
84+
// Ensure the headerType exists in the TYPE_DEFINITION_MAP
85+
const typeDefinition = TYPE_DEFINITION_MAP[headerType];
86+
if (!typeDefinition) {
87+
throw new Error(
88+
formatErrorMessage(ERRORS.missingTypeDefinition, { headerType })
89+
);
90+
}
91+
92+
// Add the option to the schema after removing the '--' prefix
93+
nodeOptions.properties[option.replace('--', '')] = typeDefinition;
94+
}
95+
96+
nodeOptions.properties = Object.fromEntries(
97+
Object.keys(nodeOptions.properties)
98+
.sort()
99+
.map(key => [key, nodeOptions.properties[key]])
100+
);
101+
102+
await writeFile(
103+
join(options.output, 'node-config-schema.json'),
104+
JSON.stringify(schema, null, 2) + '\n'
105+
);
106+
107+
return schema;
108+
},
109+
};
110+
111+
/**
112+
* Helper function to replace placeholders in error messages with dynamic values.
113+
* @param {string} message - The error message with placeholders.
114+
* @param {Object} params - The values to replace the placeholders.
115+
* @returns {string} - The formatted error message.
116+
*/
117+
function formatErrorMessage(message, params) {
118+
return message.replace(/{{(\w+)}}/g, (_, key) => params[key] || `{{${key}}}`);
119+
}

0 commit comments

Comments
 (0)