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
252 lines
7.5 KiB
TypeScript
252 lines
7.5 KiB
TypeScript
import {complete, CompletionItem, trimTokenVector} from "./completion.js";
|
|
import {DescriptionDictionary} from "./completion/descriptionDictionary.js";
|
|
import {BooleanData} from "./data/boolean.js";
|
|
import {Dictionary} from "./data/dictionary.js";
|
|
import {StringData} from "./data/string.js";
|
|
import {wellKnownFunctions} from "./funcs.js";
|
|
import {FunctionDefinition, FunctionInfo} from "./funcs/info.js";
|
|
import {Lexer, TokenType} from "./lexer.js";
|
|
|
|
const testContext = new Dictionary(
|
|
{
|
|
key: "env",
|
|
value: new Dictionary(
|
|
{
|
|
key: "FOO",
|
|
value: new StringData("")
|
|
},
|
|
{
|
|
key: "BAR_TEST",
|
|
value: new StringData("")
|
|
}
|
|
)
|
|
},
|
|
{
|
|
key: "github",
|
|
value: new DescriptionDictionary(
|
|
{
|
|
key: "actor",
|
|
value: new StringData(""),
|
|
description: "The name of the person or app that initiated the workflow. For example, octocat."
|
|
},
|
|
{
|
|
key: "inputs",
|
|
value: new DescriptionDictionary({
|
|
key: "name",
|
|
value: new StringData("monalisa"),
|
|
description: "The name of a person"
|
|
})
|
|
}
|
|
)
|
|
},
|
|
{
|
|
key: "secrets",
|
|
value: new Dictionary({
|
|
key: "AWS_TOKEN",
|
|
value: new BooleanData(true)
|
|
})
|
|
},
|
|
{
|
|
key: "vars",
|
|
value: new Dictionary({
|
|
key: "VAR_NAME",
|
|
value: new BooleanData(true)
|
|
})
|
|
},
|
|
{
|
|
key: "hashFiles(1,255)",
|
|
value: new Dictionary()
|
|
}
|
|
);
|
|
|
|
const testFunctions: FunctionInfo[] = [];
|
|
|
|
const testComplete = (input: string, functions?: Map<string, FunctionDefinition>): CompletionItem[] => {
|
|
const pos = input.indexOf("|");
|
|
input = input.replace("|", "");
|
|
|
|
const results = complete(input.slice(0, pos >= 0 ? pos : input.length), testContext, testFunctions, functions);
|
|
|
|
return results;
|
|
};
|
|
|
|
function completionItems(...labels: string[]): CompletionItem[] {
|
|
return labels.map(label => ({label, function: false}));
|
|
}
|
|
|
|
describe("auto-complete", () => {
|
|
describe("top-level", () => {
|
|
it("includes built-in functions", () => {
|
|
const expected: CompletionItem = {
|
|
label: "toJson",
|
|
description:
|
|
"`toJSON(value)`\n\nReturns a pretty-print JSON representation of `value`. You can use this function to debug the information provided in contexts.",
|
|
function: true
|
|
};
|
|
expect(testComplete("to")).toContainEqual(expected);
|
|
expect(testComplete("toJs")).toContainEqual(expected);
|
|
expect(testComplete("1 == toJS")).toContainEqual(expected);
|
|
expect(testComplete("1 == (toJS")).toContainEqual(expected);
|
|
expect(testComplete("toJS| == 1")).toContainEqual(expected);
|
|
expect(testComplete("(toJS| == (foo.bar)")).toContainEqual(expected);
|
|
expect(testComplete("(((toJS| == (foo.bar)")).toContainEqual(expected);
|
|
});
|
|
|
|
it("removes parentheses from passed in function context", () => {
|
|
expect(testComplete("|")).toContainEqual({
|
|
label: "hashFiles",
|
|
function: true
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("in multi-line expressions", () => {
|
|
it("includes built-in functions", () => {
|
|
expect(testComplete("1 == (\nto").map(x => x.label)).toContainEqual("toJson");
|
|
});
|
|
});
|
|
|
|
describe("functions", () => {
|
|
it("uses provided function definitions", () => {
|
|
expect(
|
|
testComplete(
|
|
"fromJson('invalid').|",
|
|
new Map(
|
|
Object.entries({
|
|
fromjson: {
|
|
...wellKnownFunctions.fromjson,
|
|
call: () =>
|
|
new Dictionary({
|
|
key: "foo",
|
|
value: new StringData("bar")
|
|
})
|
|
}
|
|
})
|
|
)
|
|
)
|
|
).toEqual<CompletionItem[]>([{label: "foo", function: false}]);
|
|
});
|
|
});
|
|
|
|
describe("for contexts", () => {
|
|
it("provides suggestions for top-level context", () => {
|
|
const expected = completionItems("BAR_TEST", "FOO");
|
|
expect(testComplete("env.X")).toEqual(expected);
|
|
expect(testComplete("1 == env.F")).toEqual(expected);
|
|
expect(testComplete("env.")).toEqual(expected);
|
|
expect(testComplete("env.FOO")).toEqual(expected);
|
|
expect(testComplete("(env).")).toEqual(expected);
|
|
});
|
|
|
|
it("provides suggestions for nested context", () => {
|
|
const expected: CompletionItem[] = [
|
|
{
|
|
label: "name",
|
|
function: false,
|
|
description: "The name of a person"
|
|
}
|
|
];
|
|
expect(testComplete("github.inputs.|")).toEqual(expected);
|
|
expect(testComplete("(github).inputs.|")).toEqual(expected);
|
|
expect(testComplete("(github.inputs).|")).toEqual(expected);
|
|
expect(testComplete("'test' == github.inputs.|")).toEqual(expected);
|
|
expect(testComplete("github.inputs.| == 'monalisa'")).toEqual(expected);
|
|
});
|
|
|
|
it("provides suggestions for secrets", () => {
|
|
const expected = completionItems("AWS_TOKEN");
|
|
|
|
expect(testComplete("secrets.A")).toEqual(expected);
|
|
expect(testComplete("1 == secrets.F")).toEqual(expected);
|
|
expect(testComplete("toJSON(secrets.")).toEqual(expected);
|
|
});
|
|
|
|
it("provides suggestions for variables", () => {
|
|
const expected = completionItems("VAR_NAME");
|
|
|
|
expect(testComplete("vars.V")).toEqual(expected);
|
|
expect(testComplete("1 == vars.F")).toEqual(expected);
|
|
expect(testComplete("toJSON(vars.")).toEqual(expected);
|
|
});
|
|
|
|
it("provides suggestions for contexts in function call", () => {
|
|
expect(testComplete("toJSON(env.|)")).toEqual(completionItems("BAR_TEST", "FOO"));
|
|
expect(testComplete("toJSON(secrets.")).toEqual(completionItems("AWS_TOKEN"));
|
|
});
|
|
|
|
describe("with descriptions", () => {
|
|
it("top-level", () => {
|
|
expect(testComplete("github.")).toContainEqual<CompletionItem>({
|
|
label: "actor",
|
|
function: false,
|
|
description: "The name of the person or app that initiated the workflow. For example, octocat."
|
|
});
|
|
});
|
|
|
|
it("nested", () => {
|
|
expect(testComplete("github.inputs.")).toContainEqual<CompletionItem>({
|
|
label: "name",
|
|
function: false,
|
|
description: "The name of a person"
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("trimTokenVector", () => {
|
|
test.each<{
|
|
input: string;
|
|
expected: TokenType[];
|
|
}>([
|
|
{
|
|
input: "env.",
|
|
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.EOF]
|
|
},
|
|
{
|
|
input: "github.act",
|
|
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
|
|
},
|
|
{
|
|
input: "1 == github.act",
|
|
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
|
|
},
|
|
{
|
|
input: "github.mona == github.act",
|
|
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
|
|
},
|
|
{
|
|
input: "github.mona == (github).act",
|
|
expected: [
|
|
TokenType.LEFT_PAREN,
|
|
TokenType.IDENTIFIER,
|
|
TokenType.RIGHT_PAREN,
|
|
TokenType.DOT,
|
|
TokenType.IDENTIFIER,
|
|
TokenType.EOF
|
|
]
|
|
},
|
|
{
|
|
input: "github.mona == (github.",
|
|
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.EOF]
|
|
},
|
|
{
|
|
input: "github['test'].",
|
|
expected: [
|
|
TokenType.IDENTIFIER,
|
|
TokenType.LEFT_BRACKET,
|
|
TokenType.STRING,
|
|
TokenType.RIGHT_BRACKET,
|
|
TokenType.DOT,
|
|
TokenType.EOF
|
|
]
|
|
}
|
|
])("$input", ({input, expected}) => {
|
|
const l = new Lexer(input);
|
|
const lr = l.lex();
|
|
|
|
const tv = trimTokenVector(lr.tokens);
|
|
expect(tv.map(x => x.type)).toEqual(expected);
|
|
});
|
|
});
|