-
-
Notifications
You must be signed in to change notification settings - Fork 161
fix(cjs-default,test): one shared <mod>.default table; cc's MCP debug logger shape and exec/execFile callback order pinned (#9500)
#9531
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
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
7694638
refactor(cjs-default): one shared table for the `<mod>.default` modul…
a23fa1b
test(gap): pin claude-code's MCP debug logger write shape (#9500)
f5d1519
test(gap): exec/execFile callbacks fire in completion order (#9500 pa…
1ac6975
changelog: fragment for #9531 (#9500)
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
changelog.d/9531-cjs-default-table-mcp-logger-exec-order.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| **One shared table for the CommonJS-default module set; claude-code's MCP | ||
| debug logger shape and exec/execFile callback order pinned (#9500).** | ||
|
|
||
| The set of Node builtins whose `require()` / default import hands out a | ||
| distinct `<mod>.default` namespace was hand-maintained in five places (two of | ||
| them inside the HIR alone, already disagreeing on `ffi`, `inspector`, | ||
| `inspector/promises` and `wasi`); the method-call router's copy is the one | ||
| that drifted far enough to break `require('child_process').spawn` (#9485, | ||
| #9498). The table now lives once in `perry-dispatch`, built from one literal | ||
| per module, and the runtime's property-read and method-call paths, the | ||
| `default`-export resolver and the HIR's import lowering all derive from it. | ||
| Adding a module is one line; tests pin the table's shape, the HIR's | ||
| classification of every row, and the router test's list against the table in | ||
| both directions. No behaviour change. | ||
|
|
||
| Two fixtures pin the issue's other findings. The MCP debug logger's exact | ||
| write shape — the `using`-downlevel fs wrapper, the timer/dispose buffered | ||
| writer, the graceful-shutdown cleanup set and the `appendFileSync` → ENOENT → | ||
| `mkdirSync(recursive)` recovery arm that is the only code creating the log | ||
| tree — is byte-compared to node; it fails on a pre-#9491 build (the append | ||
| did not throw, so the tree was never created) and passes on main. The | ||
| exec/execFile callback order is pinned as what node guarantees — completion | ||
| order, whichever API launched the child or came first; the inverted order for | ||
| two instant `echo`s is a same-turn batch-delivery artefact node flips with | ||
| submission order, not a rule. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| //! The one table of Node builtins whose CommonJS `module.exports` is a | ||
| //! namespace object distinct from the ESM namespace — the modules for which | ||
| //! `require('<mod>')`, `import x from '<mod>'` and | ||
| //! `process.getBuiltinModule('<mod>')` hand out a `<mod>.default` namespace | ||
| //! whose method calls and property reads must reach the base module. | ||
| //! | ||
| //! #9500 (from #9485 / #9498): this knowledge used to live in FOUR | ||
| //! hand-maintained copies — the runtime's `cjs_default_base_module` and | ||
| //! `cjs_default_namespace_name` tables, the `cjs_default_export_value` match | ||
| //! arm, the method-call router's own `<mod>.default → base` list, and the | ||
| //! HIR's `is_cjs_style_native_default_import` (itself duplicated in two files | ||
| //! that had already drifted apart: one lacked `ffi`, `inspector`, | ||
| //! `inspector/promises` and `wasi`). The router's copy drifted far enough that | ||
| //! `require('child_process').spawn(...)` dispatched under a name with no | ||
| //! bucket and returned `undefined` WITHOUT SPAWNING, which is why claude-code's | ||
| //! MCP stdio client reported `Failed to connect` (#9485). Every consumer now | ||
| //! derives from this table: adding a module here is the whole edit. | ||
| //! | ||
| //! Base names are the runtime's canonical spellings (`path.posix`, not | ||
| //! `path/posix`; `util`, not `sys`) — the alias folding happens in | ||
| //! `normalize_native_module_name` before any lookup here. | ||
|
|
||
| /// Builds the `(base, "<base>.default")` pairs from one literal per module, so | ||
| /// the two spellings cannot disagree. | ||
| macro_rules! cjs_default_namespace_modules { | ||
| ($($base:literal),+ $(,)?) => { | ||
| /// `(base module, "<base>.default")` for every Node builtin with a | ||
| /// distinct CommonJS default namespace. Sorted by base name. | ||
| pub const CJS_DEFAULT_NAMESPACE_MODULES: &[(&str, &str)] = | ||
| &[$(($base, concat!($base, ".default"))),+]; | ||
| }; | ||
| } | ||
|
|
||
| cjs_default_namespace_modules!( | ||
| "async_hooks", | ||
| "child_process", | ||
| "cluster", | ||
| "constants", | ||
| "dns", | ||
| "dns/promises", | ||
| "ffi", | ||
| "inspector", | ||
| "inspector/promises", | ||
| "module", | ||
| "node-pty", | ||
| "os", | ||
| "path", | ||
| "path.posix", | ||
| "path.win32", | ||
| "process", | ||
| "punycode", | ||
| "querystring", | ||
| "repl", | ||
| "sea", | ||
| "url", | ||
| "util", | ||
| "wasi", | ||
| ); | ||
|
|
||
| /// Whether `base` (canonical spelling) has a distinct `<base>.default` | ||
| /// CommonJS namespace. | ||
| pub fn has_cjs_default_namespace(base: &str) -> bool { | ||
| cjs_default_namespace_name(base).is_some() | ||
| } | ||
|
|
||
| /// `base` → `"<base>.default"`, the name the CJS default namespace object is | ||
| /// created under. | ||
| pub fn cjs_default_namespace_name(base: &str) -> Option<&'static str> { | ||
| CJS_DEFAULT_NAMESPACE_MODULES | ||
| .iter() | ||
| .find(|(b, _)| *b == base) | ||
| .map(|(_, name)| *name) | ||
| } | ||
|
|
||
| /// `"<base>.default"` → `base`: the module a CJS default namespace's method | ||
| /// calls and property reads dispatch against. | ||
| pub fn cjs_default_base_module(namespace_name: &str) -> Option<&'static str> { | ||
| CJS_DEFAULT_NAMESPACE_MODULES | ||
| .iter() | ||
| .find(|(_, name)| *name == namespace_name) | ||
| .map(|(base, _)| *base) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn every_row_round_trips() { | ||
| for (base, name) in CJS_DEFAULT_NAMESPACE_MODULES { | ||
| assert_eq!(*name, format!("{base}.default")); | ||
| assert_eq!(cjs_default_namespace_name(base), Some(*name)); | ||
| assert_eq!(cjs_default_base_module(name), Some(*base)); | ||
| assert!(has_cjs_default_namespace(base)); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn rows_are_unique_and_sorted() { | ||
| let bases: Vec<&str> = CJS_DEFAULT_NAMESPACE_MODULES | ||
| .iter() | ||
| .map(|(b, _)| *b) | ||
| .collect(); | ||
| let mut sorted = bases.clone(); | ||
| sorted.sort_unstable(); | ||
| sorted.dedup(); | ||
| assert_eq!(bases, sorted, "keep the table sorted and duplicate-free"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn base_names_are_canonical_spellings() { | ||
| for (base, _) in CJS_DEFAULT_NAMESPACE_MODULES { | ||
| assert!(!base.starts_with("node:"), "{base}: strip the node: scheme"); | ||
| assert!( | ||
| !matches!(*base, "sys" | "path/posix" | "path/win32"), | ||
| "{base}: an alias, not a canonical module name" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn modules_without_a_cjs_default_namespace_are_absent() { | ||
| for base in [ | ||
| "fs", | ||
| "crypto", | ||
| "events", | ||
| "http", | ||
| "stream", | ||
| "test", | ||
| "child_process.default", | ||
| ] { | ||
| assert!(!has_cjs_default_namespace(base), "{base}"); | ||
| assert_eq!(cjs_default_namespace_name(base), None, "{base}"); | ||
| } | ||
| assert_eq!(cjs_default_base_module("child_process"), None); | ||
| assert_eq!(cjs_default_base_module("fs.default"), None); | ||
| } | ||
|
|
||
| /// The #9485 regression, pinned at the source of truth. | ||
| #[test] | ||
| fn child_process_default_maps_to_child_process() { | ||
| assert_eq!( | ||
| cjs_default_base_module("child_process.default"), | ||
| Some("child_process") | ||
| ); | ||
| assert_eq!( | ||
| cjs_default_namespace_name("child_process"), | ||
| Some("child_process.default") | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove the incorrect no-change statement.
The preceding text says that the HIR lists had drifted on
ffi,inspector,inspector/promises, andwasi. This change restores their CJS-style classification. Replace “No behaviour change.” with the shipped behavior.Proposed revision
Based on learnings, changelog fragments must describe the final shipped behavior as one coherent release-note entry.
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Learnings