Skip to content

src: support namespace options in configuration #58073

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

Merged
Merged
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
17 changes: 12 additions & 5 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,11 +933,19 @@ in the `$schema` must be replaced with the version of Node.js you are using.
],
"watch-path": "src",
"watch-preserve-output": true
},
"testRunner": {
"test-isolation": "process"
}
}
```

In the `nodeOptions` field, only flags that are allowed in [`NODE_OPTIONS`][] are supported.
The configuration file supports namespace-specific options:

* The `nodeOptions` field contains CLI flags that are allowed in [`NODE_OPTIONS`][].

* Namespace fields like `testRunner` contain configuration specific to that subsystem.

No-op flags are not supported.
Not all V8 flags are currently supported.

Expand All @@ -951,7 +959,7 @@ For example, the configuration file above is equivalent to
the following command-line arguments:

```bash
node --import amaro/strip --watch-path=src --watch-preserve-output
node --import amaro/strip --watch-path=src --watch-preserve-output --test-isolation=process
```

The priority in configuration is as follows:
Expand All @@ -964,11 +972,10 @@ Values in the configuration file will not override the values in the environment
variables and command-line options, but will override the values in the `NODE_OPTIONS`
env file parsed by the `--env-file` flag.

If duplicate keys are present in the configuration file, only
the first key will be used.
Keys cannot be duplicated within the same or different namespaces.

The configuration parser will throw an error if the configuration file contains
unknown keys or keys that cannot used in `NODE_OPTIONS`.
unknown keys or keys that cannot be used in a namespace.

Node.js will not sanitize or perform validation on the user-provided configuration,
so **NEVER** use untrusted configuration files.
Expand Down
129 changes: 129 additions & 0 deletions doc/node-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,135 @@
}
},
"type": "object"
},
"testRunner": {
"type": "object",
"additionalProperties": false,
"properties": {
"experimental-test-coverage": {
"type": "boolean"
},
"experimental-test-module-mocks": {
"type": "boolean"
},
"test-concurrency": {
"type": "number"
},
"test-coverage-branches": {
"type": "number"
},
"test-coverage-exclude": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-coverage-functions": {
"type": "number"
},
"test-coverage-include": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-coverage-lines": {
"type": "number"
},
"test-force-exit": {
"type": "boolean"
},
"test-global-setup": {
"type": "string"
},
"test-isolation": {
"type": "string"
},
"test-name-pattern": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-only": {
"type": "boolean"
},
"test-reporter": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-reporter-destination": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-shard": {
"type": "string"
},
"test-skip-pattern": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-timeout": {
"type": "number"
},
"test-update-snapshots": {
"type": "boolean"
}
}
}
},
"type": "object"
Expand Down
71 changes: 57 additions & 14 deletions lib/internal/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
getCLIOptionsInfo,
getEmbedderOptions: getEmbedderOptionsFromBinding,
getEnvOptionsInputType,
getNamespaceOptionsInputType,
} = internalBinding('options');

let warnOnAllowUnauthorized = true;
Expand All @@ -38,7 +39,22 @@ function getEmbedderOptions() {
}

function generateConfigJsonSchema() {
const map = getEnvOptionsInputType();
const envOptionsMap = getEnvOptionsInputType();
const namespaceOptionsMap = getNamespaceOptionsInputType();

function createPropertyForType(type) {
if (type === 'array') {
return {
__proto__: null,
oneOf: [
{ __proto__: null, type: 'string' },
{ __proto__: null, items: { __proto__: null, type: 'string', minItems: 1 }, type: 'array' },
],
};
}

return { __proto__: null, type };
}

const schema = {
__proto__: null,
Expand All @@ -60,31 +76,58 @@ function generateConfigJsonSchema() {
type: 'object',
};

const nodeOptions = schema.properties.nodeOptions.properties;
// Get the root properties object for adding namespaces
const rootProperties = schema.properties;
const nodeOptions = rootProperties.nodeOptions.properties;

for (const { 0: key, 1: type } of map) {
// Add env options to nodeOptions (backward compatibility)
for (const { 0: key, 1: type } of envOptionsMap) {
const keyWithoutPrefix = StringPrototypeReplace(key, '--', '');
if (type === 'array') {
nodeOptions[keyWithoutPrefix] = {
__proto__: null,
oneOf: [
{ __proto__: null, type: 'string' },
{ __proto__: null, items: { __proto__: null, type: 'string', minItems: 1 }, type: 'array' },
],
};
} else {
nodeOptions[keyWithoutPrefix] = { __proto__: null, type };
nodeOptions[keyWithoutPrefix] = createPropertyForType(type);
}

// Add namespace properties at the root level
for (const { 0: namespace, 1: optionsMap } of namespaceOptionsMap) {
// Create namespace object at the root level
rootProperties[namespace] = {
__proto__: null,
type: 'object',
additionalProperties: false,
properties: { __proto__: null },
};

const namespaceProperties = rootProperties[namespace].properties;

// Add all options for this namespace
for (const { 0: optionName, 1: optionType } of optionsMap) {
const keyWithoutPrefix = StringPrototypeReplace(optionName, '--', '');
namespaceProperties[keyWithoutPrefix] = createPropertyForType(optionType);
}

// Sort the namespace properties alphabetically
const sortedNamespaceKeys = ArrayPrototypeSort(ObjectKeys(namespaceProperties));
const sortedNamespaceProperties = ObjectFromEntries(
ArrayPrototypeMap(sortedNamespaceKeys, (key) => [key, namespaceProperties[key]]),
);
rootProperties[namespace].properties = sortedNamespaceProperties;
}

// Sort the proerties by key alphabetically.
// Sort the top-level properties by key alphabetically
const sortedKeys = ArrayPrototypeSort(ObjectKeys(nodeOptions));
const sortedProperties = ObjectFromEntries(
ArrayPrototypeMap(sortedKeys, (key) => [key, nodeOptions[key]]),
);

schema.properties.nodeOptions.properties = sortedProperties;

// Also sort the root level properties
const sortedRootKeys = ArrayPrototypeSort(ObjectKeys(rootProperties));
const sortedRootProperties = ObjectFromEntries(
ArrayPrototypeMap(sortedRootKeys, (key) => [key, rootProperties[key]]),
);

schema.properties = sortedRootProperties;

return schema;
}

Expand Down
8 changes: 7 additions & 1 deletion lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,13 @@ let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
});

const kIsolatedProcessName = Symbol('kIsolatedProcessName');
const kFilterArgs = ['--test', '--experimental-test-coverage', '--watch'];
const kFilterArgs = [
'--test',
'--experimental-test-coverage',
'--watch',
'--experimental-default-config-file',
'--experimental-config-file',
];
const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'];
const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];

Expand Down
16 changes: 14 additions & 2 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,7 @@ static ExitCode InitializeNodeWithArgsInternal(
default:
UNREACHABLE();
}
node_options_from_config = per_process::config_reader.AssignNodeOptions();
node_options_from_config = per_process::config_reader.GetNodeOptions();
// (@marco-ippolito) Avoid reparsing the env options again
std::vector<std::string> env_argv_from_config =
ParseNodeOptionsEnvVar(node_options_from_config, errors);
Expand Down Expand Up @@ -956,7 +956,19 @@ static ExitCode InitializeNodeWithArgsInternal(
#endif

if (!(flags & ProcessInitializationFlags::kDisableCLIOptions)) {
const ExitCode exit_code =
// Parse the options coming from the config file.
// This is done before parsing the command line options
// as the cli flags are expected to override the config file ones.
std::vector<std::string> extra_argv =
per_process::config_reader.GetNamespaceFlags();
// [0] is expected to be the program name, fill it in from the real argv.
extra_argv.insert(extra_argv.begin(), argv->at(0));
// Parse the extra argv coming from the config file
ExitCode exit_code = ProcessGlobalArgsInternal(
&extra_argv, nullptr, errors, kDisallowedInEnvvar);
if (exit_code != ExitCode::kNoFailure) return exit_code;
// Parse options coming from the command line.
exit_code =
ProcessGlobalArgsInternal(argv, exec_argv, errors, kDisallowedInEnvvar);
if (exit_code != ExitCode::kNoFailure) return exit_code;
}
Expand Down
Loading
Loading