- Which result and explanation are correct?
Answer: It prints "false true" because b.cjs observes a.cjs's partially initialized exports during the cycle.
When you see circular dependencies in CommonJS, the key concept to focus on is partial initialization — Node.js does not wait for a module to fully execute before handing it to another module that requires it mid-cycle.
Here's what actually happens step by step: app.cjs requires a.cjs, which begins executing. First, exports.ready = false runs. Then a.cjs requires b.cjs, which pauses a.cjs's execution. Inside b.cjs, Node detects the cycle and immediately returns a.cjs's current, partially-built exports object — at this moment, that object only has { ready: false }. So b.cjs sets exports.observed = false. Control returns to a.cjs, which sets exports.seenByB = false, then exports.ready = true. When app.cjs logs a.seenByB and a.ready, it prints false true — making A correct.
B is wrong because CommonJS does not block or wait for a module to finish before returning it in a cycle — that's the entire reason partial initialization exists. C is wrong because properties assigned before the nested require call are absolutely visible; ready: false was set before b.cjs was loaded, which is exactly why b.cjs sees false rather than undefined. D is wrong because Node.js deliberately allows circular CommonJS dependencies by returning the partial exports object rather than throwing.
As a study tip, remember: in a CommonJS cycle, the required module receives a snapshot of exports at the moment the cycle is detected, not a final version — whatever has been assigned up to that point is visible, nothing more.
- What does the console display, and why?
Answer: It displays "legacy-setup" because the classic script runs before the deferred module graph.
When you see HTML mixing <script type="module"> and classic <script> tags, the key question is: in what order do they actually execute? Browser modules behave like deferred scripts — they wait until the HTML is parsed before running, even without an explicit defer attribute. Classic scripts without defer or async, however, run immediately as the parser encounters them.
Here, the parser reads the module tag first but defers its entire execution (including fetching and running setup.js). Then it hits legacy.js — a classic script — which runs immediately, setting globalThis.mode = "legacy". Only after the document finishes parsing does the browser execute the module graph: setup.js runs first (as an imported dependency), appending "-setup" to produce "legacy-setup", and then main.js logs it. So B is correct: the classic script runs before the deferred module graph, giving you "legacy-setup".
A is wrong because it assumes the module starts executing before legacy.js. The module tag appears first in the HTML, but modules are always deferred — legacy.js wins the execution race. C is wrong on two counts: it claims "legacy" is logged (missing "-setup"), and it misunderstands module execution order — imported dependencies run before the importing module's body, not after. D is simply false; importing a module purely for side effects with import "./setup.js" is completely valid ES module syntax.
Your study tip: remember the mantra — modules are always deferred, classic scripts are not. When you see mixed script types, mentally sort them: classic scripts first, module graphs after parsing completes.
- What value is assigned to values?
Answer: An object containing alpha: 1 and gamma: 3, but no beta property.
When working with CommonJS modules, the key distinction to understand is that exports and module.exports are not the same thing — they just start out pointing to the same object. What require() actually returns is always module.exports, never exports directly.
In values.cjs, three things happen in sequence. First, exports.alpha = 1 adds alpha to the shared object, so both exports and module.exports now reference { alpha: 1 }. Second, exports = { beta: 2 } reassigns the local exports variable to a brand-new object — but this only breaks the reference for exports. module.exports is untouched and still holds { alpha: 1 }. Third, module.exports.gamma = 3 adds gamma directly to module.exports, making it { alpha: 1, gamma: 3 }. Since require() returns module.exports, values ends up as { alpha: 1, gamma: 3 }, confirming A is correct.
B is wrong because after exports is reassigned, beta: 2 lives on an orphaned object that module.exports never references. C is wrong for the same reason — beta is never merged into module.exports, so all three properties cannot coexist in the result. D is a tempting trap: yes, exports was reassigned, but that reassignment has no effect on module.exports, so claiming only beta: 2 survives misunderstands which variable actually matters.
A useful mental model: think of exports as a convenient shortcut alias. The moment you reassign that alias with =, you lose the shortcut — but module.exports is the real export, and it's unaffected.
- What is the intended effect when other packages consume this package?
Answer: ES module import loads index.js, while CommonJS require loads index.cjs through conditional exports.
When you see a question about Node.js module resolution with an exports field in package.json, focus on how conditional exports work: Node.js inspects how a package is being loaded and routes the request to the appropriate file based on matching conditions.
The exports field here defines two conditions under the "." entry point: "import" and "require". When a consumer uses import (ES module syntax), Node.js matches the "import" condition and loads ./dist/index.js. When a consumer uses require() (CommonJS), Node.js matches the "require" condition and loads ./dist/index.cjs. This is the dual-package pattern — a single npm package serving both module systems cleanly. That makes B the correct answer.
A is wrong because "type": "module" at the package level only sets the default interpretation of .js files within that package — it does not override or collapse the conditional exports logic. The exports field takes precedence for entry-point resolution regardless of "type".
C reverses the file mappings entirely. There is no "extension conversion" mechanism in Node.js that swaps .js for .cjs or vice versa during import resolution. The conditions are explicit and matched directly.
D reflects a common outdated misconception. Publishing both ESM and CJS from one package is not only possible — it's a well-established best practice called a "dual package," and conditional exports exist specifically to enable it.
A useful rule of thumb: whenever you see "import" and "require" as sibling keys inside exports, you're looking at a dual-package setup where each module system gets its own dedicated file.
- Why is the bundler generally more likely to remove unused functions from the ES module version?
Answer: Static import and export syntax makes the dependency structure easier to analyze without executing the modules.
When you see a question about bundlers and "tree shaking," the core concept being tested is static analysis — a bundler's ability to determine which code is actually used before running the program.
ES modules use import and export keywords that are statically declared, meaning the dependency graph is fixed at the time you write the code. A bundler can read your files, trace exactly which exports are imported where, and safely eliminate anything that's never referenced — without ever executing the code. This is why D is correct: the static nature of the syntax is what enables reliable dead-code elimination, not anything about runtime behavior.
A is wrong because it conflates syntax with runtime size. Named exports aren't inherently smaller than object properties — the performance difference is about what the bundler can remove, not how individual functions are stored in memory.
B is wrong because ES modules make no such guarantee about side effects. A module can absolutely have side effects (like modifying globalThis), which is actually why bundlers respect a "sideEffects" field in package.json — they can't assume safety automatically.
C is wrong on two counts: require does not reload modules on every call (Node.js caches them after the first load), and the real issue with CommonJS is that require can be called dynamically (e.g., require(someVariable)), making static analysis impossible — not that it reloads.
As a study tip: when you see tree shaking questions, the keyword is always static. If the import/export structure can be determined without running the code, a bundler can eliminate dead code safely.
- Which statement correctly describes the behavior?
Answer: The log is "0 1", and assigning 10 fails because the imported binding is read-only locally.
When working with ES modules in JavaScript, the critical concept to understand is live bindings — imported names are not copies of values, but live, read-only references into the exporting module's scope.
Here's what actually happens in main.mjs: count starts at 0, so before captures 0. After increment() runs, the exporting module's count becomes 1. Because the import is a live binding, count in main.mjs reflects that updated value immediately — making the log "0 1". That confirms B is correct. The second part of B is also true: if you write count = 10 in the importing module, JavaScript throws a TypeError at runtime (or a syntax error depending on the environment) because imported bindings are read-only on the consumer side. Only the exporting module can mutate its own exported variable.
A describes how require() in CommonJS works — it does copy primitive values at the moment of import, so the log would be "0 0" and reassignment would silently succeed. ES modules behave fundamentally differently, so don't confuse the two systems.
C is wrong about the log value ("1 1" would mean before captured 1, but increment() hadn't run yet when before was assigned). It also incorrectly suggests you can assign through the binding to affect the exporting module — you cannot write to it at all.
D incorrectly claims that assigning to an import creates a shadow variable. No such shadowing occurs; it's simply an error.
Study tip: Anytime you see ES module import questions, ask two things: "Is this a live binding?" (yes) and "Can the importer write to it?" (never).
- What does app.cjs print under normal Node.js module resolution?
Answer: It prints "1 2" because both require calls share the cached state module object.
When you see a question involving multiple require() calls to the same module, the key concept to focus on is CommonJS module caching. Node.js executes a module's code exactly once, then stores the resulting exports in a cache. Every subsequent require() of that same path returns the same cached object — not a fresh copy.
Here's what actually happens: state.cjs exports the object { n: 0 }. This object is cached after its first load. When first.cjs is required, it gets a reference to that same object and increments n to 1, then exports 1. When second.cjs is required next, it also gets a reference to the same cached object (where n is now 1), increments it to 2, and exports 2. So app.cjs prints 1 2 — confirming C is correct.
A is wrong because it assumes each require("./state.cjs") produces a fresh object. CommonJS caching means all modules share one instance — that's the whole point of the cache. B is wrong because it imagines the exports are "frozen" at 0 before the increment runs; in reality, ++state.n executes eagerly when the module is first loaded. D is wrong because Node.js resolves require() calls in the order they appear in source code — left to right — so first.cjs always runs before second.cjs.
As a study tip: whenever you see shared state across modules in a CommonJS question, ask yourself "is this the same cached object?" If the same file is required twice anywhere in the dependency tree, the answer is almost certainly yes.
- Which replacement most robustly meets these requirements?
Answer: Convert import.meta.url with fileURLToPath, then pass the resulting filename to path.dirname.
When migrating from CommonJS to ES modules in Node.js, you lose access to globals like __dirname and __filename. The replacement requires two steps: extracting a file path from import.meta.url (which gives you a file:// URL, not a plain path), then deriving the directory from that path.
The Node.js built-in fileURLToPath from the url module is purpose-built for this conversion. It correctly handles URL encoding (e.g., %20 for spaces), cross-platform differences (Windows drive letters, backslashes), and edge cases that naive string manipulation misses. Once you have the absolute file path, passing it to path.dirname gives you the directory — exactly what __dirname used to provide. That makes D the correct and most robust approach.
A is wrong because process.cwd() returns the current working directory — wherever the Node.js process was launched — not the directory of the module file itself. These differ whenever you run the script from a different folder, making path resolution fragile.
B is tempting but dangerous. Simply stripping file:// with a string replace doesn't handle URL-encoded characters (a space encoded as %20 would remain in the path, breaking file lookups) or Windows paths (which look like file:///C:/...). This is exactly the kind of manual handling fileURLToPath exists to prevent.
C is entirely fabricated. module.exports.filename is not a valid property in ES modules (which don't use module.exports at all), so this would throw a reference error.
As a study tip: whenever you see a question about ES module path resolution, remember that import.meta.url is always a URL — you must convert it with fileURLToPath before treating it as a filesystem path.
- Which change most directly allows Node.js to run this file as intended?
Answer: Rename app.js to app.cjs while leaving its CommonJS statements unchanged.
When Node.js sees "type": "module" in package.json, it treats every .js file in that package as an ES module. ES modules don't support require() — that's CommonJS syntax — so running app.js as-is will throw a require is not defined error. The question tests whether you know how to override the package-level module type for a single file without changing the package.json setting.
Node.js uses file extensions to resolve this conflict. Renaming the file to app.cjs signals to Node.js that this specific file should always be treated as CommonJS, regardless of what "type": "module" says in package.json. The .cjs extension is essentially a hard override. This makes A the correct answer — the require() call works, config.mode is logged, and the package's ES module setting is preserved everywhere else.
B is wrong because "use strict" enables strict mode; it has nothing to do with the module system. It won't make require() available in an ES module context. C goes in the wrong direction entirely — the .mjs extension forces Node.js to treat the file as an ES module, which would make the require() call fail even harder. D is a red herring; wrapping require() in an async function doesn't change how Node.js interprets the module format. Dynamic import() is the async ES module alternative, but that would require rewriting the code, not just wrapping it.
A handy rule to remember: .cjs = always CommonJS, .mjs = always ES module, .js = inherits from the nearest package.json "type" field.
- Which implementation best satisfies the requirement while remaining valid ES module code?
Answer: Await import("./formatter.js") inside the if statement, then use the returned module namespace.
When working with ES modules, it's crucial to distinguish between static imports (resolved at parse time) and dynamic imports (resolved at runtime). This question tests whether you understand how to defer module loading until it's actually needed.
The key requirement here is conditional, lazy loading — formatter.js should only initialize when shouldFormat is true. The tool for this in ES modules is the dynamic import() function, which returns a Promise and can live anywhere in your code, including inside conditionals. That makes C the correct choice: await import("./formatter.js") inside the if block loads the module on demand, giving you a module namespace object from which you can access format. This satisfies both the performance requirement and ES module syntax rules.
A is a common trap — static import declarations are always hoisted to the top of the module graph regardless of where you physically write them. Placing one inside an if statement is actually a syntax error in ES modules; the spec explicitly forbids it.
B reaches for require(), which is CommonJS syntax. ES modules do not support require() — mixing them this way will throw a runtime error unless you're in a special compatibility layer, which isn't assumed here.
D sounds reasonable, but it defeats the entire purpose. A static top-level import loads and executes formatter.js unconditionally at startup, triggering the unwanted initialization side effect even when shouldFormat is false.
As a study tip: whenever a question mentions "load only when needed" in an ES module context, immediately think dynamic import() — it's the only spec-compliant way to achieve true conditional or lazy loading.