Skip to main content

aube_settings/
meta.rs

1//! Static metadata for every setting in `settings.toml`.
2//!
3//! The `SETTINGS` slice is emitted at build time by `build.rs` from
4//! the crate-local `settings.toml`. Each entry is a `'static`
5//! borrow so there's zero runtime allocation and the compiler
6//! verifies the schema shape at compile time.
7//!
8//! Use cases:
9//! - Future `aube settings` subcommand to render a CLI version of
10//!   pnpm's settings reference page
11//! - The docs generator that reads this and emits
12//!   `docs/settings/*.md`
13
14/// Metadata for one setting. Every field is `'static` because the
15/// containing `SETTINGS` slice is a `const` built from string
16/// literals at compile time.
17#[derive(Debug, Clone, Copy)]
18#[allow(dead_code)] // Fields are consumed by tests and future generators.
19pub struct SettingMeta {
20    /// pnpm-style canonical name (e.g. `preferFrozenLockfile`, `registries`)
21    pub name: &'static str,
22    /// One-line summary
23    pub description: &'static str,
24    /// TOML-ish type spec (e.g. `"bool"`, `"list<string>"`, `"\"highest\" | \"time-based\""`)
25    pub type_: &'static str,
26    /// Default value rendered as a string (e.g. `"true"`, `"undefined"`)
27    pub default: &'static str,
28    /// Longer markdown docs body
29    pub docs: &'static str,
30    /// CLI flags that set this setting
31    pub cli_flags: &'static [&'static str],
32    /// Environment variables that set this setting
33    pub env_vars: &'static [&'static str],
34    /// `.npmrc` keys that set this setting (aliases listed together;
35    /// the first entry is treated as the canonical spelling)
36    pub npmrc_keys: &'static [&'static str],
37    /// `pnpm-workspace.yaml` (and `aube-workspace.yaml`) keys that
38    /// set this setting.
39    pub workspace_yaml_keys: &'static [&'static str],
40    /// Example invocations
41    pub examples: &'static [&'static str],
42    /// Escape hatch for the workspace-level accessor audit in
43    /// `tests/accessor_audit.rs`. `true` means "this setting is
44    /// honored but not via its generated `resolved::<name>` typed
45    /// accessor" — e.g. read through `std::env::var` behind a
46    /// hand-rolled `LazyLock`, looked up in `NpmConfig` by string
47    /// key, or accepted for pnpm parity with no behavior wired.
48    /// Default `false`; the audit fails CI when a setting with a
49    /// supported scalar type has no `resolved::<name>` call site
50    /// anywhere in the workspace and this flag is not set.
51    pub typed_accessor_unused: bool,
52    /// Marks a setting as part of the npm-shared `.npmrc` surface:
53    /// npm (and yarn / pnpm) read it from `.npmrc` too, so
54    /// `aube config set` writes it there to keep the multi-tool
55    /// contract. Aube-specific / pnpm-only settings leave this
56    /// `false` and route to aube's own config (`config.toml`).
57    pub npm_shared: bool,
58}
59
60// Pulls in `pub const SETTINGS: &[SettingMeta] = &[...]` generated by build.rs.
61include!(concat!(env!("OUT_DIR"), "/settings_meta_data.rs"));
62
63/// Return the full slice of settings, alphabetically sorted by name.
64pub fn all() -> &'static [SettingMeta] {
65    SETTINGS
66}
67
68/// Look up a setting by its canonical pnpm name. `SETTINGS` is
69/// generated sorted by name (build.rs guarantees this via
70/// `BTreeMap` iteration), so a binary search drops the lookup from
71/// O(N) to O(log N). With ~120 entries and dozens of lookups per
72/// command, the saving compounds across every `aube run` startup.
73///
74/// The sort invariant is asserted in debug builds. A future hand
75/// edit of `SETTINGS` (or a regression in the build.rs sort) would
76/// silently flip valid lookups to `None`; the assert catches that
77/// in test runs without slowing release builds.
78pub fn find(name: &str) -> Option<&'static SettingMeta> {
79    debug_assert!(
80        SETTINGS.windows(2).all(|w| w[0].name <= w[1].name),
81        "SETTINGS must be sorted by name for find() to work"
82    );
83    SETTINGS
84        .binary_search_by(|s| s.name.cmp(name))
85        .ok()
86        .map(|i| &SETTINGS[i])
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn settings_metadata_has_expected_count() {
95        assert!(
96            SETTINGS.len() >= 100,
97            "expected ≥100 settings, got {}",
98            SETTINGS.len()
99        );
100    }
101
102    #[test]
103    fn every_setting_has_required_fields() {
104        for s in SETTINGS {
105            assert!(!s.name.is_empty(), "empty name in entry");
106            assert!(!s.description.is_empty(), "{}: empty description", s.name);
107            assert!(!s.type_.is_empty(), "{}: empty type", s.name);
108        }
109    }
110
111    #[test]
112    fn known_settings_resolve() {
113        assert!(find("registries").is_some());
114        assert!(find("preferFrozenLockfile").is_some());
115        assert!(find("aubeNoLock").is_some());
116        assert!(find("totally-fake-setting").is_none());
117    }
118
119    #[test]
120    fn find_matches_all() {
121        for s in all() {
122            assert_eq!(find(s.name).map(|x| x.name), Some(s.name));
123        }
124    }
125
126    /// Every setting that advertises a `workspaceYaml` source must
127    /// actually be readable from `pnpm-workspace.yaml` — i.e. the key
128    /// has to deserialize onto `WorkspaceConfig` without falling into
129    /// the `#[serde(flatten)] extra` catch-all. Guards against metadata
130    /// rotting away from real workspace-config support.
131    ///
132    /// Lives here (in `aube-settings` — a crate that `aube-manifest`
133    /// itself does not depend on) so it's a dev-only parity check; the
134    /// dependency edge only exists for tests.
135    #[test]
136    fn workspace_yaml_keys_deserialize_onto_workspace_config() {
137        // Field types on WorkspaceConfig vary, so there's no single
138        // YAML value that parses for every field. Try a handful of
139        // shapes in turn — if at least one parses AND the key doesn't
140        // fall into `extra`, the field is wired up.
141        let shapes = ["null", "true", "\"x\"", "0", "{}", "[]"];
142        for s in SETTINGS {
143            for key in s.workspace_yaml_keys {
144                let mut recognized = false;
145                let top_level_key = key.split('.').next().unwrap_or(key);
146                for shape in shapes {
147                    let yaml = format!("{top_level_key}: {shape}\n");
148                    if let Ok(cfg) = yaml_serde::from_str::<aube_manifest::WorkspaceConfig>(&yaml)
149                        && !cfg.extra.contains_key(top_level_key)
150                    {
151                        recognized = true;
152                        break;
153                    }
154                }
155                assert!(
156                    recognized,
157                    "{}: workspaceYaml key `{key}` is not a real field on WorkspaceConfig \
158                     — it fell through to `extra` (or failed to deserialize entirely). \
159                     Either add the field or remove the metadata.",
160                    s.name
161                );
162            }
163        }
164    }
165}