Files
languageservices/expressions/src/funcs/format.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

115 lines
3.3 KiB
TypeScript

import {ExpressionData, StringData} from "../data/index.js";
import {FunctionDefinition} from "./info.js";
export const format: FunctionDefinition = {
name: "format",
description:
"`format( string, replaceValue0, replaceValue1, ..., replaceValueN)`\n\nReplaces values in the `string`, with the variable `replaceValueN`. Variables in the `string` are specified using the `{N}` syntax, where `N` is an integer. You must specify at least one `replaceValue` and `string`. There is no maximum for the number of variables (`replaceValueN`) you can use. Escape curly braces using double braces.",
minArgs: 1,
maxArgs: 255 /*MAX_ARGUMENTS*/,
call: (...args: ExpressionData[]): ExpressionData => {
const fs = args[0].coerceString();
const result: string[] = [];
let index = 0;
while (index < fs.length) {
const lbrace = fs.indexOf("{", index);
const rbrace = fs.indexOf("}", index);
// Left brace
if (lbrace >= 0 && (rbrace < 0 || rbrace > lbrace)) {
// Escaped left brace
if (safeCharAt(fs, lbrace + 1) === "{") {
result.push(fs.substr(index, lbrace - index + 1));
index = lbrace + 2;
continue;
}
// Left brace, number, optional format specifiers, right brace
if (rbrace > lbrace + 1) {
const argIndex = readArgIndex(fs, lbrace + 1);
if (argIndex.success) {
// Check parameter count
if (1 + argIndex.result > args.length - 1) {
throw new Error(`The following format string references more arguments than were supplied: ${fs}`);
}
// Append the portion before the left brace
if (lbrace > index) {
result.push(fs.substr(index, lbrace - index));
}
// Append the arg
result.push(`${args[1 + argIndex.result].coerceString()}`);
index = rbrace + 1;
continue;
}
}
throw new Error(`The following format string is invalid: ${fs}`);
}
// Right brace
else if (rbrace >= 0) {
// Escaped right brace
if (safeCharAt(fs, rbrace + 1) === "}") {
result.push(fs.substr(index, rbrace - index + 1));
index = rbrace + 2;
} else {
throw new Error(`The following format string is invalid: ${fs}`);
}
}
// Last segment
else {
result.push(fs.substr(index));
break;
}
}
return new StringData(result.join(""));
}
};
function safeCharAt(string: string, index: number): string {
if (string.length > index) {
return string[index];
}
return "\0";
}
function readArgIndex(string: string, startIndex: number): ArgIndex {
// Count the number of digits
let length = 0;
for (;;) {
const nextChar = safeCharAt(string, startIndex + length);
if (nextChar >= "0" && nextChar <= "9") {
length++;
} else {
break;
}
}
// Validate at least one digit
if (length < 1) {
return <ArgIndex>{
success: false
};
}
// Parse the number
const endIndex = startIndex + length - 1;
const result = parseInt(string.substr(startIndex, length));
return <ArgIndex>{
success: !isNaN(result),
result: result,
endIndex: endIndex
};
}
interface ArgIndex {
success: boolean;
result: number;
endIndex: number;
}