Fix: publint Not Working — Package Exports Invalid, Types Not Found, or Dual Package Errors
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix publint package validation issues — exports field configuration, dual ESM/CJS packaging, type resolution, main/module/types fields, files array, and common packaging mistakes.
The Problem
publint reports errors on your package:
npx publint
# ✗ "exports['.'].import.types" types is not the first in the object
# ✗ "main" file does not exist
# ✗ "module" field should be ESM but found CJSOr consumers can’t import your package correctly:
import { something } from 'my-package';
// Error: Cannot find module 'my-package' or its corresponding type declarationsOr the package works in one environment but not another:
Works with: import { foo } from 'my-package'
Fails with: const { foo } = require('my-package')Why This Happens
publint checks that your package.json is configured correctly for publishing to npm. Modern packages need to support multiple module systems and provide proper type information:
exportsfield is the modern standard — it replacesmain,module, andtypesfor Node.js 12+. But many tools still read the legacy fields, so you need both.typesmust be first in conditional exports — TypeScript resolves types from the first matching condition. Iftypesisn’t listed beforedefault, TypeScript may not find your type declarations.- ESM and CJS have different file extensions —
.mjsis always ESM,.cjsis always CJS,.jsfollows"type"inpackage.json. Mismatched extensions cause “Cannot use import/require” errors. filescontrols what’s published — ifdist/isn’t in thefilesarray, npm publish excludes it, and consumers get an empty package.
The other half of the story is that the runtime ecosystem keeps fragmenting. Node.js, Bun, and Deno all read the exports field, but each prioritizes conditions slightly differently. Bun follows the Node.js resolution algorithm but also accepts a bun condition, which can shadow the node condition unexpectedly. Deno historically used import and deno conditions and only added full exports support in recent releases — older Deno versions silently fall back to a raw file lookup. The result: a package that passes publint on Node may still fail on one of the other runtimes if you do not declare the right conditions.
The third complication is how the package manager treats the publish lifecycle itself. npm runs prepublishOnly, then prepare, then prepack, then packs and uploads the tarball. pnpm reorders things slightly and tightens what it sees as “the published package” via its own pnpm pack implementation. Yarn (Berry) has its own pipeline with prepack and postpack, and Yarn workspaces apply workspace:* rewrites at publish time. If your dist/ is built during the wrong hook, the tarball will be missing files even though everything looks fine on disk. publint detects the symptom (“file does not exist”) but the root cause is the lifecycle, not the JSON.
Fix 1: Run publint and Fix Errors
npx publint
# Or check online: https://publint.dev/
# Common output:
# ✗ "exports['.'].import.types" types is not the first in the object
# ✗ "exports['.'].require" file does not exist
# ✓ "main" is valid
# ✓ "types" is validFix each error type:
// WRONG — types is not first
{
"exports": {
".": {
"import": {
"default": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
}
}
// CORRECT — types must be first
{
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}
}Fix 2: Correct package.json for Dual ESM/CJS
// package.json — the complete modern setup
{
"name": "my-package",
"version": "1.0.0",
"description": "My awesome package",
"type": "module",
// Legacy fields — for older tools and bundlers
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
// Modern exports — takes precedence over main/module
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./utils": {
"import": {
"types": "./dist/utils.d.ts",
"default": "./dist/utils.js"
},
"require": {
"types": "./dist/utils.d.cts",
"default": "./dist/utils.cjs"
}
},
// Export CSS
"./styles.css": "./dist/styles.css",
// Export package.json (some tools need this)
"./package.json": "./package.json"
},
// What's included in the published package
"files": [
"dist",
"README.md",
"LICENSE"
],
// Side effects — enables tree-shaking
"sideEffects": false,
// Or specify files with side effects:
// "sideEffects": ["./dist/styles.css"]
// Engines
"engines": {
"node": ">=18"
},
// Keywords for npm search
"keywords": ["utility", "typescript"],
// Repository
"repository": {
"type": "git",
"url": "https://github.com/user/my-package"
},
"license": "MIT",
"scripts": {
"build": "tsup",
"lint": "publint && attw --pack",
"prepublishOnly": "npm run build && npm run lint"
}
}Fix 3: ESM-Only Package
// Simpler setup when you don't need CJS support
{
"name": "my-esm-package",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"],
"sideEffects": false
}Fix 4: CJS-Only Package (Legacy)
// For packages that must support older Node.js
{
"name": "my-cjs-package",
"version": "1.0.0",
// No "type": "module" — defaults to CJS
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js"
}
},
"files": ["dist"]
}Fix 5: Validate with Multiple Tools
# publint — checks package.json configuration
npx publint
# arethetypeswrong — checks TypeScript resolution
npx @arethetypeswrong/cli --pack
# Shows how different moduleResolution settings resolve your types
# Are the Types Wrong output:
# ┌───────────────────┬──────────────────┬──────────────────┐
# │ │ node16 (import) │ node16 (require) │
# ├───────────────────┼──────────────────┼──────────────────┤
# │ "my-package" │ OK │ OK │
# │ "my-package/utils"│ OK │ No types │
# └───────────────────┴──────────────────┴──────────────────┘
# npm pack --dry-run — see what will be published
npm pack --dry-run
# Lists all files that will be in the tarball
# Check package size
npx pkg-size
# Or: npx bundlephobia my-package// package.json — validation scripts
{
"scripts": {
"build": "tsup",
"check:exports": "publint",
"check:types": "attw --pack",
"check:size": "pkg-size",
"prerelease": "npm run build && npm run check:exports && npm run check:types"
}
}Fix 6: Common Patterns and Fixes
// React component library
{
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./styles.css": "./dist/styles.css"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"sideEffects": ["*.css"]
}
// CLI tool
{
"type": "module",
"bin": {
"my-cli": "./dist/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["dist"]
}
// Package with subpath exports (e.g., my-pkg/server, my-pkg/client)
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./server": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
},
"./client": {
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
}
}
}Fix 7: Platform-Specific Resolution — Node vs Bun vs Deno
The same exports block resolves differently across runtimes. Declare conditions explicitly so each runtime gets the right file.
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"bun": "./dist/index.bun.js",
"deno": "./dist/index.deno.js",
"workerd": "./dist/index.worker.js",
"browser": "./dist/index.browser.js",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
}
}
}Runtime notes you need to know:
- Node.js picks
importfor ESM callers andrequirefor CJS callers. It ignoresbun,deno, andworkerd. - Bun prefers
bunoverimport. If yourbunentry points at a stale file, Bun consumers silently get the wrong build — Node-targeted CI passes, Bun-targeted CI fails. publint flags missing files but does not catch a wrong-but-existing target. - Deno reads
denofirst, then falls back toimport. Older Deno versions (before full npm specifier support) ignoredexportsentirely and resolved againstmain— pin a Deno minimum if you rely on conditions. - Cloudflare Workers uses the
workerdcondition. Without it, the bundler picksbrowserorimportand can pull in Node-only modules likefs, which fail at build time.
npm vs pnpm vs Yarn publish lifecycle: what actually ends up in the tarball depends on which manager you ran. npm publish runs prepublishOnly then prepack. pnpm publish skips lifecycle scripts unless you pass --no-ignore-scripts and applies publishConfig.directory if set. yarn npm publish (Berry) runs prepack/postpack and rewrites workspace:* ranges to concrete versions. If dist/ is missing from the published tarball, the cause is almost always one of:
# Run the publish in dry-run mode and inspect the tarball contents
npm pack --dry-run
pnpm pack --pack-destination /tmp
yarn pack --dry-run
# pnpm: if you build inside a workspace package, this is required
pnpm publish --no-git-checks --access publicMonorepo (workspaces) vs single package: in pnpm workspaces and Yarn workspaces, you typically run the build at the workspace root via Turborepo or Nx, then publish per package. publint runs against the individual package.json — but the exports paths must resolve relative to the package directory, not the workspace root. A common bug: dist/ lives at the workspace root because the build script writes there, while exports points to ./dist/index.js inside the package. publint says “file does not exist” and the fix is to either move the build output into the package or adjust exports to the workspace path.
ESM-CJS dual export warnings: publint also flags “dual package hazard” — when a consumer can import and require the same package, they get two different module instances. If your package holds state (a cache, a singleton, a class registry), the two copies will not see each other. Either ship ESM-only, or extract the stateful core to a separate package and re-export from both entries.
Still Not Working?
“types is not the first in the object” — in each exports condition block, the types key must come before default, import, or require. TypeScript resolves types from the first matching condition, so types must be first for correct resolution.
“file does not exist” — the file referenced in exports, main, module, or types isn’t in the built output. Run your build command first, then run publint. Check that files in package.json includes the dist/ directory.
Package works with import but not require — the require condition in exports is missing or points to an ESM file. ESM files (.js with "type": "module", or .mjs) can’t be require()d. Generate a .cjs output for CJS consumers.
Types resolve in one moduleResolution but not another — run npx @arethetypeswrong/cli --pack to check all resolution modes. node16 and bundler resolve differently. The types condition in exports fixes most issues. For node16 CJS, you need .d.cts files alongside .cjs files.
Works on Node, broken on Bun or Cloudflare Workers — you are missing a bun or workerd condition, or the matched condition points at a Node-only build. Run bun build --target=bun separately and verify the output works inside a real Bun process. For Workers, use wrangler dev to catch node:fs-style failures before publish.
Tarball is missing dist/ even though files is correct — your build did not run during the publish lifecycle. With pnpm, lifecycle scripts are skipped by default in some configurations — add prepack instead of prepublishOnly, since prepack always runs. With Yarn Berry, ensure dist/ is not gitignored and listed in files, because Berry intersects the two.
Subpath import works in your tests but fails when published — your test runner resolves through the source src/ while consumers resolve through dist/. Add a pnpm pack && npm install ./my-package-1.0.0.tgz step to a sandbox project and re-run the failing import there.
attw passes but a consumer still gets any types — the consumer’s tsconfig.json is set to "moduleResolution": "node" (the old algorithm), which ignores the exports field entirely and falls back to main/types. Tell consumers to switch to "moduleResolution": "bundler" or "node16"/"nodenext", and keep the legacy main/types fields populated as a safety net.
publint passes locally but the published tarball has the wrong files — you ran publint against the source package.json, but npm publish may rewrite paths via publishConfig. Verify the published artifact by running npm view your-package@latest --json and comparing the exports object against your local file. If publishConfig.directory is set, the package root inside the tarball is that directory, not the repo root, and all exports paths must be relative to it.
Tree-shaking does not work for consumers — your sideEffects field is missing or wrong. Set "sideEffects": false for pure libraries. For libraries with CSS imports, list the CSS files explicitly: "sideEffects": ["*.css", "./dist/polyfill.js"]. Bundlers like Rollup, esbuild, and Vite respect this and skip importing unused exports; consumers see smaller bundles.
For related packaging issues, see Fix: tsup Not Working, Fix: Changesets Not Working, Fix: pnpm Peer Dependency Error, and Fix: Turborepo Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Changesets Not Working — Version Not Bumping, Changelog Empty, or GitHub Action Failing
How to fix @changesets/cli issues — changeset creation, version bumping, changelog generation, monorepo support, npm publishing, and GitHub Actions automation.
Fix: tsup Not Working — Build Failing, Types Not Generated, or ESM/CJS Output Wrong
How to fix tsup bundler issues — entry points, dual ESM/CJS output, TypeScript declaration files, external dependencies, tree shaking, and package.json exports configuration.
Fix: unbuild Not Working — Build Output Empty, Stub Mode Failing, or Rollup Errors
How to fix unbuild issues — build configuration, stub mode for development, ESM and CJS output, TypeScript declarations, external dependencies, and monorepo workspace builds.
Fix: Clack Not Working — Prompts Not Displaying, Spinners Stuck, or Cancel Not Handled
How to fix @clack/prompts issues — interactive CLI prompts, spinners, multi-select, confirm dialogs, grouped tasks, cancellation handling, and building CLI tools with beautiful output.