Files with their own scope
A module is one JavaScript file with its own scope. Names made at its top level stay private to that file. export shares chosen names; import uses names another module shares. Here main.js imports from counter.js.
Load and link
You start the program at main.js, with node main.js or a <script type="module"> tag. The engine fetches main.js and parses it, but runs nothing yet. Line 1 says it needs count and add from ./counter.js.
The engine fetches and parses counter.js too, and notes its exports: count and add. step has no export, so it stays private. A module is loaded once and kept, however many files import it.
Before any line runs, the engine links each import in main.js to the binding counter.js exports. main's count is not a copy: it is a link to counter's count. add works already; count is still empty.
Run, dependencies first
A module's imports run before the module itself. So counter.js runs first, top to bottom: count gets 0, step gets 1, and line 6 prints "counter.js runs". Lines 3 to 5 only define add.
Now main.js runs. Line 2 reads count. The name leads through the link to counter's count, which holds 0, so the console prints 0.
add() runs line 4 of counter.js, inside counter's own scope: count = 0 + 1, so count now holds 1. main.js did not change count itself; the module that owns it did.
Live, private, read-only
Line 4 reads count again and prints 1. The import is live: main's count is a link to the same binding, so it shows counter's new value at once. A copy would still say 0.
Line 5 asks for step. main.js has no name called step, because counter.js never exported it. typeof gives "undefined" instead of an error. Private names let a module hide its details.
Line 6 tries count = 5. An import is read-only for the module that imports it, so the engine throws a TypeError, and the program stops. Only counter.js can change count, through add.
What happens
// step 1/10 · files module one file, with its own top-level scope export shares a name with other modules import uses a name another module shares counter.js exports count and add, keeps step main.js imports count and add
Imports are hoisted: the engine reads every import line and links it before the module's first line runs, wherever the import statements are in the file. Module code is always in strict mode. The boxes are a simplification: each module has a module environment record, and main.js's imports are indirect bindings that point at counter.js's own bindings (CreateImportBinding in ECMA-262).