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
370 lines
9.8 KiB
TypeScript
370 lines
9.8 KiB
TypeScript
import {isScalar, parseWorkflow} from "@actions/workflow-parser";
|
|
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
|
import {TokenType} from "@actions/workflow-parser/templates/tokens/types";
|
|
import {nullTrace} from "../nulltrace.js";
|
|
import {getPositionFromCursor} from "../test-utils/cursor-position.js";
|
|
import {findToken} from "./find-token.js";
|
|
|
|
type testTokenInfo = [definitionKey: string | null, tokenType: TokenType, literalValue?: string];
|
|
|
|
function getTokenInfo(token: TemplateToken | null): testTokenInfo | null {
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
token.definition?.key ?? null,
|
|
token.templateTokenType,
|
|
isScalar(token) ? token.toDisplayString() : undefined
|
|
].filter(x => x !== undefined) as testTokenInfo;
|
|
}
|
|
|
|
function testFindToken(input: string): {
|
|
parent: testTokenInfo | null;
|
|
key: testTokenInfo | null;
|
|
token: testTokenInfo | null;
|
|
path: testTokenInfo[];
|
|
} {
|
|
const [textDocument, pos] = getPositionFromCursor(input);
|
|
const result = parseWorkflow(
|
|
{
|
|
content: textDocument.getText(),
|
|
name: "wf.yaml"
|
|
},
|
|
nullTrace
|
|
);
|
|
|
|
const r = findToken(pos, result.value);
|
|
|
|
return {
|
|
parent: getTokenInfo(r.parent),
|
|
key: getTokenInfo(r.keyToken),
|
|
token: getTokenInfo(r.token),
|
|
path: r.path.map(x => getTokenInfo(x)!) // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
|
};
|
|
}
|
|
|
|
describe("find-token", () => {
|
|
it("on string key", () => {
|
|
expect(testFindToken(`o|n: push`)).toEqual({
|
|
path: [["workflow-root-strict", TokenType.Mapping]],
|
|
parent: ["workflow-root-strict", TokenType.Mapping],
|
|
key: null,
|
|
token: ["on-strict", TokenType.String, "on"]
|
|
});
|
|
});
|
|
|
|
it("on string value", () => {
|
|
expect(testFindToken(`on: pu|sh`)).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["on-strict", TokenType.String, "on"]
|
|
],
|
|
parent: ["workflow-root-strict", TokenType.Mapping],
|
|
key: ["on-strict", TokenType.String, "on"],
|
|
token: ["push-string", TokenType.String, "push"]
|
|
});
|
|
});
|
|
|
|
it("on mapping", () => {
|
|
expect(
|
|
testFindToken(`on:
|
|
pu|sh:`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["on-strict", TokenType.String, "on"],
|
|
["on-mapping-strict", TokenType.Mapping]
|
|
],
|
|
parent: ["on-mapping-strict", TokenType.Mapping],
|
|
key: null,
|
|
token: ["push", TokenType.String, "push"]
|
|
});
|
|
});
|
|
|
|
it("on sequence", () => {
|
|
expect(
|
|
testFindToken(`on:
|
|
- pu|sh`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["on-strict", TokenType.String, "on"],
|
|
["on-strict", TokenType.Sequence]
|
|
],
|
|
parent: ["on-strict", TokenType.Sequence],
|
|
key: null,
|
|
token: ["push-string", TokenType.String, "push"]
|
|
});
|
|
});
|
|
|
|
it("on sequence with cursor outside of sequence values", () => {
|
|
expect(
|
|
testFindToken(`on:
|
|
-| push`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["on-strict", TokenType.String, "on"],
|
|
["on-strict", TokenType.Sequence]
|
|
],
|
|
parent: ["on-strict", TokenType.Sequence],
|
|
key: null,
|
|
token: null
|
|
});
|
|
});
|
|
|
|
it("on sequence with multiple values", () => {
|
|
expect(
|
|
testFindToken(`on:
|
|
- push
|
|
- pull_request|`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["on-strict", TokenType.String, "on"],
|
|
["on-strict", TokenType.Sequence]
|
|
],
|
|
parent: ["on-strict", TokenType.Sequence],
|
|
key: null,
|
|
token: ["pull-request-string", TokenType.String, "pull_request"]
|
|
});
|
|
});
|
|
|
|
it("single-line sequence with multiple values", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
runs-on: [ubuntu-latest, self|]`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"],
|
|
["job-factory", TokenType.Mapping],
|
|
["runs-on", TokenType.String, "runs-on"],
|
|
["runs-on", TokenType.Sequence]
|
|
],
|
|
parent: ["runs-on", TokenType.Sequence],
|
|
key: null,
|
|
token: ["non-empty-string", TokenType.String, "self"]
|
|
});
|
|
});
|
|
|
|
it("jobs key", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jo|bs:
|
|
build:`)
|
|
).toEqual({
|
|
path: [["workflow-root-strict", TokenType.Mapping]],
|
|
parent: ["workflow-root-strict", TokenType.Mapping],
|
|
key: null,
|
|
token: ["jobs", TokenType.String, "jobs"]
|
|
});
|
|
});
|
|
|
|
it("value in job", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
runs-on: ubu|`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"],
|
|
["job-factory", TokenType.Mapping],
|
|
["runs-on", TokenType.String, "runs-on"]
|
|
],
|
|
parent: ["job-factory", TokenType.Mapping],
|
|
key: ["runs-on", TokenType.String, "runs-on"],
|
|
token: ["non-empty-string", TokenType.String, "ubu"]
|
|
});
|
|
});
|
|
|
|
it("key in job", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
run|s-on: ubu`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"],
|
|
["job-factory", TokenType.Mapping]
|
|
],
|
|
parent: ["job-factory", TokenType.Mapping],
|
|
key: null,
|
|
token: ["runs-on", TokenType.String, "runs-on"]
|
|
});
|
|
});
|
|
|
|
it("pos after colon in empty null mapping ", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
continue-on-error:|`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"],
|
|
["job-factory", TokenType.Mapping]
|
|
],
|
|
parent: ["job-factory", TokenType.Mapping],
|
|
key: ["boolean-strategy-context", TokenType.String, "continue-on-error"],
|
|
token: [null, TokenType.Null, ""]
|
|
});
|
|
});
|
|
|
|
it("pos after colon in empty string mapping", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
container:|`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"],
|
|
["job-factory", TokenType.Mapping]
|
|
],
|
|
parent: ["job-factory", TokenType.Mapping],
|
|
key: ["container", TokenType.String, "container"],
|
|
token: ["string", TokenType.String, ""]
|
|
});
|
|
});
|
|
|
|
it("pos after colon in mapping", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
continue-on-error:|foo`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"]
|
|
],
|
|
parent: ["jobs", TokenType.Mapping],
|
|
key: ["job", TokenType.String, "build"],
|
|
token: [null, TokenType.String, "continue-on-error:foo"]
|
|
});
|
|
});
|
|
|
|
it("pos after mapping key", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
continue-on-error:| foo`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"],
|
|
["job-factory", TokenType.Mapping]
|
|
],
|
|
parent: ["job-factory", TokenType.Mapping],
|
|
key: null,
|
|
token: null
|
|
});
|
|
});
|
|
|
|
it("pos at end of completed mapping key", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
continue-on-error|: foo`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"],
|
|
["job-factory", TokenType.Mapping]
|
|
],
|
|
parent: ["job-factory", TokenType.Mapping],
|
|
key: null,
|
|
token: ["boolean-strategy-context", TokenType.String, "continue-on-error"]
|
|
});
|
|
});
|
|
|
|
it("pos in mapping key without comment", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
runs-|`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"]
|
|
],
|
|
parent: ["jobs", TokenType.Mapping],
|
|
key: ["job", TokenType.String, "build"],
|
|
token: [null, TokenType.String, "runs-"]
|
|
});
|
|
});
|
|
|
|
it("pos in mapping key before comment", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
runs-|
|
|
#`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"]
|
|
],
|
|
parent: ["jobs", TokenType.Mapping],
|
|
key: ["job", TokenType.String, "build"],
|
|
token: [null, TokenType.String, "runs-"]
|
|
});
|
|
});
|
|
|
|
it("empty node", () => {
|
|
expect(
|
|
testFindToken(`on: push
|
|
jobs:
|
|
build:
|
|
concurrency:
|
|
runs-on: ubu|`)
|
|
).toEqual({
|
|
path: [
|
|
["workflow-root-strict", TokenType.Mapping],
|
|
["jobs", TokenType.String, "jobs"],
|
|
["jobs", TokenType.Mapping],
|
|
["job", TokenType.String, "build"],
|
|
["job-factory", TokenType.Mapping],
|
|
["runs-on", TokenType.String, "runs-on"]
|
|
],
|
|
parent: ["job-factory", TokenType.Mapping],
|
|
key: ["runs-on", TokenType.String, "runs-on"],
|
|
token: ["non-empty-string", TokenType.String, "ubu"]
|
|
});
|
|
});
|
|
});
|