A JavaScript file is plain text with no header and no fixed layout. The language standard, ECMA-262, says what text is valid and what it means. It does not say how a file is stored or named: that is left to the host, the browser or runtime that loads the file.
Source text
To the language, a program is a sequence of Unicode code points (ECMA-262, Source Text). Any code point from U+0000 to U+10FFFF may appear where the grammar allows it, including lone surrogates. No normalisation is applied, so two identifiers that look the same but use different code points are different names.
The standard defines four types of source code (Types of Source Code):
| Type | What it is | Can be a whole file |
|---|---|---|
| Global code | Source text parsed as a Script, excluding the bodies of functions in it | Yes |
| Module code | Source text parsed as a Module, excluding the bodies of functions in it | Yes |
| Function code | The parameters and body of a function | No |
| Eval code | The text passed to eval | No |
Since a file is one Script or one Module, the goal symbol the
parser starts from decides what the text may contain.
Script:
[ScriptBody]
ScriptBody:
StatementList
Module:
[ModuleBody]
ModuleBody:
ModuleItemList
ModuleItemList:
ModuleItem
ModuleItemList ModuleItem
ModuleItem:
ImportDeclaration
ExportDeclaration
StatementListItem
Both bodies are optional, so an empty file is a valid script and a valid module.
Scripts and modules
The same text can be valid as both and mean different things. Nothing in the file says which it is, unless it uses syntax only one goal allows.
| Feature | Script | Module |
|---|---|---|
import and export declarations | Syntax error | Allowed at the top level |
import() expressions | Allowed | Allowed |
import.meta | Syntax error | Allowed |
| Strict mode | Only with a "use strict" directive | Always on |
Top-level this | The global object | undefined |
Top-level var and functions | Become properties of the global object | Local to the module |
Top-level let, const and classes | Shared between all scripts of a realm, but not properties of the global object | Local to the module |
Top-level await | Not allowed; await is an ordinary identifier | Allowed |
| HTML-like comments | Allowed where Annex B applies | Syntax error |
| Evaluated | Each time it is loaded | Once per module map entry, even if imported many times |
The Modules page covers
import, export and how modules are linked. The
Variables and scope page
covers the global object.
How a browser chooses
A web page says which goal to use with the type attribute of
<script>:
| Markup | Parsed as | Notes |
|---|---|---|
<script src="a.js"></script> | Script | Blocks parsing of the page unless it has defer or async |
<script type="module" src="a.js"></script> | Module | Deferred by default; fetched with CORS |
<script nomodule src="a.js"></script> | Script | Skipped by browsers that support modules, for older browsers |
<script type="importmap"> | Not JavaScript | A JSON map from module specifiers to URLs |
new Worker(url, { type: 'module' }) | Module | Without the option, a worker is a script |
A browser takes no notice of the file extension. A module must be served with a
JavaScript MIME type, so text/javascript is the right one for
.js and .mjs alike.
How Node.js chooses
Node.js has its own module system, CommonJS, as well as ES modules. A CommonJS file is parsed as a script wrapped in a function: see CommonJS and interop. Node.js decides between the two for each file:
| File | Nearest package.json | Treated as |
|---|---|---|
.mjs | Any | ES module |
.cjs | Any | CommonJS |
.js | "type": "module" | ES module |
.js | "type": "commonjs" | CommonJS |
.js or no extension | None, or no "type" field | CommonJS, unless syntax detection finds ES module syntax |
- The nearest
package.jsonis the first one found going up from the file’s folder, stopping at anode_modulesfolder or the volume root. - Syntax detection (on by default since Node.js 22.7.0 and
20.19.0) treats ambiguous input as an ES module if it would throw when evaluated as
CommonJS. That covers
importandexportstatements,import.meta, top-levelawait, and lexical redeclarations ofrequire,module,exports,__dirnameor__filename. Animport()call does not count, as it is valid in CommonJS. - The Node.js documentation advises package authors to always write the
"type"field, even for all-CommonJS packages.
{
"name": "example",
"type": "module",
"exports": "./index.js"
}
package.json, every .js file in the package is an ES module; use .cjs for any CommonJS fileFile extensions and media type
| Extension | Meaning |
|---|---|
.js | JavaScript source. Script or module depends on the host, as above |
.mjs | An ES module. A convention from Node.js, also recognised by servers and editors |
.cjs | A CommonJS module, in Node.js |
The media type is text/javascript (RFC 9239, May 2022), which also lists
.js and .mjs. Older names such as
application/javascript are now marked obsolete.
The hashbang line
A file meant to run directly on Unix-like systems may start with a line that names its interpreter. ECMA-262 has allowed this since ES2023 as a Hashbang Comment.
HashbangComment::
#! [SingleLineCommentChars]
#!/usr/bin/env node
console.log('run directly from the shell');
- The
#!must be the very first two code points of the source text. Nothing may come before it, not even white space or a byte order mark. - The line is discarded like any other comment, up to the first line terminator.
- It is allowed at the start of a script and of a module only. A
#!anywhere else is a syntax error, andnew Functiondoes not accept one.
Encoding and the byte order mark
ECMA-262 treats the text as code points, and says the encoding used to store it is not relevant to the language. In practice the encoding is UTF-8, and RFC 9239 gives these rules for files served with the JavaScript media type:
- A leading byte order mark takes precedence:
EF BB BFis UTF-8,FF FEis UTF-16 little-endian, andFE FFis UTF-16 big-endian. - Otherwise a valid
charsetparameter is used, for scripts only. - Otherwise the text is assumed to be UTF-8.
A module must be UTF-8, and other encodings fail. A script should be UTF-8, and others are discouraged. Save every source file as UTF-8.
The byte order mark is U+FEFF (ZWNBSP), which the language classes as
white space, so a leading
one does no harm. It does stop a hashbang from being recognised, because #!
must come first.
Directives
A directive prologue is the longest run of expression statements, each only a string literal, at the start of a script, a module or a function body (Directive Prologues and the Use Strict Directive). Each one is a directive.
'use strict'; // a Use Strict Directive
'custom directive'; // another directive: ignored by the language
function f() {
"use strict"; // applies to the function only
}
- A Use Strict Directive is a directive whose text is exactly
use strict, written"use strict"or'use strict'with no escape sequences and no line continuation. Upper case does not count. - Other strings in the prologue are valid and do nothing to the language. Some tools give them meaning.
- A string that comes after any other statement is not a directive, just an expression statement with no effect.
- A function with a
"use strict"directive may not have non-simple parameters, that is, default values, rest parameters or destructuring. It is a syntax error since ES2016. - Module code and the code of class definitions are always strict, with or without the directive.
The Strict mode page lists everything the directive changes.
Typical layout
There are no sections in the file format, but files usually follow this order:
- An optional hashbang.
- An optional licence comment. Minifiers keep comments starting
/*!by convention. - Any directives.
- In a module, the
importdeclarations. They may appear anywhere at the top level and are hoisted, but are normally written first. - Declarations and statements, with
exporton the declarations or in a list at the end.
#!/usr/bin/env node
/*! A small example. MIT licence. */
import { readFile } from 'node:fs/promises';
export async function wordCount(path) {
const text = await readFile(path, 'utf8');
return text.split(/\s+/).length;
}
console.log(await wordCount(process.argv[2]));
await