How SXO Resolves Configuration Precedence Between CLI Flags, Environment Variables, and Config Files
SXO resolves configuration precedence by merging four sources in strict order—explicit CLI flags override config files, which override environment variables, which override built-in defaults—using the pickDefined helper in src/js/config.js to ensure the first defined value wins.
SXO (Simple eXtensible Organizer) is an open-source CLI tool that requires flexible configuration management across multiple sources. Understanding how SXO resolves configuration precedence between flags, environment variables, and config files is essential for debugging deployment issues and managing complex development workflows.
The Four-Layer Configuration Hierarchy
SXO builds its final configuration by evaluating four distinct layers. When the same option appears in multiple layers, the value from the highest-priority layer wins.
1. Explicit Command-Line Flags (Highest Priority)
Flags typed directly into the terminal take precedence over all other sources. SXO tracks which flags were explicitly provided versus which hold default values, ensuring that only user-supplied arguments override lower layers.
2. User Configuration Files
The user config file (sxo.config.{mjs|js|cjs|json}) provides project-level defaults. SXO searches for and loads this file in src/js/config.js via the loadUserConfig function (lines 68‑112), merging its exports into the configuration stack.
3. Environment Variables
Environment variables from process.env sit below config files in the hierarchy. Before loading user configs, SXO invokes loadDotenv (lines 57‑66) to populate process.env from .env and .env.local files without overwriting existing environment values.
4. Built-in Command Defaults (Lowest Priority)
Each SXO command (dev, build, etc.) defines its own sensible defaults. These defaults only apply if no higher layer provides a value for a given option.
How the Merge Logic Works in src/js/config.js
The resolution engine lives in src/js/config.js, where the resolveConfig function orchestrates the merge process.
Loading Environment and Config Files
First, SXO prepares the ground layers:
// Lines 57-66: Dotenv loading without clobbering existing env vars
await loadDotenv();
// Lines 68-112: User config loading with format detection (mjs/js/cjs/json)
const fileConfig = await loadUserConfig();
Normalizing and Filtering Explicit Flags
Before merging, SXO normalizes each layer to ensure type consistency. The critical step is explicit flag filtering (lines 94‑102):
// Only flags actually typed on the CLI are allowed to override lower layers
const flagsExplicit = {};
for (const key of Object.keys(flags)) {
if (flags[key] !== undefined && /* flag was explicitly provided */) {
flagsExplicit[key] = flags[key];
}
}
This prevents default flag values (e.g., --open defaulting to true for the dev command) from unintentionally stomping on user config or environment settings.
The Precedence Merge with pickDefined
The final merge occurs in normalizeConfig (lines 109‑122) using the pickDefined helper:
// Precedence: flags → file → env → defaults
const resolved = {
port: pickDefined(flagsExplicit.port, fileConfig.port, env.PORT, defaults.port),
publicPath: pickDefined(flagsExplicit.publicPath, fileConfig.publicPath, env.PUBLIC_PATH, defaults.publicPath),
// ... additional options
};
The pickDefined function returns the first argument that is not undefined, effectively implementing the priority stack.
Final Adjustments
After merging, SXO applies post-processing (lines 124‑140) such as port clamping, path normalization, and default open handling to ensure the configuration is valid and secure.
Practical Configuration Resolution Example
Consider a scenario where the same option is defined in multiple places:
# 1. Environment variables (lowest active priority)
export PORT=4000
export PUBLIC_PATH=/static/
# 2. Config file (higher priority than env)
# sxo.config.js
module.exports = {
port: 5000,
publicPath: "/assets/"
};
# 3. CLI flags (highest priority)
sxo dev --port 6000 --public-path /custom/
The resolved configuration would be:
- port:
6000(from explicit CLI flag) - publicPath:
/custom/(from explicit CLI flag) - open:
true(command default fordev, since no override provided)
You can inspect the final resolved configuration using sxo dev --print-config or by examining the output of resolveConfig in src/js/config.js.
Summary
- SXO uses a four-layer precedence stack: CLI flags > Config files > Environment variables > Built-in defaults.
- The merge logic is centralized in
src/js/config.js, specifically within theresolveConfigandnormalizeConfigfunctions. - Explicit flag filtering prevents default CLI values from overriding user configs or environment variables.
- The
pickDefinedhelper selects the first defined value from the precedence chain (flags → file → env → defaults). - Dotenv files (
.env,.env.local) are loaded early but do not overwrite existingprocess.envvalues.
Frequently Asked Questions
What happens if I specify the same option in both a config file and as a CLI flag?
The CLI flag always wins. According to the precedence logic in src/js/config.js (lines 109‑122), explicit flags are checked first via pickDefined, so --port 3000 overrides port: 4000 in sxo.config.js.
Does SXO support .env files for local development overrides?
Yes. SXO loads .env and .env.local files via the loadDotenv function (lines 57‑66 in src/js/config.js). These files populate process.env without overwriting existing environment variables, placing them below config files but above built-in defaults in the precedence stack.
How does SXO handle boolean flags that have default values?
SXO uses explicit flag filtering to prevent boolean defaults from stomping on user configuration. In src/js/config.js (lines 94‑102), only flags actually typed on the CLI are added to flagsExplicit. This ensures that a default like --open true for the dev command does not override an open: false setting in your config file or OPEN=false environment variable.
Can I see the final resolved configuration without running the command?
Yes. You can inspect the fully resolved configuration by running your command with the --print-config flag (e.g., sxo dev --print-config). This outputs the result of the resolveConfig function after all layers have been merged and normalized, showing exactly which source won for each option.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →