Files
languageservices/expressions/src/xlang.test.ts
T
eric sciple 656a821a94 ESM migration: Add .js extensions for node16 moduleResolution (#257)
Migrate expressions, workflow-parser, and languageservice packages to use
proper ESM imports with .js extensions that work with node16 moduleResolution.

Changes:
- Update tsconfig.build.json in each package to use module: node16 and
  moduleResolution: node16
- Add .js extensions to all relative import paths (Option B approach)
- Fix yaml internal type imports in workflow-parser by defining local types
- Add skipLibCheck to handle @types/node compatibility issues
- Add TypeScript 5.8.3 override in root package.json
- Add ESM migration plan documentation

The languageserver package is deferred due to test hang issues that need
further investigation.

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

Test results:
- expressions: 1068 tests passed
- workflow-parser: 292 tests passed
- languageservice: 452 tests passed

* docs: update ESM migration plan with findings

- Update languageserver blocker: vscode-languageserver v8.0.2 lacks ESM
  exports (not a test hang issue)
- Document that Option B (manual .js extensions) was chosen over Option A
  due to ts-jest compatibility issues
- Add workaround for yaml package internal types (LinePos, NodeBase)
- Update migration status table with accurate reason for deferral
- Add skipLibCheck note for @types/node compatibility
2025-12-18 13:35:48 -06:00

181 lines
5.3 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
import {Expr} from "./ast.js";
import * as data from "./data/index.js";
import {kindStr} from "./data/expressiondata.js";
import {replacer} from "./data/replacer.js";
import {reviver} from "./data/reviver.js";
import {ExpressionError} from "./errors.js";
import {Evaluator} from "./evaluator.js";
import {Lexer, Result} from "./lexer.js";
import {Parser} from "./parser.js";
interface TestResult {
value: data.ExpressionData;
kind: string;
}
enum TestErrorKind {
Lexing = "lexing",
Parsing = "parsing",
Evaluation = "evaluation"
}
enum SkipOptionKind {
Typescript = "typescript"
}
interface TestOptions {
skip: SkipOptionKind[];
}
interface TestError {
value: string;
kind: TestErrorKind;
}
interface TestCase {
expr: string;
result: TestResult;
err: TestError | undefined;
contexts: data.Dictionary;
options: TestOptions;
}
function testCaseReviver(key: string, val: unknown): unknown {
if (key === "contexts") {
const tmp = JSON.stringify(val);
return JSON.parse(tmp, reviver);
}
if (key === "result") {
const t = val as TestResult;
t.value = reviver("value", t.value);
}
return val;
}
const testFiles = "./testdata";
describe("x-lang tests", () => {
const files = fs.readdirSync(testFiles);
for (const file of files) {
const fileName = path.join(testFiles, file);
const fileStat = fs.statSync(fileName);
if (fileStat.isDirectory()) {
throw new Error("sub-directories are not supported");
}
if (path.extname(fileName) !== ".json") {
throw new Error("only json files are supported " + file);
}
const testFile = fs.readFileSync(fileName, "utf8");
const testCases = JSON.parse(testFile, testCaseReviver) as {
[name: string]: TestCase[];
};
for (const testCaseName of Object.keys(testCases)) {
let tests = testCases[testCaseName];
// Filter out tests that are not supported by typescript
tests = tests.filter(t => !t.options?.skip?.includes(SkipOptionKind.Typescript));
const testName = path.basename(file, ".json");
if (tests.length === 0) {
continue;
}
describe(testName, () => {
test.each(tests)(`${testName}: ${testCaseName} ($expr)`, testCase => {
if (!testCase.contexts) {
testCase.contexts = new data.Dictionary();
}
const lexer = new Lexer(testCase.expr);
let result: Result;
try {
result = lexer.lex();
if (testCase.err && testCase.err.kind === TestErrorKind.Lexing) {
throw new Error("expected error lexing expression, but got none");
}
} catch (e) {
const err = e as Error;
// Did test expect lexing error? If so, compare error message.
if (testCase.err && testCase.err.kind === TestErrorKind.Lexing) {
expect(err.message).toContain(testCase.err.value);
return;
}
throw new Error(`unexpected error lexing expression: ${err.message} ${err.stack || ""}`);
}
// Parse
const contextNames = testCase.contexts.pairs().map(x => x.key);
const parser = new Parser(result.tokens, contextNames, []);
let expr: Expr;
try {
expr = parser.parse();
if (testCase.err?.kind === TestErrorKind.Parsing) {
throw new Error("expected error parsing expression, but got none");
}
} catch (e) {
// Did test expect parsing error?
if (testCase.err?.kind === TestErrorKind.Parsing) {
// Test expects parsing error
const pe = e as ExpressionError;
expect(errorWithExpression(pe, testCase.expr)).toContain(testCase.err.value);
return;
}
const err = e as Error;
throw new Error(`unexpected error parsing expression: ${err.message} ${err.stack || ""}`);
}
// Evaluate expression
try {
let result: data.ExpressionData;
if (expr !== undefined) {
const evaluator = new Evaluator(expr, testCase.contexts);
result = evaluator.evaluate();
} else {
result = new data.Null();
}
if (testCase.err?.kind === TestErrorKind.Evaluation) {
throw new Error("expected error evaluating expression, but got none");
}
expect(kindStr(result.kind)).toBe(testCase.result.kind);
expect(JSON.stringify(result, replacer)).toEqual(JSON.stringify(testCase.result.value, replacer));
} catch (e) {
if (testCase.err?.kind === TestErrorKind.Evaluation) {
const pe = e as ExpressionError;
expect(errorWithExpression(pe, testCase.expr)).toContain(testCase.err.value);
return;
}
const err = e as Error;
throw new Error(`unexpected error evaluating expression: ${err.message} ${err.stack || ""}`);
}
});
});
}
}
});
function errorWithExpression(e: ExpressionError, expr: string): string {
if (e.pos !== undefined) {
return `${e.message}. Located at position ${e.pos.column + 1} within expression: ${expr}`;
}
return `${e.message}. Located within expression: ${expr}`;
}