-
Notifications
You must be signed in to change notification settings - Fork 256
Description
Bug Description
@near-js/client fails in ESM environments due to missing .js
extensions in import paths. Current build output:
// lib/esm/index.js
export * from './constants'; // Missing .js extension
export * from './crypto'; // Missing .js extension
Error in Vite/other ESM environments:
Error: Cannot find module '@near-js/client/lib/esm/constants' imported from '@near-js/client/lib/esm/index.js'
Technical Context
To understand this bug, it helps to know that JavaScript has two different ways of importing/exporting code between files:
- The older CommonJS (CJS) way:
// Importing
const { thing } = require('./file')
// Exporting
module.exports = { thing }
- The newer ESM way:
// Importing
import { thing } from './file.js' // Note the .js!
// Exporting
export const thing = {}
The key difference here is that ESM requires explicit file extensions (.js
) in import paths, while CommonJS doesn't. This is part of the ESM specification and helps with performance and reliability.
Root Cause
Our package has several issues that combine to create this bug:
- Package declares
"type": "module"
making it ESM-first - ESM spec requires explicit file extensions in import paths
- TypeScript's
moduleResolution: "node"
outputs extension-less imports - No post-processing step to add extensions
In other words: we're telling JavaScript "use the new ESM system" (type: "module"
), but our build process is creating import statements that only work with the old system.
Impact
- Breaks in all ESM environments (Vite, webpack w/ESM, Node.js w/ESM)
- Affects downstream packages using native ESM
- Type definitions also lack extensions, causing TS errors
Current Setup
Here's our current build configuration:
// package.json
{
"type": "module",
"main": "lib/esm/index.js",
"exports": {
"require": "./lib/commonjs/index.cjs",
"import": "./lib/esm/index.js"
},
"scripts": {
"build": "pnpm compile:esm && pnpm compile:cjs",
"compile:esm": "tsc -p tsconfig.json",
"compile:cjs": "tsc -p tsconfig.cjs.json && cjsify ./lib/commonjs"
}
}
This setup attempts to support both CJS and ESM (called "dual package support"), but the ESM output isn't spec-compliant because of the missing extensions.
Proposed Solution: Migrate to tsup
Instead of managing all this complexity ourselves, we should switch to tsup. Think of tsup as a smart bundler that knows how to handle all these module format issues automatically - it's become the standard tool for building modern TypeScript libraries.
Implementation
- Install tsup:
pnpm add -D tsup
- Update package.json:
{
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts"
}
}
- Update tsconfig.json:
Even though tsup handles the build output, we still need a proper tsconfig.json for:
- Editor support (VSCode, etc.)
- Type checking during development
- Ensuring ESM compatibility for source files
Our current tsconfig:
{
"compilerOptions": {
"module": "es2022",
"moduleResolution": "node",
"target": "es2022"
// ... other options
}
}
With tsup, we have two options:
A. Keep using moduleResolution: "node"
since tsup will handle the ESM output:
{
"compilerOptions": {
"module": "es2022",
"moduleResolution": "node",
"target": "es2022"
}
}
B. Switch to modern module resolution to catch ESM issues during development:
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022"
}
}
Option B is recommended as it helps catch ESM compatibility issues earlier in the development process rather than at build time.
Benefits of tsup
- Handles ESM/CJS dual package output automatically
- Adds proper file extensions for ESM
- Bundles dependencies correctly
- Much faster than our current two-step build process
- Follows modern TypeScript library best practices
Alternative Solutions
While tsup is recommended, there are other approaches we could take:
-
Add
.js
extensions manually in our TypeScript code- ✅ Simple to understand
- ❌ Requires changing all import statements
- ❌ More maintenance burden
- ❌ Easy to forget for new code
-
Switch back to
"type": "commonjs"
- ✅ Quick fix
- ❌ Goes against modern JavaScript trends
- ❌ May cause issues for ESM-only environments
- ❌ Technical debt we'll need to fix later
-
Add a post-processing step
- ✅ No code changes needed
- ❌ More complex build process
- ❌ Another tool to maintain
- ❌ Can be brittle
Next Steps
- Try the tsup migration in a branch
- Verify it works with different consumers:
- Vite
- webpack
- Node.js (both ESM and CJS)
- Check bundle sizes
- Review the generated types
- Run integration tests
Let me know if you'd like me to create a PR with these changes or if you have any questions about the solution!
Metadata
Metadata
Assignees
Labels
Type
Projects
Status