Skip to content

Commit 8ea1fc5

Browse files
pmarchinitargos
authored andcommitted
src: support namespace options in configuration file
PR-URL: #58073 Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Giovanni Bucci <github@puskin.it> Reviewed-By: Daniel Lemire <daniel@lemire.me>
1 parent d3fea00 commit 8ea1fc5

20 files changed

+1018
-275
lines changed

doc/api/cli.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -931,11 +931,19 @@ in the `$schema` must be replaced with the version of Node.js you are using.
931931
],
932932
"watch-path": "src",
933933
"watch-preserve-output": true
934+
},
935+
"testRunner": {
936+
"test-isolation": "process"
934937
}
935938
}
936939
```
937940

938-
In the `nodeOptions` field, only flags that are allowed in [`NODE_OPTIONS`][] are supported.
941+
The configuration file supports namespace-specific options:
942+
943+
* The `nodeOptions` field contains CLI flags that are allowed in [`NODE_OPTIONS`][].
944+
945+
* Namespace fields like `testRunner` contain configuration specific to that subsystem.
946+
939947
No-op flags are not supported.
940948
Not all V8 flags are currently supported.
941949

@@ -949,7 +957,7 @@ For example, the configuration file above is equivalent to
949957
the following command-line arguments:
950958

951959
```bash
952-
node --import amaro/strip --watch-path=src --watch-preserve-output
960+
node --import amaro/strip --watch-path=src --watch-preserve-output --test-isolation=process
953961
```
954962

955963
The priority in configuration is as follows:
@@ -962,11 +970,10 @@ Values in the configuration file will not override the values in the environment
962970
variables and command-line options, but will override the values in the `NODE_OPTIONS`
963971
env file parsed by the `--env-file` flag.
964972

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

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

971978
Node.js will not sanitize or perform validation on the user-provided configuration,
972979
so **NEVER** use untrusted configuration files.

doc/node-config-schema.json

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,135 @@
584584
}
585585
},
586586
"type": "object"
587+
},
588+
"testRunner": {
589+
"type": "object",
590+
"additionalProperties": false,
591+
"properties": {
592+
"experimental-test-coverage": {
593+
"type": "boolean"
594+
},
595+
"experimental-test-module-mocks": {
596+
"type": "boolean"
597+
},
598+
"test-concurrency": {
599+
"type": "number"
600+
},
601+
"test-coverage-branches": {
602+
"type": "number"
603+
},
604+
"test-coverage-exclude": {
605+
"oneOf": [
606+
{
607+
"type": "string"
608+
},
609+
{
610+
"items": {
611+
"type": "string",
612+
"minItems": 1
613+
},
614+
"type": "array"
615+
}
616+
]
617+
},
618+
"test-coverage-functions": {
619+
"type": "number"
620+
},
621+
"test-coverage-include": {
622+
"oneOf": [
623+
{
624+
"type": "string"
625+
},
626+
{
627+
"items": {
628+
"type": "string",
629+
"minItems": 1
630+
},
631+
"type": "array"
632+
}
633+
]
634+
},
635+
"test-coverage-lines": {
636+
"type": "number"
637+
},
638+
"test-force-exit": {
639+
"type": "boolean"
640+
},
641+
"test-global-setup": {
642+
"type": "string"
643+
},
644+
"test-isolation": {
645+
"type": "string"
646+
},
647+
"test-name-pattern": {
648+
"oneOf": [
649+
{
650+
"type": "string"
651+
},
652+
{
653+
"items": {
654+
"type": "string",
655+
"minItems": 1
656+
},
657+
"type": "array"
658+
}
659+
]
660+
},
661+
"test-only": {
662+
"type": "boolean"
663+
},
664+
"test-reporter": {
665+
"oneOf": [
666+
{
667+
"type": "string"
668+
},
669+
{
670+
"items": {
671+
"type": "string",
672+
"minItems": 1
673+
},
674+
"type": "array"
675+
}
676+
]
677+
},
678+
"test-reporter-destination": {
679+
"oneOf": [
680+
{
681+
"type": "string"
682+
},
683+
{
684+
"items": {
685+
"type": "string",
686+
"minItems": 1
687+
},
688+
"type": "array"
689+
}
690+
]
691+
},
692+
"test-shard": {
693+
"type": "string"
694+
},
695+
"test-skip-pattern": {
696+
"oneOf": [
697+
{
698+
"type": "string"
699+
},
700+
{
701+
"items": {
702+
"type": "string",
703+
"minItems": 1
704+
},
705+
"type": "array"
706+
}
707+
]
708+
},
709+
"test-timeout": {
710+
"type": "number"
711+
},
712+
"test-update-snapshots": {
713+
"type": "boolean"
714+
}
715+
}
587716
}
588717
},
589718
"type": "object"

lib/internal/options.js

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
getCLIOptionsInfo,
1414
getEmbedderOptions: getEmbedderOptionsFromBinding,
1515
getEnvOptionsInputType,
16+
getNamespaceOptionsInputType,
1617
} = internalBinding('options');
1718

1819
let warnOnAllowUnauthorized = true;
@@ -38,7 +39,22 @@ function getEmbedderOptions() {
3839
}
3940

4041
function generateConfigJsonSchema() {
41-
const map = getEnvOptionsInputType();
42+
const envOptionsMap = getEnvOptionsInputType();
43+
const namespaceOptionsMap = getNamespaceOptionsInputType();
44+
45+
function createPropertyForType(type) {
46+
if (type === 'array') {
47+
return {
48+
__proto__: null,
49+
oneOf: [
50+
{ __proto__: null, type: 'string' },
51+
{ __proto__: null, items: { __proto__: null, type: 'string', minItems: 1 }, type: 'array' },
52+
],
53+
};
54+
}
55+
56+
return { __proto__: null, type };
57+
}
4258

4359
const schema = {
4460
__proto__: null,
@@ -60,31 +76,58 @@ function generateConfigJsonSchema() {
6076
type: 'object',
6177
};
6278

63-
const nodeOptions = schema.properties.nodeOptions.properties;
79+
// Get the root properties object for adding namespaces
80+
const rootProperties = schema.properties;
81+
const nodeOptions = rootProperties.nodeOptions.properties;
6482

65-
for (const { 0: key, 1: type } of map) {
83+
// Add env options to nodeOptions (backward compatibility)
84+
for (const { 0: key, 1: type } of envOptionsMap) {
6685
const keyWithoutPrefix = StringPrototypeReplace(key, '--', '');
67-
if (type === 'array') {
68-
nodeOptions[keyWithoutPrefix] = {
69-
__proto__: null,
70-
oneOf: [
71-
{ __proto__: null, type: 'string' },
72-
{ __proto__: null, items: { __proto__: null, type: 'string', minItems: 1 }, type: 'array' },
73-
],
74-
};
75-
} else {
76-
nodeOptions[keyWithoutPrefix] = { __proto__: null, type };
86+
nodeOptions[keyWithoutPrefix] = createPropertyForType(type);
87+
}
88+
89+
// Add namespace properties at the root level
90+
for (const { 0: namespace, 1: optionsMap } of namespaceOptionsMap) {
91+
// Create namespace object at the root level
92+
rootProperties[namespace] = {
93+
__proto__: null,
94+
type: 'object',
95+
additionalProperties: false,
96+
properties: { __proto__: null },
97+
};
98+
99+
const namespaceProperties = rootProperties[namespace].properties;
100+
101+
// Add all options for this namespace
102+
for (const { 0: optionName, 1: optionType } of optionsMap) {
103+
const keyWithoutPrefix = StringPrototypeReplace(optionName, '--', '');
104+
namespaceProperties[keyWithoutPrefix] = createPropertyForType(optionType);
77105
}
106+
107+
// Sort the namespace properties alphabetically
108+
const sortedNamespaceKeys = ArrayPrototypeSort(ObjectKeys(namespaceProperties));
109+
const sortedNamespaceProperties = ObjectFromEntries(
110+
ArrayPrototypeMap(sortedNamespaceKeys, (key) => [key, namespaceProperties[key]]),
111+
);
112+
rootProperties[namespace].properties = sortedNamespaceProperties;
78113
}
79114

80-
// Sort the proerties by key alphabetically.
115+
// Sort the top-level properties by key alphabetically
81116
const sortedKeys = ArrayPrototypeSort(ObjectKeys(nodeOptions));
82117
const sortedProperties = ObjectFromEntries(
83118
ArrayPrototypeMap(sortedKeys, (key) => [key, nodeOptions[key]]),
84119
);
85120

86121
schema.properties.nodeOptions.properties = sortedProperties;
87122

123+
// Also sort the root level properties
124+
const sortedRootKeys = ArrayPrototypeSort(ObjectKeys(rootProperties));
125+
const sortedRootProperties = ObjectFromEntries(
126+
ArrayPrototypeMap(sortedRootKeys, (key) => [key, rootProperties[key]]),
127+
);
128+
129+
schema.properties = sortedRootProperties;
130+
88131
return schema;
89132
}
90133

lib/internal/test_runner/runner.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
9898
});
9999

100100
const kIsolatedProcessName = Symbol('kIsolatedProcessName');
101-
const kFilterArgs = ['--test', '--experimental-test-coverage', '--watch'];
101+
const kFilterArgs = [
102+
'--test',
103+
'--experimental-test-coverage',
104+
'--watch',
105+
'--experimental-default-config-file',
106+
'--experimental-config-file',
107+
];
102108
const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'];
103109
const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];
104110

src/node.cc

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -910,7 +910,7 @@ static ExitCode InitializeNodeWithArgsInternal(
910910
default:
911911
UNREACHABLE();
912912
}
913-
node_options_from_config = per_process::config_reader.AssignNodeOptions();
913+
node_options_from_config = per_process::config_reader.GetNodeOptions();
914914
// (@marco-ippolito) Avoid reparsing the env options again
915915
std::vector<std::string> env_argv_from_config =
916916
ParseNodeOptionsEnvVar(node_options_from_config, errors);
@@ -956,7 +956,19 @@ static ExitCode InitializeNodeWithArgsInternal(
956956
#endif
957957

958958
if (!(flags & ProcessInitializationFlags::kDisableCLIOptions)) {
959-
const ExitCode exit_code =
959+
// Parse the options coming from the config file.
960+
// This is done before parsing the command line options
961+
// as the cli flags are expected to override the config file ones.
962+
std::vector<std::string> extra_argv =
963+
per_process::config_reader.GetNamespaceFlags();
964+
// [0] is expected to be the program name, fill it in from the real argv.
965+
extra_argv.insert(extra_argv.begin(), argv->at(0));
966+
// Parse the extra argv coming from the config file
967+
ExitCode exit_code = ProcessGlobalArgsInternal(
968+
&extra_argv, nullptr, errors, kDisallowedInEnvvar);
969+
if (exit_code != ExitCode::kNoFailure) return exit_code;
970+
// Parse options coming from the command line.
971+
exit_code =
960972
ProcessGlobalArgsInternal(argv, exec_argv, errors, kDisallowedInEnvvar);
961973
if (exit_code != ExitCode::kNoFailure) return exit_code;
962974
}

0 commit comments

Comments
 (0)