Skip to content

Commit a1bd7e8

Browse files
authored
Merge branch 'main' into rentziass/codeactions2
2 parents 106aef0 + 656a821 commit a1bd7e8

File tree

186 files changed

+1137
-764
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

186 files changed

+1137
-764
lines changed

docs/esm-migration-plan.md

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# ESM Migration Plan: Add File Extensions to Imports
2+
3+
## Overview
4+
5+
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.
6+
7+
## Issues Fixed
8+
9+
This migration will resolve the following issues:
10+
11+
- **#154** - Upgrade `moduleResolution` from `node` to `node16` or `nodenext` in tsconfig
12+
- **#110** - Published ESM code has imports without file extensions
13+
- **#64** - expressions: ERR_MODULE_NOT_FOUND attempting to run example demo script
14+
- **#146** - Can not import `@actions/workflow-parser`
15+
16+
## Problem Statement
17+
18+
### Current State
19+
20+
All packages use `"moduleResolution": "node"`:
21+
22+
| Package | moduleResolution | TypeScript |
23+
|---------|------------------|------------|
24+
| expressions | `"node"` | ^4.7.4 |
25+
| workflow-parser | `"node"` | ^4.8.4 |
26+
| languageservice | `"node"` | ^4.8.4 |
27+
| languageserver | `"node"` | ^4.8.4 |
28+
| browser-playground | `"Node16"`| ^4.9.4 |
29+
30+
This causes TypeScript to emit code like:
31+
```javascript
32+
// Published to npm - INVALID ESM
33+
export { Expr } from "./ast"; // Missing .js extension!
34+
```
35+
36+
### Why This Fails
37+
38+
ESM in Node.js 12+ **requires** explicit file extensions. When users try to import these packages:
39+
40+
```javascript
41+
// User's code
42+
import { Expr } from "@actions/expressions";
43+
```
44+
45+
Node.js fails with:
46+
```
47+
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../node_modules/@actions/expressions/dist/ast'
48+
```
49+
50+
## Migration Strategy
51+
52+
### Option A: TypeScript 5.7+ with `rewriteRelativeImportExtensions` (Recommended)
53+
54+
TypeScript 5.7 introduced a new compiler option that automatically rewrites `.ts` extensions to `.js` in output:
55+
56+
```jsonc
57+
{
58+
"compilerOptions": {
59+
"moduleResolution": "node16", // or "nodenext"
60+
"rewriteRelativeImportExtensions": true
61+
}
62+
}
63+
```
64+
65+
**Source code:**
66+
```typescript
67+
import { Expr } from "./ast.ts";
68+
```
69+
70+
**Compiled output:**
71+
```javascript
72+
export { Expr } from "./ast.js";
73+
```
74+
75+
**Pros:**
76+
- Source uses `.ts` extensions (matches actual files)
77+
- Works with Deno (which requires `.ts` extensions)
78+
- TypeScript automatically transforms to `.js`
79+
- Modern, forward-looking approach
80+
81+
**Cons:**
82+
- Requires TypeScript 5.7+
83+
- Relatively new feature
84+
- **BUG:** See "Known Issues" section below
85+
86+
### Option B: Manual `.js` Extensions
87+
88+
Use `.js` extensions in source TypeScript files:
89+
90+
```typescript
91+
import { Expr } from "./ast.js"; // Points to .ts file, but use .js extension
92+
```
93+
94+
**Pros:**
95+
- Works with TypeScript 4.7+ (with node16 moduleResolution)
96+
- Well-established pattern
97+
- No post-processing needed
98+
- Works with ts-jest without extra configuration
99+
100+
**Cons:**
101+
- Confusing - `.js` files don't exist at write time
102+
- Doesn't work with Deno out of the box
103+
104+
### Recommendation
105+
106+
**Use Option B** (manual `.js` extensions). Option A with `rewriteRelativeImportExtensions` has compatibility issues with ts-jest and requires additional workarounds.
107+
108+
---
109+
110+
## Known Issues and Workarounds (December 2025)
111+
112+
### 1. TypeScript Version Conflicts in Monorepo
113+
114+
**Problem:** The root `node_modules/typescript` was version 4.9.5 (pulled in by `ts-node` and `tsutils` dependencies), while workspace packages specified `^5.8.3`.
115+
116+
**Symptoms:**
117+
- `npx tsc --version` showed 4.9.5
118+
- `require('typescript').version` in ts-jest showed 5.8.3
119+
- Confusing build failures
120+
121+
**Solution:** Add npm overrides in root `package.json`:
122+
```json
123+
{
124+
"overrides": {
125+
"typescript": "5.8.3"
126+
}
127+
}
128+
```
129+
130+
### 2. ts-jest Compatibility with TypeScript 5.9+
131+
132+
**Problem:** ts-jest 29.4.6 uses `typescript.JSDocParsingMode.ParseAll` which doesn't exist in TypeScript's ES module exports.
133+
134+
**Error:**
135+
```
136+
TypeError: Cannot read properties of undefined (reading 'ParseAll')
137+
at Object.<anonymous> (node_modules/ts-jest/dist/compiler/ts-compiler.js:43:123)
138+
```
139+
140+
**Root Cause:** ts-jest accesses `typescript_1.default.JSDocParsingMode.ParseAll` but TypeScript has no default export in ESM.
141+
142+
**Solution:**
143+
- Use ts-jest 29.0.3 (older version that doesn't use this API)
144+
- OR wait for ts-jest fix
145+
- **Stay on TypeScript 5.8.3, not 5.9+**
146+
147+
### 3. TypeScript `rewriteRelativeImportExtensions` Bug with .d.ts Files
148+
149+
**Problem:** TypeScript's `rewriteRelativeImportExtensions: true` correctly rewrites `.ts``.js` in `.js` output files, but **incorrectly keeps `.ts` extensions in `.d.ts` declaration files**.
150+
151+
**Example:**
152+
- Source: `export { Expr } from "./ast.ts";`
153+
- Output `index.js`: `export { Expr } from "./ast.js";` ✅ Correct
154+
- Output `index.d.ts`: `export { Expr } from "./ast.ts";` ❌ Wrong (should be `.js`)
155+
156+
**Upstream Issue:** https://github.com/microsoft/TypeScript/issues/61037 (marked "Help Wanted", in Backlog, NOT FIXED as of Dec 2025)
157+
158+
**Workaround:** Post-process `.d.ts` files with a script. See `script/fix-dts-extensions.cjs`.
159+
160+
**Note:** Since we use Option B (manual `.js` extensions), this bug does not affect our migration.
161+
162+
### 4. yaml Package Internal Types Not Exported
163+
164+
**Problem:** The `yaml` package does not export internal types like `LinePos` and `NodeBase` that are used in `workflow-parser/src/workflows/yaml-object-reader.ts`.
165+
166+
**Error:**
167+
```
168+
error TS2305: Module '"yaml"' has no exported member 'LinePos'.
169+
error TS2305: Module '"yaml"' has no exported member 'NodeBase'.
170+
```
171+
172+
**Solution:** Define local type aliases in the file that uses them:
173+
```typescript
174+
// Local type definitions to replace yaml internal imports
175+
type LinePos = { line: number; col: number };
176+
type NodeBase = { range?: [number, number, number] };
177+
```
178+
179+
### 5. languageserver Blocked by vscode-languageserver Dependency
180+
181+
**Problem:** The `vscode-languageserver` package (v8.0.2) does not have proper ESM exports. When using `moduleResolution: "node16"`, TypeScript requires packages to have an `exports` field in `package.json` for subpath imports to work.
182+
183+
**Error:**
184+
```
185+
src/index.ts(6,8): error TS2307: Cannot find module 'vscode-languageserver/browser' or its corresponding type declarations.
186+
src/connection.ts(1,43): error TS2307: Cannot find module 'vscode-languageserver/node' or its corresponding type declarations.
187+
```
188+
189+
**Root Cause:** The `vscode-languageserver` package.json only has `main` and `browser` fields, but no `exports` field:
190+
```json
191+
{
192+
"main": "./lib/node/main.js",
193+
"browser": {
194+
"./lib/node/main.js": "./lib/browser/main.js"
195+
}
196+
// No "exports" field!
197+
}
198+
```
199+
200+
With `moduleResolution: "node16"`, TypeScript follows Node.js ESM resolution rules which require explicit `exports` for subpath imports like `vscode-languageserver/browser` and `vscode-languageserver/node`.
201+
202+
**Status:** Verified December 2025. Version 9.0.1 is available but ESM export support is not confirmed.
203+
204+
**Current Decision:** The languageserver package is **deferred** from this migration until the upstream `vscode-languageserver` package adds proper ESM exports. It will continue using the old `moduleResolution: "node"` configuration.
205+
206+
**Options to resolve:**
207+
- Wait for vscode-languageserver to add ESM exports
208+
- Try upgrading to vscode-languageserver v9.x to see if exports were added
209+
- Use a bundler to work around the module resolution
210+
- Fork or patch the dependency
211+
212+
---
213+
214+
## Migration Status
215+
216+
| Package | Tests | ESM Status |
217+
|---------|-------|------------|
218+
| expressions | 1068 | ✅ Migrated |
219+
| workflow-parser | 292 | ✅ Migrated |
220+
| languageservice | 452 | ✅ Migrated |
221+
| languageserver | 6 files | ⏸️ Deferred (vscode-languageserver lacks ESM exports) |
222+
223+
---
224+
225+
## Required Configuration Changes
226+
227+
### tsconfig.build.json (each migrated package)
228+
229+
**Note:** We use **Option B** (manual `.js` extensions in source files) rather than `rewriteRelativeImportExtensions` because Option A caused ts-jest compatibility issues (tests would hang indefinitely).
230+
231+
```json
232+
{
233+
"compilerOptions": {
234+
"module": "node16",
235+
"moduleResolution": "node16",
236+
"skipLibCheck": true,
237+
"lib": ["ES2022"],
238+
"target": "ES2022"
239+
}
240+
}
241+
```
242+
243+
The `skipLibCheck: true` is needed to work around @types/node compatibility issues with TypeScript 5.x (TS2386 overload signature errors).
244+
```
245+
246+
### jest.config.js (each migrated package)
247+
248+
```javascript
249+
/** @type {import('ts-jest').JestConfigWithTsJest} */
250+
export default {
251+
preset: "ts-jest/presets/default-esm",
252+
moduleNameMapper: {
253+
"^(\\.{1,2}/.*)\\.js$": "$1",
254+
"^(\\.{1,2}/.*)\\.ts$": "$1",
255+
},
256+
transform: {
257+
"^.+\\.tsx?$": [
258+
"ts-jest",
259+
{
260+
useESM: true,
261+
isolatedModules: true,
262+
},
263+
],
264+
},
265+
moduleFileExtensions: ["ts", "js"],
266+
};
267+
```
268+
269+
### Root package.json
270+
271+
```json
272+
{
273+
"overrides": {
274+
"typescript": "5.8.3"
275+
}
276+
}
277+
```
278+
279+
### Each workspace package.json
280+
281+
```json
282+
{
283+
"devDependencies": {
284+
"typescript": "^5.8.3",
285+
"ts-jest": "^29.0.3"
286+
}
287+
}
288+
```
289+
290+
---
291+
292+
## References
293+
294+
- [TypeScript moduleResolution reference](https://www.typescriptlang.org/docs/handbook/modules/reference.html)
295+
- [TypeScript 5.7 rewriteRelativeImportExtensions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-7.html#path-rewriting-for-relative-paths)
296+
- [TypeScript .d.ts extension bug #61037](https://github.com/microsoft/TypeScript/issues/61037)
297+
- [Node.js ESM mandatory extensions](https://nodejs.org/api/esm.html#mandatory-file-extensions)
298+
- [ts-jest ESM support](https://kulshekhar.github.io/ts-jest/docs/guides/esm-support)
299+
- [Community fork that works](https://github.com/boxbuild-io/actions-languageservices/commit/077fb2b58dfd2cca3d6e3df1fdf9e26e75db24ae)

expressions/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,6 @@
6060
"prettier": "^2.8.3",
6161
"rimraf": "^3.0.2",
6262
"ts-jest": "^29.0.3",
63-
"typescript": "^4.7.4"
63+
"typescript": "^5.8.3"
6464
}
6565
}

expressions/src/ast.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {ExpressionData} from "./data";
2-
import {Token} from "./lexer";
1+
import {ExpressionData} from "./data/index.js";
2+
import {Token} from "./lexer.js";
33

44
export interface ExprVisitor<R> {
55
visitLiteral(literal: Literal): R;

expressions/src/completion.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import {complete, CompletionItem, trimTokenVector} from "./completion";
2-
import {DescriptionDictionary} from "./completion/descriptionDictionary";
3-
import {BooleanData} from "./data/boolean";
4-
import {Dictionary} from "./data/dictionary";
5-
import {StringData} from "./data/string";
6-
import {wellKnownFunctions} from "./funcs";
7-
import {FunctionDefinition, FunctionInfo} from "./funcs/info";
8-
import {Lexer, TokenType} from "./lexer";
1+
import {complete, CompletionItem, trimTokenVector} from "./completion.js";
2+
import {DescriptionDictionary} from "./completion/descriptionDictionary.js";
3+
import {BooleanData} from "./data/boolean.js";
4+
import {Dictionary} from "./data/dictionary.js";
5+
import {StringData} from "./data/string.js";
6+
import {wellKnownFunctions} from "./funcs.js";
7+
import {FunctionDefinition, FunctionInfo} from "./funcs/info.js";
8+
import {Lexer, TokenType} from "./lexer.js";
99

1010
const testContext = new Dictionary(
1111
{

expressions/src/completion.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import {DescriptionPair} from "./completion/descriptionDictionary";
2-
import {Dictionary, isDictionary} from "./data/dictionary";
3-
import {ExpressionData} from "./data/expressiondata";
4-
import {Evaluator} from "./evaluator";
5-
import {wellKnownFunctions} from "./funcs";
6-
import {FunctionDefinition, FunctionInfo} from "./funcs/info";
7-
import {Lexer, Token, TokenType} from "./lexer";
8-
import {Parser} from "./parser";
1+
import {DescriptionPair} from "./completion/descriptionDictionary.js";
2+
import {Dictionary, isDictionary} from "./data/dictionary.js";
3+
import {ExpressionData} from "./data/expressiondata.js";
4+
import {Evaluator} from "./evaluator.js";
5+
import {wellKnownFunctions} from "./funcs.js";
6+
import {FunctionDefinition, FunctionInfo} from "./funcs/info.js";
7+
import {Lexer, Token, TokenType} from "./lexer.js";
8+
import {Parser} from "./parser.js";
99

1010
export type CompletionItem = {
1111
label: string;

expressions/src/completion/descriptionDictionary.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {StringData} from "../data";
2-
import {DescriptionDictionary} from "./descriptionDictionary";
1+
import {StringData} from "../data/index.js";
2+
import {DescriptionDictionary} from "./descriptionDictionary.js";
33

44
describe("description dictionary", () => {
55
it("pairs contains all values", () => {

expressions/src/completion/descriptionDictionary.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {Dictionary} from "../data/dictionary";
2-
import {ExpressionData, Kind, Pair} from "../data/expressiondata";
1+
import {Dictionary} from "../data/dictionary.js";
2+
import {ExpressionData, Kind, Pair} from "../data/expressiondata.js";
33

44
export type DescriptionPair = Pair & {description?: string};
55

expressions/src/data/array.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {ExpressionData, ExpressionDataInterface, Kind, kindStr} from "./expressiondata";
1+
import {ExpressionData, ExpressionDataInterface, Kind, kindStr} from "./expressiondata.js";
22

33
export class Array implements ExpressionDataInterface {
44
private v: ExpressionData[] = [];

expressions/src/data/boolean.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {ExpressionDataInterface, Kind} from "./expressiondata";
1+
import {ExpressionDataInterface, Kind} from "./expressiondata.js";
22

33
export class BooleanData implements ExpressionDataInterface {
44
constructor(public readonly value: boolean) {}

expressions/src/data/dictionary.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {Dictionary} from "./dictionary";
2-
import {StringData} from "./string";
1+
import {Dictionary} from "./dictionary.js";
2+
import {StringData} from "./string.js";
33

44
describe("dictionary", () => {
55
it("pairs contains all values", () => {

0 commit comments

Comments
 (0)