Files
languageservices/docs/esm-migration-plan.md
T
eric sciple 10171d84a7 docs: Add ESM migration plan
Add detailed plan for migrating to moduleResolution node16/nodenext with
file extensions in imports.

This plan addresses:
- #154 - Upgrade moduleResolution from node to node16/nodenext
- #110 - Published ESM code has imports without file extensions
- #64 - expressions: ERR_MODULE_NOT_FOUND attempting to run example demo script
- #146 - Can not import @actions/workflow-parser

The migration will:
1. Upgrade TypeScript to 5.7+
2. Enable rewriteRelativeImportExtensions
3. Add .ts extensions to all relative imports
4. Update JSON imports to use import attributes

Implementation will begin after pending PRs are merged to avoid conflicts.
2025-12-11 02:51:35 +00:00

272 lines
7.8 KiB
Markdown

# ESM Migration Plan: Add File Extensions to Imports
## Overview
This document outlines the plan to migrate from TypeScript's deprecated `"moduleResolution": "node"` (node10) to `"moduleResolution": "node16"` or `"nodenext"`. This change is necessary because the published ESM packages have extensionless imports that don't work correctly in modern ESM environments.
## Issues Fixed
This migration will resolve the following issues:
- **#154** - Upgrade `moduleResolution` from `node` to `node16` or `nodenext` in tsconfig
- **#110** - Published ESM code has imports without file extensions
- **#64** - expressions: ERR_MODULE_NOT_FOUND attempting to run example demo script
- **#146** - Can not import `@actions/workflow-parser`
## Problem Statement
### Current State
All packages use `"moduleResolution": "node"`:
| Package | moduleResolution | TypeScript |
|---------|------------------|------------|
| expressions | `"node"` | ^4.7.4 |
| workflow-parser | `"node"` | ^4.8.4 |
| languageservice | `"node"` | ^4.8.4 |
| languageserver | `"node"` | ^4.8.4 |
| browser-playground | `"Node16"` ✅ | ^4.9.4 |
This causes TypeScript to emit code like:
```javascript
// Published to npm - INVALID ESM
export { Expr } from "./ast"; // Missing .js extension!
```
### Why This Fails
ESM in Node.js 12+ **requires** explicit file extensions. When users try to import these packages:
```javascript
// User's code
import { Expr } from "@actions/expressions";
```
Node.js fails with:
```
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../node_modules/@actions/expressions/dist/ast'
```
## Migration Strategy
### Option A: TypeScript 5.7+ with `rewriteRelativeImportExtensions` (Recommended)
TypeScript 5.7 introduced a new compiler option that automatically rewrites `.ts` extensions to `.js` in output:
```jsonc
{
"compilerOptions": {
"moduleResolution": "node16", // or "nodenext"
"rewriteRelativeImportExtensions": true
}
}
```
**Source code:**
```typescript
import { Expr } from "./ast.ts";
```
**Compiled output:**
```javascript
export { Expr } from "./ast.js";
```
**Pros:**
- Source uses `.ts` extensions (matches actual files)
- Works with Deno (which requires `.ts` extensions)
- TypeScript automatically transforms to `.js`
- Modern, forward-looking approach
**Cons:**
- Requires TypeScript 5.7+
- Relatively new feature
### Option B: Manual `.js` Extensions
Use `.js` extensions in source TypeScript files:
```typescript
import { Expr } from "./ast.js"; // Points to .ts file, but use .js extension
```
**Pros:**
- Works with TypeScript 4.7+ (with node16 moduleResolution)
- Well-established pattern
**Cons:**
- Confusing - `.js` files don't exist at write time
- Doesn't work with Deno out of the box
### Recommendation
**Use Option A** - TypeScript 5.7+ with `rewriteRelativeImportExtensions`:
- Cleaner developer experience (`.ts` imports match actual files)
- Better Deno compatibility
- TypeScript 5.x upgrade is already on the roadmap (see Dependabot PRs #208-212)
## Scope of Changes
### Statistics
- **~73 source files** need import updates
- **~176 relative imports** need `.ts` extensions added
- **5 packages** need tsconfig updates (browser-playground already uses node16)
- **6 JSON imports** need attention
### Package-by-Package Breakdown
#### expressions/
- tsconfig.json: Update `moduleResolution`
- Add `.ts` extensions to all relative imports
- Files: ~15 source files
#### workflow-parser/
- tsconfig.json: Update `moduleResolution`
- Add `.ts` extensions to all relative imports
- JSON import: `workflow-v1.0.min.json` - needs `with { type: "json" }` or type assertion
- Files: ~25 source files
#### languageservice/
- tsconfig.json: Update `moduleResolution`
- Add `.ts` extensions to all relative imports
- JSON imports: webhooks, schedule, workflow_call, descriptions - need handling
- Files: ~30 source files
#### languageserver/
- tsconfig.json: Update `moduleResolution`
- Add `.ts` extensions to all relative imports
- Files: ~10 source files
#### browser-playground/
- Already uses `"moduleResolution": "Node16"`
- May need import extension updates
- Bundled via webpack, so may have different requirements
### JSON Import Handling
JSON imports require special handling in node16/nodenext. Options:
1. **Import Attributes (Node 20.10+)**
```typescript
import schema from "./workflow-v1.0.json" with { type: "json" };
```
Requires: TypeScript 5.3+, Node 20.10+
2. **fs.readFileSync at runtime**
```typescript
const schema = JSON.parse(fs.readFileSync(new URL("./schema.json", import.meta.url), "utf8"));
```
Works with any Node version but requires runtime file access
3. **Keep resolveJsonModule with assertion**
```typescript
import schema from "./schema.json" assert { type: "json" };
```
Note: `assert` is deprecated in favor of `with`
**Recommendation:** Use Import Attributes (`with { type: "json" }`) since we already require Node >= 18 and will bump TypeScript to 5.x.
## Implementation Steps
### Phase 1: Prerequisites (Wait for merge)
- [ ] Merge all pending PRs to avoid conflicts
- PR #242 (activity types)
- Any other pending PRs
### Phase 2: TypeScript Upgrade
- [ ] Upgrade TypeScript to 5.7+ in all packages
- [ ] Merge Dependabot PRs #208-212 or create unified upgrade PR
- [ ] Verify all tests pass after upgrade
### Phase 3: tsconfig Updates
Update each package's `tsconfig.json`:
```jsonc
{
"compilerOptions": {
"module": "node16", // or "nodenext"
"moduleResolution": "node16", // or "nodenext"
"rewriteRelativeImportExtensions": true
}
}
```
### Phase 4: Add Extensions to Imports
For each package, update all relative imports:
```typescript
// Before
import { Expr } from "./ast";
import { Parser } from "./parser";
// After
import { Expr } from "./ast.ts";
import { Parser } from "./parser.ts";
```
**Automation approach:**
```bash
# Can use sed/perl or a codemod tool
find src -name "*.ts" -exec sed -i "s/from '\.\//from '.\\//g" {} \;
# More sophisticated regex needed - consider using ts-morph or jscodeshift
```
### Phase 5: JSON Import Updates
Update JSON imports to use import attributes:
```typescript
// Before
import schema from "./workflow-v1.0.min.json";
// After
import schema from "./workflow-v1.0.min.json" with { type: "json" };
```
### Phase 6: Verification
- [ ] Run `npm run build` in all packages
- [ ] Run `npm test` in all packages
- [ ] Test importing published packages in:
- [ ] Node.js ESM mode (`"type": "module"`)
- [ ] Vite project
- [ ] Deno (bonus)
- [ ] Verify browser-playground still works
### Phase 7: Documentation
- [ ] Update READMEs with any new requirements
- [ ] Add migration notes to CHANGELOG
## Risks and Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Breaking change for consumers | High | Semver major version bump |
| Import attributes not supported in older bundlers | Medium | Test with webpack, vite, rollup |
| Some edge cases with re-exports | Low | Careful testing |
| browser-playground uses webpack | Low | Webpack handles bundling differently |
## Timeline
1. **Wait for pending PRs** - ~1 week
2. **TypeScript upgrade** - 1 day
3. **Import migration** - 2-3 days
4. **Testing & verification** - 1 day
5. **Documentation** - 0.5 day
**Total: ~1-2 weeks** after dependencies are ready
## References
- [TypeScript moduleResolution reference](https://www.typescriptlang.org/docs/handbook/modules/reference.html)
- [TypeScript 5.7 rewriteRelativeImportExtensions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-7.html#path-rewriting-for-relative-paths)
- [Node.js ESM mandatory extensions](https://nodejs.org/api/esm.html#mandatory-file-extensions)
- [Import Attributes proposal](https://github.com/tc39/proposal-import-attributes)
- [Community fork that works](https://github.com/boxbuild-io/actions-languageservices/commit/077fb2b58dfd2cca3d6e3df1fdf9e26e75db24ae)