Skip to main content

ESM Migration Guide

This guide helps you migrate your Gesso-based project to ECMAScript Modules (ESM), the modern JavaScript module system.

Why Migrate to ESM?

  • Modern Standard: ESM is the official JavaScript module standard
  • Better Tree Shaking: Improved dead code elimination for smaller bundles
  • Static Analysis: Better IDE support and type checking
  • Native Browser Support: No transpilation needed for modern browsers
  • Future-Proof: CommonJS is being phased out in the Node.js ecosystem

Prerequisites

  • Node.js 18+ (ESM has better support in newer versions)
  • TypeScript 5.0+
  • Understanding of the difference between CommonJS (require/module.exports) and ESM (import/export)

Migration Steps

1. Update package.json

Add "type": "module" to mark your package as ESM:

{
"name": "@acromedia/your-package",
"version": "1.0.0",
"type": "module",
...
}

Update Package Exports

Replace old main field with modern exports field:

Before (CommonJS):

{
"main": "dist/index.js",
"types": "dist/index.d.ts"
}

After (ESM):

{
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}
Multiple Exports

You can export multiple entry points:

{
"exports": {
".": "./dist/index.js",
"./client": "./dist/client.js",
"./server": "./dist/server.js"
}
}

2. Update TypeScript Configuration

Update your tsconfig.json to use NodeNext modules for package:

{
"extends": "@acromedia/config/tsconfig/react-library.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"outDir": "./dist",
"esModuleInterop": true,
"resolveJsonModule": true,
"moduleDetection": "force"
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]
}

In Next.js, make sure it’s using the following module and moduleResolution settings. After applying the changes, you might see some type errors pop up — that’s just TypeScript now enforcing stricter rules

  ...
"compilerOptions": {
...
"module": "ESNext",
"moduleResolution": "bundler",
}
TypeScript Config Options

Gesso provides pre-configured TypeScript configs:

  • @acromedia/config/tsconfig/base.json - Base ESM config
  • @acromedia/config/tsconfig/react-library.json - For React libraries
  • @acromedia/config/tsconfig/nextjs.json - For Next.js applications

These configs are already ESM-ready as of Gesso 7.2+

3. Update Import Statements

Add .js Extensions

In ESM, you must include file extensions for relative imports:

Before:

import { myFunction } from "./utils";
import MyComponent from "../components/MyComponent";

After:

import { myFunction } from "./utils.js";
import MyComponent from "../components/MyComponent.js";
Important

Even though your source files are .ts, you must use .js extensions because TypeScript compiles to .js files.

Use Node.js Built-in Prefixes

Prefix Node.js built-in modules with node::

Before:

import fs from "fs";
import path from "path";
import { execSync } from "child_process";

After:

import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";

Import JSON Files

JSON imports require an import assertion:

Before:

import packageJson from "./package.json";

After:

import packageJson from './package.json' with { type: 'json' }

4. Handle Dynamic Requires

If you need dynamic require() in ESM contexts, use createRequire:

Before:

const platform = getPlatform();
const { generateConfig } = require(`@acromedia/gesso-${platform}/cli`);

After:

import { createRequire } from "node:module";

const require = createRequire(import.meta.url);

const platform = getPlatform();
const { generateConfig } = require(`@acromedia/gesso-${platform}/cli`);
Dynamic Imports

Prefer dynamic import() when possible:

const platform = getPlatform();
const module = await import(`@acromedia/gesso-${platform}/cli`);
const { generateConfig } = module;

5. Update **dirname and **filename

ESM doesn't have __dirname and __filename. Use these alternatives:

Before:

const currentDir = __dirname;
const currentFile = __filename;

After:

import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

6. Update Exports

Replace CommonJS exports with ESM exports:

Before:

module.exports = {
myFunction,
MyClass,
};

module.exports.default = MyComponent;
exports.helper = helperFunction;

After:

export { myFunction, MyClass };
export default MyComponent;
export const helper = helperFunction;

7. Update Build Configuration

For packages using Rollup (Deprecated)

If still using Rollup, ensure it outputs ESM:

// rollup.config.js
export default {
input: "src/index.ts",
output: [
{
file: "dist/index.js",
format: "esm", // Changed from 'cjs'
sourcemap: true,
},
],
// ...
};

Gesso packages now use tsup for building. Example tsup.config.ts:

import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
sourcemap: true,
clean: true,
external: ["react", "react-dom"],
});

8. Update Test Configuration

Cypress with ESM

Update cypress.config.ts:

import { defineConfig } from "cypress";

export default defineConfig({
component: {
devServer: {
framework: "react",
bundler: "webpack",
},
specPattern: "src/**/*.spec.{js,jsx,ts,tsx}",
},
});

Common Issues and Solutions

Issue: "Cannot find module" errors

Cause: Missing .js extensions on relative imports

Solution: Add .js to all relative imports:

import { foo } from "./bar.js"; // Not './bar'

Issue: "require is not defined"

Cause: Using CommonJS require() in ESM context

Solution: Use createRequire or convert to import:

import { createRequire } from "node:module";
const require = createRequire(import.meta.url);

Issue: "__dirname is not defined"

Cause: __dirname doesn't exist in ESM

Solution: Use import.meta.url:

import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));

Issue: JSON import fails

Cause: Missing import assertion

Solution: Add with { type: 'json' }:

import data from './data.json' with { type: 'json' }

Issue: TypeScript compilation errors with extensions

Cause: TypeScript shows errors for .js extensions on .ts files

Solution: Ensure moduleResolution: "NodeNext" in tsconfig.json

Issue: Type export issue

Cause: re-export type from other package could cause issue

Solution: use type import the import statement

import type from "@package";

Testing Your Migration

After migrating, verify everything works:

  1. Type Check: Run pnpm type-check to ensure TypeScript compiles
  2. Build: Run pnpm build to verify build succeeds
  3. Tests: Run pnpm test to ensure all tests pass
  4. Lint: Run pnpm lint to catch any import issues

Gesso Package Status

As of Gesso 7.2+, the following core packages are ESM-ready:

  • @acromedia/gesso-cli - Fully migrated
  • @acromedia/config - TypeScript configs updated for ESM
  • @acromedia/gesso-core - ESM compatible
  • ✅ All plugin packages - Using ESM

References

Getting Help

If you encounter issues during migration:

  1. Check the Common Issues section
  2. Review the Gesso monorepo examples in packages/
  3. Ask in the team Slack channel

Last updated: October 2025