656a821a94
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
181 lines
5.3 KiB
TypeScript
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}`;
|
|
}
|