There's the pervarsive notion that all JS is created equal and that there's only minor and easily detectable differences between the various file formats used to author JavaScript. This is correct, from a certain point of view.
For many people writing JavaScript that gets passed into build tools, the differences can be ignored since they get compiled away. This is possible because there are parsers that accept all valid files written in the various formats. Unfortunately it's very unlikely that such a parser rejects invalid files. Which means the build tool understands the file and can transform it but no JavaScript engine would be able to run the source file directly. For example TypeScript will happily accept the following:
import x from 'x'; // only valid in modules
const await = x; // await is a reserved keyword in modules
The above is not a valid file in any JavaScript engine I know of.
It's accepted as valid by Typescript because each of the lines in isolation is
valid in some JavaScript file format.
It's a hard problem to find these conflicts once the decision is made to
treat all kinds of JavaScript input as "basically the same thing".
When compiling the import
into a require
the problem goes away
since the resulting file isn't actually a module.
For people compiling their TypeScript to CJS there's no issue.
- File extensions:
.js
. - Mime Type:
text/javascript
(among others).
Scripts are the OG JavaScript. It's what many people and tools still assume when they are handed "generic JavaScript". Even very recent APIs like service workers default to treating their URLs as classic scripts by default.
Scripts are simple. They load. They run. They use globals.
While scripts are super popular when running code in the browser, authoring code as classic scripts is far less typical. Scripts are often the output of build tools and not written by hand. This doesn't mean to discount the jQuery module pattern and hand-written IIFEs. They just lost mindshare over the past few years.
- Top level
this
isglobalThis
. - Top level
'use strict'
switches to strict mode for the current file only, default is loose mode. There's some finer details around concatenated and inline scripts. - Top level declarations are globals.
await
is a valid identifier outside of async functions.await
expressions in top level code are a syntax error.- HTML comments are valid as a line comments.
import
/export
statements are a syntax error.import.meta
expressions are a syntax error.
Files that are only valid as Classic Scripts or CommonJS:
// Syntax error in Module
<!-- HTML comment time! -->
// Syntax error in Module
const await = 42;
I'm not aware of files that are syntactically valid Classic Scripts but would cause a syntax error in CommonJS. Those differences only surface at runtime:
// Sets a global in script, sets a property on `exports` in CommonJS
this.x = 42;
(function () { 'use strict';
console.log(x); // throws in CommonJS, works in classic script
})();
// Sets a global in script, creates a file-scoped variable in CommonJS
var y = 42;
if (globalThis.y === 42) {
// runs in script but not in CommonJS
}
- File extensions:
.mjs
,.js
. - Mime Type:
text/javascript
(among others).
Modules are the attempt to standardize a proper modern file format for JavaScript code. It deviates from scripts in some ways, most importantly in that it has dependency management built-in and executes the same resource only once.
But since it was a new file format with new syntax, the standards body also took the opportunity to throw in some breaking changes. This allowed adding support for top level await and the removal of HTML comments.
- Top level
this
isundefined
. - Code runs in strict mode by default. Top-level
'use strict'
has no effect. - Top level declarations are scoped to the current module.
await
is a reserved keyword and may not be used as an identifier (syntax error).- Top level code can use
await
(pending proposal). - HTML comments are a syntax error.
import
/export
statements can be used.import.meta
expressions can be used.
Files that are only valid as modules:
// Syntax error in Classic Script and CommonJS
console.log(import.meta);
// Even worse: The same syntax error but only at runtime.
console.log(eval('import.meta'));
// Syntax error in Classic Script and CommonJS
await 42;
// Syntax error in Classic Script and CommonJS
import x from 'x';
export default 42;
- File extensions:
.cjs
,.js
. - MIME type:
application/node
.
The quirks mode of JavaScript. And I say that with loving appreciation. CommonJS doesn't have a formal specification I'm aware of. The only decent definition of CommonJS JavaScript is in terms of its implementation:
JavaScript that can be inserted into the body of a non-strict mode function
that takes certain predetermined arguments (require
, __filename
, etc.).
This leads to a number of unusual properties:
this
is neitherundefined
norglobalThis
but theexports
object.- Top level code has access to variables (
require
,__filename
) that are "implicitly" declared only for the current file. - Top level declarations are "function scoped" (file scoped).
- Execution happens on a JS stack that leaks parent modules.
- Top level code may include
return
statements. - Top level
'use strict'
switches to strict mode for the current file only, default is loose mode. - Otherwise, the behavior matches scripts. E.g. HTML comments are allowed.
This list doesn't cover the following valid CommonJS code:
console.log('never runs');
});
(function () {
console.log('require vanished', typeof require);
I'm not aware of any "true" CommonJS parser that handles this kind of code. But feed it into node and it would run just fine. Quirks mode.
@mathiasbynens Tried to sneak a link into that article but didn't find a non-awkward spot. So I just went with adding
await
to the list of differences.