The origins of the CommonJS module system
CommonJS modules have been a foundational part of JavaScript, especially in Node.js, since before ES modules (ESM) were officially adopted. Designed to enable modular and maintainable code, CommonJS allows developers to import and export functionality across files using require
and module.exports
. This approach was crucial in shaping the JavaScript ecosystem, providing a server-side module system that many developers grew accustomed to.
The future of CommonJS
However, with the rise of ESM in modern JavaScript and TypeScript, CommonJS is becoming less dominant as the ecosystem shifts towards the more standardized and natively supported module system. Understanding CommonJS remains important, especially for working with legacy code or packages that haven’t yet transitioned to ESM.
Encapsulation
A file becomes a CommonJS module when it uses the module.exports
or exports
object to export values and the require()
function to import them.
Everything inside a CommonJS module is private by default. This means that the variables, functions, and objects defined within a module are not accessible from other modules unless explicitly exported using module.exports
or exports
.
Synchronous Loading
CommonJS modules are designed to be loaded synchronously. This means that the code within a module is executed immediately upon loading, and further code execution is blocked until loading is done. This synchronous nature is suitable for server-side applications, where file I/O operations are typically fast.
Single Instance
CommonJS modules are cached after the first time they are loaded. Subsequent require
calls for the same module return the cached instance. This helps in avoiding unnecessary re-execution of the module’s code.
const privateVariable = "I am private"; function privateFunction() { console.log("This is a private function"); } // Exporting public functionality module.exports = { publicVariable: "I am public", publicFunction: function() { console.log("This is a public function"); } };
can be used like that:
const myModule = require('./myModule'); console.log(myModule.publicVariable); myModule.publicFunction();
Static vs Dynamic Loading in CommonJS
CommonJS does support the dynamic loading of modules. However, all CommonJS modules are not loaded dynamically by default.
Static Loading: When you use require()
at the top of your file, it’s a form of static loading. The module is loaded synchronously at the start of the script execution.
const math = require('./math'); console.log(math.add(2, 3));
Dynamic Loading: You can also use require()
within functions or conditional statements, which allows modules to be loaded dynamically, meaning the module is loaded only when the code that calls require()
is executed.
function loadModule(condition) { if (condition) { const math = require('./math'); console.log(math.add(2, 3)); } }
Static Analysis of CommonJS
CommonJS modules can be statically analyzed, but there are some limitations due to the dynamic nature of JavaScript.
- Static Analysis Tools: Tools like ESLint, JSHint, and TypeScript’s compiler can analyze CommonJS code for syntax errors, potential bugs, and style issues. They can also perform some level of dependency analysis by following
require()
calls. - Module Bundlers: Tools like Webpack, Browserify, and Rollup can perform static analysis to bundle CommonJS modules for the browser. They analyze the module dependencies and include only the necessary code.
Limitations
- Dynamic
require()
Calls: Ifrequire()
is used dynamically (e.g., inside functions or based on runtime conditions), it can be challenging for static analysis tools to fully understand the module dependencies. - Non-Standard Patterns: Any non-standard patterns or usage of CommonJS can complicate static analysis. For example, modifying
module.exports
in unusual ways or using conditional logic to determine what to export.
Despite these challenges, static analysis is a powerful tool for improving code quality and identifying issues in CommonJS modules. The key is to use tools that are aware of CommonJS conventions and to write code in a way that facilitates analysis.
Execution Order
Incomplete Exports: If Module A
requires Module B
before Module B
has finished loading, Module A
will receive an incomplete version of Module B
‘s exports (and vice versa). This means that any exports from Module B
up to the point where it required Module A
will be available, but not anything defined after that point.
Loading Modules: Node.js uses a synchronous, depth-first algorithm to load modules. When a module is required, Node.js will load it and its dependencies.
// a.js console.log('Loading A'); exports.a = 'A'; const b = require('./b'); console.log('In A, B:', b); exports.completeA = 'Complete A'; // b.js console.log('Loading B'); exports.b = 'B'; const a = require('./a'); console.log('In B, A:', a); exports.completeB = 'Complete B'; // main.js const a = require('./a'); const b = require('./b'); console.log('Main, A:', a); console.log('Main, B:', b);
The output is:
Loading A Loading B In B, A: { a: 'A' } In A, B: { b: 'B', completeB: 'Complete B' } Main, A: { a: 'A', completeA: 'Complete A' } Main, B: { b: 'B', completeB: 'Complete B' }
Key Points
- Loading Order Matters: The order in which modules require each other affects the state of the exports at any given time.
- Incomplete Export Objects: Modules will receive partially constructed export objects if they’re required before the dependent module has finished loading.
CommonJS in TypeScript
When working with TypeScript, developers often encounter CommonJS modules, especially when integrating with legacy Node.js projects or third-party libraries that haven’t adopted ES modules (ESM). TypeScript provides robust support for CommonJS through configuration options like module: "commonjs"
in the tsconfig.json
file. This allows TypeScript to compile code that uses require
and module.exports
while also supporting type-checking and modern language features. However, using CommonJS in TypeScript can introduce friction when transitioning to ESM or when dealing with interoperability, making it important for developers to be mindful of how modules are structured and imported.
// Export export = someFunction; // Import import someFunction = require('./someModule');
Does TypeScript use the “require” import syntax?
No, TypeScript does not use the require
import syntax natively. Instead, TypeScript follows the modern ES module syntax, using import
and export
statements. However, when targeting CommonJS in the tsconfig.json
(using module: "commonjs"
), TypeScript compiles the code into JavaScript that uses require
and module.exports
under the hood.
For working with CommonJS modules in TypeScript, you may sometimes see require
used, but this is generally for compatibility purposes, such as importing a CommonJS module that doesn’t have default ESM support. In such cases, TypeScript allows you to use const module = require('module-name')
, although the preferred approach is to use the ES module syntax wherever possible.
Setting up TypeScript with CommonJS for Node.js
// tsconfig.json { "compilerOptions": { "target": "ES2020", "module": "CommonJS", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] }
"target": "ES2020"
: Specifies the ECMAScript version to target."module": "CommonJS"
: Specifies the module system to use (CommonJS is the default for Node.js).
Create a simple TypeScript file in the src
directory:
// src/index.ts const greeting: string = 'Hello, Node.js!'; console.log(greeting);
Can I use CommonJS in the browser?
Yes, but you usually should not do that. The trend in frontend development is towards using native ES6 modules and more modern tools like Webpack with Babel to transpile code, rather than relying on CommonJS directly. That being said, while CommonJS modules were originally designed for server-side JavaScript (Node.js), they can also be used on the frontend, especially in scenarios where bundlers or transpilers are employed to make them compatible with browsers. However, there are some considerations to keep in mind:
- Compatibility:
- Browsers do not natively support CommonJS modules. If you want to use CommonJS modules in the frontend, you need a tool like Browserify or Webpack, which can bundle your CommonJS-style code into a format that browsers can understand.
- Synchronous Loading:
- CommonJS modules are designed for synchronous loading, which may not be suitable for all frontend scenarios. Browsers typically prefer asynchronous loading to avoid blocking the rendering of the page. As a result, bundlers often transform CommonJS code to work asynchronously on the frontend.
- Bundle Size:
- CommonJS modules, when used directly in the frontend, might contribute to larger bundle sizes. This is because the bundler needs to include not only the code but also the CommonJS runtime, which can add overhead.
- ES6 Modules:
- With the widespread adoption of ECMAScript 6 (ES6) modules, many frontend developers prefer using the native module system supported by browsers. ES6 modules offer a more standardized and flexible module syntax that is natively supported by modern browsers.