Files
languageservices/languageservice/src/utils/find-token.test.ts
T
eric scipleandGitHub 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

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"]
});
});
});