arkit
Version:
Visualises JavaScript, TypeScript and Flow codebases as meaningful and committable architecture diagrams
487 lines (371 loc) • 10.7 kB
Markdown
# ESM Migration Plan for Arkit
## Current State (as of Dec 2025)
**Module System:** CommonJS (CJS)
- TypeScript compiles to CommonJS (`"module": "commonjs"`)
- `index.js` uses `require()`
- Compatible with Node.js 12+
**Dependency Constraints:**
- `yargs` locked to v17.7.2 (last CJS-compatible version)
- `yargs` v18+ requires ESM and cannot be `require()`'d
- Other modern dependencies increasingly ESM-only
**Problem:** The JavaScript ecosystem is moving to ESM, and staying on CommonJS limits access to modern tooling and dependencies.
## Migration Strategy: Dual Package Approach
**Goal:** Support both ESM and CommonJS for backwards compatibility
### Phase 1: Source Migration to ESM
**1.1 Update package.json**
```json
{
"type": "module",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs"
},
"./package.json": "./package.json"
},
"bin": {
"arkit": "./bin/arkit.js"
},
"engines": {
"node": ">=16.0.0"
}
}
```
**1.2 Convert Source Files**
All `*.ts` files in `src/`:
- Change `import x = require('x')` → `import x from 'x'`
- Change `export =` → `export default`
- Use `.js` extensions in relative imports: `import { foo } from './bar.js'`
Example:
```typescript
// Before (CommonJS style)
import * as fs from "fs";
import * as path from "path";
// After (ESM style)
import fs from "fs";
import path from "path";
// or
import { readFileSync } from "fs";
```
**1.3 Update TypeScript Configuration**
Create `tsconfig.esm.json`:
```json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ES2020",
"moduleResolution": "node",
"target": "ES2020",
"outDir": "dist/esm",
"declaration": true,
"declarationMap": true
}
}
```
Create `tsconfig.cjs.json`:
```json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node",
"target": "ES2015",
"outDir": "dist/cjs",
"declaration": true,
"declarationMap": true
}
}
```
**1.4 Update Build Scripts**
```json
{
"scripts": {
"clean": "rm -rf dist",
"build:esm": "tsc --project tsconfig.esm.json",
"build:cjs": "tsc --project tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
"build": "npm run clean && npm run build:esm && npm run build:cjs",
"compile": "npm run build"
}
}
```
The `package.json` in `dist/cjs/` tells Node.js to treat `.js` files there as CommonJS.
### Phase 2: CLI Entry Point
**2.1 Create ESM-compatible CLI wrapper**
`bin/arkit.js`:
```javascript
#!/usr/bin/env node
// Detect if we're running as ESM or CJS
const isESM = typeof import.meta !== 'undefined';
if (isESM) {
// ESM entry
import('../dist/esm/cli.js').then(({ cli }) => {
// ... CLI logic
});
} else {
// CJS fallback
const { cli } = require('../dist/cjs/index.cjs');
// ... CLI logic
}
```
Or simpler: Use pure ESM and require Node.js 16+:
`bin/arkit.js`:
```javascript
#!/usr/bin/env node
import { cli, arkit, getConfig, getOutputs } from '../dist/esm/index.js';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
if (process.argv[1] === fileURLToPath(import.meta.url)) {
cli.epilogue(`${pkg.description} ${pkg.homepage}`)
.version(pkg.version)
.help('help');
// ... rest of CLI logic
}
```
**2.2 Remove old `index.js`**
The old `index.js` becomes unnecessary as package.json `exports` handles routing.
### Phase 3: Dependencies Update
**3.1 Upgrade yargs**
```bash
npm install yargs@latest # v18+
```
**3.2 Update yargs usage for ESM**
```typescript
// src/cli.ts
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
export const cli = yargs(hideBin(process.argv))
.scriptName('arkit')
.usage('Usage: $0 [directory] [options]')
// ... rest of config
```
**3.3 Other dependencies**
Check for ESM-only updates:
- `nanomatch` → `picomatch` (modern alternative)
- Consider latest versions of all deps
---
### Phase 4: Testing
**4.1 Update Jest Configuration**
For ESM support in Jest:
```json
{
"jest": {
"preset": "ts-jest/presets/default-esm",
"extensionsToTreatAsEsm": [".ts"],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
},
"transform": {
"^.+\\.ts$": ["ts-jest", {
"useESM": true,
"tsconfig": "tsconfig.esm.json"
}]
}
}
}
```
**4.2 Test Both Module Formats**
Create tests that verify both ESM and CJS work:
```javascript
// test/module-formats.test.ts
describe('Module Formats', () => {
test('ESM import works', async () => {
const { arkit } = await import('../dist/esm/index.js');
expect(typeof arkit).toBe('function');
});
test('CJS require works', () => {
const { arkit } = require('../dist/cjs/index.cjs');
expect(typeof arkit).toBe('function');
});
});
```
**4.3 Test CLI in both modes**
Verify the CLI binary works:
```bash
./bin/arkit.js --help
node --input-type=module ./bin/arkit.js --help
node --input-type=commonjs ./bin/arkit.js --help # Should fail gracefully
```
### Phase 5: Documentation Updates
**5.1 README.md**
Add note about ESM:
```markdown
## Installation
```bash
npm install arkit --save-dev
```
Arkit supports both ESM and CommonJS:
**ESM (Recommended):**
```javascript
import { arkit } from 'arkit';
await arkit({
directory: './src',
output: 'architecture.svg'
});
```
**CommonJS (Legacy):**
```javascript
const { arkit } = require('arkit');
arkit({
directory: './src',
output: 'architecture.svg'
}).then(console.log);
```
```
**5.2 CLAUDE.md**
Update architecture section:
- Note dual-package setup
- Explain ESM as primary, CJS as compatibility layer
- Document build process
**5.3 Migration Guide**
Create `MIGRATION_GUIDE_v2.md` for users upgrading:
- How to migrate from CJS imports
- Breaking changes (if any)
- New features available with ESM
## Rollout Plan
### Step 1: Development (1-2 weeks)
1. Create feature branch: `feature/esm-migration`
2. Implement Phase 1-2 (source + build)
3. Verify dual builds work
4. Test locally with both module formats
### Step 2: Testing (1 week)
1. Implement Phase 3-4 (deps + tests)
2. Update CI to test both ESM and CJS outputs
3. Test in sample projects (both ESM and CJS)
4. Check compatibility with Node 16, 18, 20, 22
### Step 3: Documentation (3-5 days)
1. Update all docs (Phase 5)
2. Write migration guide
3. Update examples in repo
### Step 4: Beta Release
1. Release as `arkit@2.0.0-beta.1`
2. Ask community to test
3. Collect feedback on GitHub
4. Fix issues
### Step 5: Stable Release
1. Release `arkit@2.0.0`
2. Announce on GitHub, npm, social media
3. Monitor issues for 2-4 weeks
4. Patch any compatibility issues
## Breaking Changes to Consider
### Major Version: v2.0.0
**Required:**
- Minimum Node.js version: 16+ (from 12+)
- `yargs` upgraded to v18+ (affects CLI API slightly)
**Optional Breaking Changes to Bundle:**
- Remove deprecated options/features
- Rename confusing config options
- Modernize default behavior
**Non-Breaking (Backwards Compatible):**
- ESM/CJS dual package
- All existing code works with require()
- CLI remains compatible
## Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|-----------|
| CJS output doesn't work | High | Thorough testing, keep v1.x available |
| ESM output doesn't work | High | Test in multiple environments |
| CLI breaks | High | Test with both module loaders |
| Dependency issues | Medium | Gradual upgrade, test each |
| Community backlash | Medium | Clear docs, beta period, support |
| Build complexity | Low | Document build process clearly |
## Success Criteria
✅ **Technical:**
- Both ESM and CJS imports work
- All 19+ tests pass
- CLI works in all scenarios
- No performance regression
- CI passes on Node 16, 18, 20, 22
✅ **User Experience:**
- Existing code doesn't break
- Clear migration path
- Better developer experience
- Access to modern dependencies
✅ **Documentation:**
- Updated README
- Migration guide exists
- Examples for both formats
- Clear breaking changes documented
## Timeline Estimate
| Phase | Duration | Effort |
|-------|----------|--------|
| Source Migration | 3-5 days | High |
| CLI Rewrite | 2-3 days | Medium |
| Testing Setup | 2-3 days | Medium |
| Dual Build Config | 1-2 days | Medium |
| Dependency Updates | 1-2 days | Low |
| Documentation | 3-5 days | Medium |
| Testing & QA | 5-7 days | High |
| **Total** | **3-4 weeks** | |
## Alternative: ESM-Only (v3.0 consideration)
If user base supports it, eventually drop CJS entirely:
**Benefits:**
- Simpler build process
- Smaller package size
- Modern tooling access
- Less maintenance
**Timeline:**
- Not before 2026
- After v2.0 has been stable for 6+ months
- When Node 16+ adoption is high (>95%)
- Survey users first
## Reference Links
- [Node.js ESM Documentation](https://nodejs.org/api/esm.html)
- [TypeScript Handbook: Modules](https://www.typescriptlang.org/docs/handbook/modules.html)
- [Dual Package Hazard](https://nodejs.org/api/packages.html#dual-commonjses-module-packages)
- [Pure ESM Package Guide](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
- [yargs v18 ESM Support](https://github.com/yargs/yargs/issues/1929)
## Notes
**Why Dual Package?**
- Large user base on CommonJS
- Breaking everyone's code is bad UX
- Gradual migration is safer
- Industry standard approach (see: `chalk`, `got`, etc.)
**Why Not Pure ESM Now?**
- Too disruptive for existing users
- Requires Node 16+ (many still on 14)
- CJS still widely used
- Can do pure ESM in v3.0 later
**Current Blocker Resolved:**
- This PR stays on CommonJS + yargs v17
- Gets local PlantUML + WASM shipped
- ESM can be tackled properly in v2.0
## Action Items for ESM Migration
When ready to start:
- [ ] Create `feature/esm-migration` branch
- [ ] Update `package.json` with exports
- [ ] Convert source files to ESM syntax
- [ ] Create dual TypeScript configs
- [ ] Update build scripts
- [ ] Rewrite CLI entry point
- [ ] Upgrade yargs to v18+
- [ ] Update Jest for ESM
- [ ] Test both module formats
- [ ] Update all documentation
- [ ] Create migration guide
- [ ] Beta release
- [ ] Collect feedback
- [ ] Stable v2.0.0 release
**Status:** 📋 Planning Phase
**Target:** Q1-Q2 2026
**Owner:** TBD
**Related:** Issue #XXX (to be created)