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"
}
}
}
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",
}
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";
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`);
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,
},
],
// ...
};
For packages using tsup (Recommended)
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:
- Type Check: Run
pnpm type-checkto ensure TypeScript compiles - Build: Run
pnpm buildto verify build succeeds - Tests: Run
pnpm testto ensure all tests pass - Lint: Run
pnpm lintto 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:
- Check the Common Issues section
- Review the Gesso monorepo examples in
packages/ - Ask in the team Slack channel
Last updated: October 2025