Intersect injected flags with the Node binary's accepted set - #398
Conversation
…d set nub injects `--experimental-*` flags to unflag Node features early, with several open-ended version bands on the premise that once a flag exists Node accepts it forever. That premise is false: Node keeps some unflagged flags as no-ops but hard-removes others (`--experimental-policy`, `--experimental-network-imports`; `--experimental-permission` was deleted at 24.0 when it stabilized to `--permission`). Injecting a removed flag is a `node: bad option` startup abort, so an open-ended band would crash nub on a future Node that drops the flag. Probe the target Node's `process.allowedNodeEnvironmentFlags` once, cache it per (path, mtime), and intersect it with the version-band inject set in `compute_inject_flags` (Stage 4). A flag the running Node does not accept is dropped, so injection self-corrects with no nub release when Node removes a flag. Probe unavailable falls back to the prior version-band behavior. The probe clears NODE_OPTIONS so an inherited preload can't pollute or side-effect it. A host-Node invariant test asserts the intersection drops nothing on a current, supported Node (nub only injects env-allowed flags). Refs #395.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — makes --experimental-* flag injection self-correcting by intersecting the version-band inject set with the target Node's actual allowedNodeEnvironmentFlags, so a flag a future Node hard-removes is dropped instead of aborting startup with node: bad option.
- Probe + cache the accepted-flag set —
discovery::accepted_env_flags(node_path)runs onenode -eprobe ofprocess.allowedNodeEnvironmentFlagsand caches it per(path, mtime)innode-env-flags.json; warm path is spawn-free. Clears inheritedNODE_OPTIONSfor the probe so it stays deterministic and side-effect-free. - Stage-4 intersection in
compute_inject_flags— newaccepted_env_flags: Option<&BTreeSet<String>>param drops any flag the binary doesn't accept, matched on the flag NAME (split_once('=')) so value-bearing flags like--disable-warning=…match the bare accepted name.Nonepreserves pure version-band behavior, so a failed probe never regresses. - Threaded through the spawn paths —
compute_augmentation_envgained anode_pathparam (all three call sites updated), andspawn_node+run_watchpass the probed set. - Webstorage injection stays outside the intersection deliberately — its band is closed (
22.4–<25) and the flag stabilized at 25, so there's no open-ended-removal hazard; documented inline. - Invariant test —
host_node_accepts_every_injected_flagasserts the intersection drops nothing on the host Node, encoding the design's load-bearing property (nub only injects envvar-allowed flags).
The soundness argument holds: nub propagates every injected flag via NODE_OPTIONS, and allowedNodeEnvironmentFlags enumerates exactly the envvar-allowed universe, so the intersection can only ever bite a future Node that removed a flag. The fail-safe None fallback, the name-based match, and the real-Node invariant test are all correct. The non-atomic read-modify-write in write_env_flags_cache mirrors the pre-existing write_version_cache pattern, so it's consistent with convention rather than a new concern. Mergeable as-is.
Claude Opus | 𝕏
|
Shipped in v0.4.6: https://github.com/nubjs/nub/releases/tag/v0.4.6 |

nub injects
--experimental-*flags to unflag Node features early. Several of those bands are open-ended ([lo, ∞), e.g.--experimental-import-texton[26.5.0, ∞)) on the assumption that once a flag exists Node keeps accepting it forever.That assumption is false. Node keeps some unflagged flags as accepted no-ops (
--experimental-fetch,--experimental-modules) but hard-removes others:--experimental-policyand--experimental-network-importsare gone, and--experimental-permissionwas accepted through Node 23.11 then deleted at 24.0 (it stabilized to--permission). Passing a removed flag is anode: bad optionstartup abort (exit 9), before any code runs. So an open-ended band would crash nub on whatever future Node drops the flag — and runtime feature-detection can't help, since the flag is parsed before any preload loads.Fix
Make injection self-correcting by intersecting the version-band set with the target Node's actual accepted-flag set:
discovery::accepted_env_flags(node_path)probesprocess.allowedNodeEnvironmentFlagsonce and caches it per(path, mtime)in~/.cache/nub/node-env-flags.json. Amortized cost ≈ 0 (one probe per binary, ever; spawn-free on the warm path).compute_inject_flagsgained a Stage-4 intersection: a flag the running Node doesn't accept is dropped.None(probe unavailable) preserves the prior version-band behavior, so a failed probe never regresses.NODE_OPTIONS, so every injected flag is already envvar-allowed — exactly whatallowedNodeEnvironmentFlagsenumerates. Ahost_node_accepts_every_injected_flagtest guards the invariant (the intersection drops nothing on a current, supported Node).A future Node that removes a flag now needs no nub release — the probe stops offering it and Stage 4 drops it.
Verification
node_options.cc(no-op vs hard-removed flags; the--experimental-permission→--permissiondeletion at 24.0).--experimental-import-textis injected on Node 26.5 (present in its probed set) and not on 26.2 (absent); warm runs hit the cache with no re-probe.cargo clippy --all-targets --all-features -D warnings,cargo fmt --check, and thenub-corenode test suite pass.Refs #395.