Navigate ESM and CommonJS, interop between them, and structure imports so bundlers can tree-shake. Use when hitting module-resolution errors, mixing ESM and CJS, or shrinking a bundle.
Installs into .claude/skills of the current project.
Are you the author of Js Modules?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/amey-thakur-js-modules)
---
name: js-modules
description: Navigate ESM and CommonJS, interop between them, and structure imports so bundlers can tree-shake. Use when hitting module-resolution errors, mixing ESM and CJS, or shrinking a bundle.
---
# JS modules
The JavaScript module story is two systems (ESM and CommonJS) that mostly
interoperate and occasionally do not, and most "cannot use import outside
a module" and "require is not defined" errors come from a mismatch nobody
configured on purpose. Get the model straight and the errors resolve.
## Method
1. **Default to ESM (`import`/`export`).** It is the standard, statically
analyzable (which enables tree-shaking), and what the ecosystem is
converging on. Signal it with `"type": "module"` in package.json (or
`.mjs` files). Reserve CommonJS (`require`/`module.exports`) for
legacy code and tools that still need it.
2. **Understand the interop asymmetry.** ESM can import CommonJS (the
`module.exports` becomes the default import), but CommonJS cannot
`require` an ESM module synchronously (it needs dynamic `import()`).
This one-way friction is behind most dual-system pain; know which
direction you are crossing (see tsconfig-mastery for making the
compiler agree).
3. **Match module settings across the toolchain.** package.json `type`,
tsconfig `module`/`moduleResolution`, and the bundler must agree. A
`.ts` file compiled as CJS but run in an ESM package, or `import`s
without extensions under `NodeNext`, produce the classic resolution
errors. Set them once, consistently, for your runtime.
4. **Structure imports for tree-shaking.** Import named exports from
modules that have no side effects at import time, and mark your package
`"sideEffects": false` (or list the files that do) so bundlers can drop
unused code (see bundle-size). Deep-import from large libraries
(`import x from "lib/x"`) or use ones with proper ESM exports; a
barrel file that re-exports everything can defeat tree-shaking and slow
cold starts.
5. **Use dynamic `import()` for code-splitting and lazy loading.**
`await import("./heavy")` loads a module on demand, splitting it into
its own chunk: the mechanism behind route-level and feature-level lazy
loading (see lazy-loading, bundle-size). It also bridges CJS
to ESM where needed.
6. **Publish libraries dual-format deliberately.** If others consume your
package, provide both ESM and CJS builds via the `exports` map
(conditional `import`/`require` entries) so both kinds of consumer
work, and pin the types entry (see npm-publishing). Getting the
`exports` map wrong is the top reason a published package "cannot be
imported".
## Boundaries
- Module format is a build and runtime concern; it does not change your
code's logic, only whether the toolchain can resolve and optimize it.
- Barrel files (`index.ts` re-exporting a directory) trade import
convenience for bundler and cold-start cost; use them at true public
API boundaries, not for every internal folder (see
python-project-structure's analogous `__init__` rule).
- The ESM/CJS transition is ongoing; some dependencies are ESM-only or
CJS-only, occasionally forcing dynamic import or a build step: check a
dependency's format before adopting it in a mismatched project.