Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions docs/esm-migration-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
# 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)