Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESM Import Resolution Issues #1454

Open
r-near opened this issue Feb 20, 2025 · 0 comments
Open

ESM Import Resolution Issues #1454

r-near opened this issue Feb 20, 2025 · 0 comments
Labels
bug Something isn't working

Comments

@r-near
Copy link
Contributor

r-near commented Feb 20, 2025

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:

  1. The older CommonJS (CJS) way:
// Importing
const { thing } = require('./file')
// Exporting
module.exports = { thing }
  1. 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:

  1. Package declares "type": "module" making it ESM-first
  2. ESM spec requires explicit file extensions in import paths
  3. TypeScript's moduleResolution: "node" outputs extension-less imports
  4. 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

  1. Install tsup:
pnpm add -D tsup
  1. 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"
  }
}
  1. 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

  1. Handles ESM/CJS dual package output automatically
  2. Adds proper file extensions for ESM
  3. Bundles dependencies correctly
  4. Much faster than our current two-step build process
  5. Follows modern TypeScript library best practices

Alternative Solutions

While tsup is recommended, there are other approaches we could take:

  1. 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
  2. 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
  3. Add a post-processing step

    • ✅ No code changes needed
    • ❌ More complex build process
    • ❌ Another tool to maintain
    • ❌ Can be brittle

Next Steps

  1. Try the tsup migration in a branch
  2. Verify it works with different consumers:
    • Vite
    • webpack
    • Node.js (both ESM and CJS)
  3. Check bundle sizes
  4. Review the generated types
  5. 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!

@r-near r-near added the bug Something isn't working label Feb 20, 2025
@github-project-automation github-project-automation bot moved this to NEW❗ in DevTools Feb 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
Status: NEW❗
Development

No branches or pull requests

1 participant