A CJS-only package blocks ESM consumers entirely — they cannot use static import syntax and must resort to dynamic import() or createRequire workarounds. ESM-only packages break all CommonJS consumers (many Node.js scripts, Jest without experimental VM modules, older tools). ISO 25010 interoperability requires supporting both resolution systems until the ecosystem fully migrates. SSDF PS.3.2 flags format mismatches as a distribution integrity concern: the format published must match what consumers expect given the type field and exports conditions.
High because CJS-only packages are incompatible with ESM-native bundlers and Node.js projects using `type: "module"`, and ESM-only packages break all CJS consumers.
Configure your build tool to emit both ESM and CJS outputs and map them in package.json exports.
// tsup.config.ts — dual format output:
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
splitting: false,
clean: true
})
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"type": "module"
}
If you commit to ESM-only, set "type": "module" and document the minimum Node.js version (18+) in the engines field and README.
ID: sdk-package-quality.build-distribution.dual-format
Severity: high
What to look for: List all output formats produced by the build. For each format, check the build output and configuration for module format:
package.json type field: "module" (ESM default) or "commonjs" (CJS default) or absent (CJS default)exports field conditions: presence of both import and require conditions indicates dual formatformat options in tsup, rollup, or esbuild for ['esm', 'cjs'].mjs (ESM), .cjs (CJS), .js (depends on type field)type: "module" is set and there's a clear rationale (e.g., Node.js 18+ minimum)Pass criteria: The package provides BOTH ESM and CJS output formats (dual format), OR the package is explicitly ESM-only with "type": "module" in package.json and the README or engines field indicates the minimum Node.js version requirement — at least 2 formats (ESM and CJS) must be published for broad compatibility. Report: "X output formats found."
Fail criteria: The package provides only CJS without ESM (blocking ESM-only consumers), OR the build output format doesn't match the type field and exports conditions (e.g., .js files that are actually ESM but type is not "module").
Skip (N/A) when: Python (uses wheels/sdist), Rust (cargo handles this), Go (source distribution), or browser-only package that only ships a UMD/IIFE bundle.
Cross-reference: The exports-field check verifies these formats are correctly mapped in package.json exports.
Detail on fail: "Package ships only CommonJS (no 'type' field, single .js output). ESM consumers using 'import' syntax will get ERR_REQUIRE_ESM or need dynamic import() workarounds. Add ESM output or set type: 'module'."
Remediation: Most modern tools expect ESM. Dual format (ESM + CJS) gives maximum compatibility.
// tsup.config.ts — dual format:
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
splitting: false,
clean: true
})
// package.json — dual format exports:
{
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}