Frontend Development 12 min read

Master Dynamic Entry Points in Node Packages: main, type, exports & module

This article explains how third‑party library authors can configure package.json fields such as main, type, exports, and module to provide dynamic entry points for both CommonJS and ESModule consumers, while also covering TypeScript's resolution strategy and practical code examples.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
Master Dynamic Entry Points in Node Packages: main, type, exports & module

Introduction

Third‑party library authors need to write appropriate entry files to achieve "dynamic" imports, which also helps bundlers eliminate unused code and reduce bundle size.

main

The

main

field in

package.json

is the most common way to specify the entry file.

<code>{
  "name": "@homy/test-entry",
  "version": "1.0.0",
  "description": "",
  "main": "index.js"
}</code>

When a developer imports

@homy/test-entry

, the entry file resolved is

index.js

.

type

The

type

field indicates whether the package should be treated as

commonjs

or

module

. If

type: "module"

is set,

.js

files are interpreted as ESModules; otherwise they are treated as CommonJS.

<code>{
  "name": "@homy/test-entry",
  "version": "1.0.0",
  "type": "commonjs", // or "module", default is commonjs
  "main": "index.js"
}</code>
type: module works only on Node.js >= 14 and requires import ; require is not supported.

exports

The

exports

field is a more powerful replacement for

main

. It can specify different entry points for

import

(ESM) and

require

(CJS), and provides a fallback with

default

.

<code>{
  "name": "@homy/test-entry",
  "main": "index.js",
  "exports": {
    "import": "./index.mjs",
    "require": "./index.cjs",
    "default": "./index.mjs"
  }
}</code>

If a subpath is accessed that is not defined in

exports

, Node throws an error:

<code>const pkg = require('@homy/test-entry/test.js'); // Error! Package subpath './test.js' is not defined by "exports"
</code>

Submodule configuration can be added by defining additional keys:

<code>{
  "exports": {
    ".": "./index.mjs",
    "./mobile": "./mobile.mjs",
    "./pc": "./pc.mjs"
  }
}

// or more detailed
{
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.cjs",
      "default": "./index.mjs"
    },
    "./mobile": {
      "import": "./mobile.mjs",
      "require": "./mobile.cjs",
      "default": "./mobile.mjs"
    }
  }
}</code>

The

imports

field (similar to import maps) can control resolution of non‑relative imports, but in Node it must be prefixed with

#

.

module

Many bundlers (webpack, rollup, esbuild) also support the

module

field to specify an ESModule entry point, which aids tree‑shaking.

<code>{
  "name": "@homy/test-entry",
  "module": "index.mjs"
}</code>

TypeScript entry files

TypeScript first checks the

main

field, then looks for a corresponding declaration file (

lib/index.d.ts

). If a

types

(or

typings

) field is present, it is used instead of

main

for type resolution.

<code>{
  "name": "my-package",
  "type": "module",
  "exports": {
    ".": {
      "types": "./types/index.d.ts",
      "import": "./esm/index.js",
      "require": "./commonjs/index.cjs"
    }
  },
  "main": "./commonjs/index.cjs",
  "types": "./types/index.d.ts"
}</code>

In

tsconfig.json

, the

moduleResolution

option can be

classic

(default) or

node

, affecting how relative and non‑relative imports are resolved.

Relative imports search the current directory for

.ts

or

.d.ts

files, while non‑relative imports walk up the directory tree and finally look into

node_modules

, also considering

@types

packages.

<code>// relative import example
import { b } from "./moduleB";
// Node resolves to /root/src/folder/moduleB.ts (or .d.ts)

// non‑relative import example
import { b } from "moduleB";
// Node searches upward and then /node_modules/moduleB for .js/.ts/.d.ts etc.
</code>

Summary

Node uses

main

and

type

to specify the entry file and its module format;

exports

provides a more flexible alternative.

Bundlers add support for the

module

field to enable top‑level ESM entry points.

TypeScript prefers the

types

field, falling back to

main

and then searching for matching declaration files.

TypeScriptNode.jspackage.jsonCommonJSESMexportsmodule resolution
Taobao Frontend Technology
Written by

Taobao Frontend Technology

The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.