Skip to content

Latest commit

 

History

History
175 lines (148 loc) · 5.87 KB

File metadata and controls

175 lines (148 loc) · 5.87 KB

Exports Generation

This guide shows a simple before/after flow when using duel --exports to emit package.json exports.

Tip

Convention over Configuration

The --exports option is designed to be zero-config. It determines your public API by scanning your build output and applying standard Node.js patterns. It assumes that your directory structure reflects your intended module boundaries. If you need to hide specific files or create complex custom mappings, you should manage the exports field manually; duel is built to handle the 90% of use cases that follow standard project layouts without requiring a separate configuration file.

Scenario

  • package.json has "type": "module" and no exports field.
  • tsconfig.json uses outDir: "dist".
  • Running duel produces ESM in dist and CJS in dist/cjs.

Example layout (source tree):

  • src/index.ts
  • src/components/button.ts
  • src/components/card.ts
  • src/utils/math/add.ts
  • src/utils/math/subtract.ts

Before

{
  "name": "my-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}

After: duel --exports name

Keys stay extensionless; targets keep explicit extensions. Values are concrete (no wildcards) because each file gets its own subpath. The subpath key is derived from the file name (via path.parse().name), not its directory path.

Warning

If two files share the same basename (e.g., foo.ts in different folders), they collide on that subpath: the later file discovered by the glob pass overwrites the earlier one.

{
  "name": "my-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/cjs/index.cjs",
      "default": "./dist/index.js"
    },
    "./index": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/cjs/index.cjs",
      "default": "./dist/index.js"
    },
    "./button": {
      "types": "./dist/components/button.d.ts",
      "import": "./dist/components/button.js",
      "require": "./dist/cjs/components/button.cjs",
      "default": "./dist/components/button.js"
    },
    "./card": {
      "types": "./dist/components/card.d.ts",
      "import": "./dist/components/card.js",
      "require": "./dist/cjs/components/card.cjs",
      "default": "./dist/components/card.js"
    },
    "./add": {
      "types": "./dist/utils/math/add.d.ts",
      "import": "./dist/utils/math/add.js",
      "require": "./dist/cjs/utils/math/add.cjs",
      "default": "./dist/utils/math/add.js"
    },
    "./subtract": {
      "types": "./dist/utils/math/subtract.d.ts",
      "import": "./dist/utils/math/subtract.js",
      "require": "./dist/cjs/utils/math/subtract.cjs",
      "default": "./dist/utils/math/subtract.js"
    }
  }
}

After: duel --exports dir

Directory-based keys are emitted with a trailing /*; values are wildcarded to cover all files under that directory.

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/cjs/index.cjs",
      "default": "./dist/index.js"
    },
    "./components/*": {
      "types": "./dist/components/*.d.ts",
      "import": "./dist/components/*.js",
      "require": "./dist/cjs/components/*.cjs",
      "default": "./dist/components/*.js"
    },
    "./math/*": {
      "types": "./dist/utils/math/*.d.ts",
      "import": "./dist/utils/math/*.js",
      "require": "./dist/cjs/utils/math/*.cjs",
      "default": "./dist/utils/math/*.js"
    }
  }
}

After: duel --exports wildcard

Wildcard keys use the first path segment and cover folders; values are wildcarded to match all files in that segment. With the same layout as above, keys group by the first directory (components, utils) instead of the deepest one.

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/cjs/index.cjs",
      "default": "./dist/index.js"
    },
    "./components/*": {
      "types": "./dist/components/*.d.ts",
      "import": "./dist/components/*.js",
      "require": "./dist/cjs/components/*.cjs",
      "default": "./dist/components/*.js"
    },
    "./utils/*": {
      "types": "./dist/utils/*.d.ts",
      "import": "./dist/utils/*.js",
      "require": "./dist/cjs/utils/*.cjs",
      "default": "./dist/utils/*.js"
    }
  }
}

Notes

  • Keys are extensionless to keep the public API stable; targets carry .js/.cjs/.d.ts so Node resolution stays explicit.
  • For dir/wildcard, both keys and values use wildcards (./dir/* -> ./dist/dir/*.js etc.).
  • The root . entry uses your main (if set) to pick the default orientation (import vs require) and mirrors both builds when present.
  • If main is absent and no non-wildcard subpath exists, . is not promoted.
  • Windows paths are normalized with path.posix.

Exports config (JSON)

If you want to constrain which built files become exports while keeping a conventional layout, pass --exports-config <file>. The file must be JSON with this shape:

{
  "entries": ["./dist/index.js", "./dist/folder/module.js"],
  "main": "./dist/index.js"
}
  • entries (required): array of strings pointing to emitted files (relative with ./). Only these bases are exported.
  • main (optional): overrides the main used for the root . entry and default orientation.

Convention over configuration remains the default: if you omit --exports-config, duel scans the output and infers exports automatically.

Validation-only

Use --exports-validate to compute and validate the exports map without writing package.json. Combine with --exports and/or --exports-config to emit after validation. When run alone, it logs success and leaves your package.json untouched.