Dual ESM/CJS format or clear ESM-only stance
Why it matters
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.
Severity rationale
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.
Remediation
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.
Detection
-
ID:
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.jsontypefield:"module"(ESM default) or"commonjs"(CJS default) or absent (CJS default)exportsfield conditions: presence of bothimportandrequireconditions indicates dual format- Build tool config: check
formatoptions in tsup, rollup, or esbuild for['esm', 'cjs'] - Output file extensions:
.mjs(ESM),.cjs(CJS),.js(depends ontypefield) - If ESM-only: check that
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"inpackage.jsonand the README orenginesfield 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
typefield andexportsconditions (e.g.,.jsfiles that are actually ESM buttypeis 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-fieldcheck 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" } } }
External references
- iso-25010:2011 · compatibility.interoperability — Interoperability — dual ESM/CJS supports both bundler and Node.js consumers
- ssdf:800-218 · PS.3.2 — Archive and protect each software release — format contracts are part of the release
Taxons
History
- 2026-04-18·v1.0.0·Initial import from sdk-package-quality·automated