Skip to content

Instantly share code, notes, and snippets.

@jkrems
Last active September 13, 2024 06:14
Show Gist options
  • Save jkrems/b14894e0b8efde10aa10a28c652d3541 to your computer and use it in GitHub Desktop.
Save jkrems/b14894e0b8efde10aa10a28c652d3541 to your computer and use it in GitHub Desktop.
JavaScript: Classic Scripts vs. Modules vs. CommonJS

JavaScript File Format Differences

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.

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.

(Classic) Script

  • 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.

  1. Top level this is globalThis.
  2. 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.
  3. Top level declarations are globals.
  4. await is a valid identifier outside of async functions.
  5. await expressions in top level code are a syntax error.
  6. HTML comments are valid as a line comments.
  7. import/export statements are a syntax error.
  8. 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
}

Module

  • 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.

  1. Top level this is undefined.
  2. Code runs in strict mode by default. Top-level 'use strict' has no effect.
  3. Top level declarations are scoped to the current module.
  4. await is a reserved keyword and may not be used as an identifier (syntax error).
  5. Top level code can use await (pending proposal).
  6. HTML comments are a syntax error.
  7. import/export statements can be used.
  8. 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;

CommonJS (CJS)

  • 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:

  1. this is neither undefined nor globalThis but the exports object.
  2. Top level code has access to variables (require, __filename) that are "implicitly" declared only for the current file.
  3. Top level declarations are "function scoped" (file scoped).
  4. Execution happens on a JS stack that leaks parent modules.
  5. Top level code may include return statements.
  6. Top level 'use strict' switches to strict mode for the current file only, default is loose mode.
  7. 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.

@jkrems
Copy link
Author

jkrems commented Jan 3, 2020

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment