mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-25 18:01:07 -05:00
Made a bunch of utility methods to work with Action metadata
This commit is contained in:
parent
938329d6a1
commit
21333ee75b
1 changed files with 683 additions and 0 deletions
683
src/utils/actions/action-metadata.ts
Normal file
683
src/utils/actions/action-metadata.ts
Normal file
|
@ -0,0 +1,683 @@
|
||||||
|
import { JS_MULTILINE_FRAME_STYLE, OptionalAutoGeneratedWarningFrameOptions, generateAutoGeneratedWarningFrame } from "@/utils/auto-generated";
|
||||||
|
import { DEFAULT_NEWLINE, UNIX_NEWLINE } from "@/utils/environment";
|
||||||
|
import { AsyncFilePath, AsyncReadFileOptions, AsyncReadFileOptionsObject, AsyncWriteFileOptionsObject } from "@/utils/io";
|
||||||
|
import { $i } from "@/utils/collections";
|
||||||
|
import { hashString } from "@/utils/string-utils";
|
||||||
|
import { TypeScriptComment, TypeScriptDocument, TypeScriptImport, TypeScriptInterface, TypeScriptTypeAlias, TypeScriptTypeLiteral, TypeScriptVariable, getIndentation, getNewline, getQuotes, incrementIndent } from "@/utils/typescript";
|
||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { basename } from "node:path";
|
||||||
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
||||||
|
import { ActionGroup, DEFAULT_ACTION_GROUP_DELIMITER } from "./action-group";
|
||||||
|
import { ActionInput, SYNTHETIC_UNDEFINED } from "./action-input";
|
||||||
|
import { ActionInputDescriptor, getActionInputDescriptors } from "./action-input-descriptor";
|
||||||
|
import { ActionOutput } from "./action-output";
|
||||||
|
import { ActionOutputDescriptor, getActionOutputDescriptors } from "./action-output-descriptor";
|
||||||
|
import { ActionParameter } from "./action-parameter";
|
||||||
|
import { ActionParameterDescriptor, ActionParameterDescriptorExtractionOptions } from "./action-parameter-descriptor";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes GitHub Action metadata file (`action.yml` or `action.yaml`).
|
||||||
|
*/
|
||||||
|
export interface ActionMetadata {
|
||||||
|
/**
|
||||||
|
* The name of your action.
|
||||||
|
*
|
||||||
|
* GitHub displays the `name` in the Actions tab to help visually identify actions in each job.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes types represented by input and output parameters of the action.
|
||||||
|
*
|
||||||
|
* @custom
|
||||||
|
*/
|
||||||
|
types?: {
|
||||||
|
/**
|
||||||
|
* Describes a type represented by input parameters of the action.
|
||||||
|
*/
|
||||||
|
input?: string | { name: string; description?: string; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes a type represented by output parameters of the action.
|
||||||
|
*/
|
||||||
|
output?: string | { name: string; description?: string; };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A short description of the action.
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the action's author.
|
||||||
|
*/
|
||||||
|
author?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures branding for the action.
|
||||||
|
*/
|
||||||
|
branding?: {
|
||||||
|
/**
|
||||||
|
* The background color of the badge.
|
||||||
|
*/
|
||||||
|
color: "white" | "yellow" | "blue" | "green" | "orange" | "red" | "purple" | "gray-dark";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the v4.28.0 Feather icon to use.
|
||||||
|
*/
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes groups for this action.
|
||||||
|
*
|
||||||
|
* @custom
|
||||||
|
*/
|
||||||
|
groups?: {
|
||||||
|
/**
|
||||||
|
* Describes input groups.
|
||||||
|
*/
|
||||||
|
input?: Record<string, ActionGroup>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes output groups.
|
||||||
|
*/
|
||||||
|
output?: Record<string, ActionGroup>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input parameters allow you to specify data that the action expects to use during runtime.
|
||||||
|
* GitHub stores input parameters as environment variables.
|
||||||
|
* Input ids with uppercase letters are converted to lowercase during runtime. We recommend using lowercase input ids.
|
||||||
|
*/
|
||||||
|
inputs?: Record<string, ActionInput>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output parameters allow you to declare data that an action sets.
|
||||||
|
* Actions that run later in a workflow can use the output data set in previously run actions.
|
||||||
|
* For example, if you had an action that performed the addition of two inputs (x + y = z), the action could output the sum (z) for other actions to use as an input.
|
||||||
|
*
|
||||||
|
* Outputs are Unicode strings, and can be a maximum of 1 MB. The total of all outputs in a workflow run can be a maximum of 50 MB.
|
||||||
|
*/
|
||||||
|
outputs?: Record<string, ActionOutput>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the path to the action's code and the runtime used to execute the code.
|
||||||
|
*/
|
||||||
|
runs: {
|
||||||
|
/**
|
||||||
|
* The runtime used to execute the code specified in `main`.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* Due to the deprecation of Node12, the available options are quite limited now.
|
||||||
|
*/
|
||||||
|
using: "node16";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file that contains your action code.
|
||||||
|
*
|
||||||
|
* The runtime specified in `using` executes this file.
|
||||||
|
*/
|
||||||
|
main: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents options available for configuring action metadata template processing.
|
||||||
|
*/
|
||||||
|
interface ActionMetadataTemplateProcessingOptions {
|
||||||
|
/**
|
||||||
|
* The delimiter to use when concatenating group and input/output names.
|
||||||
|
*
|
||||||
|
* Defaults to `"-"`.
|
||||||
|
*/
|
||||||
|
groupDelimiter?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether template-only fields such as `include`, `exclude`, and `unique`
|
||||||
|
* should be removed from groups and inputs/outputs.
|
||||||
|
*
|
||||||
|
* Defaults to `true`.
|
||||||
|
*/
|
||||||
|
removeTemplateOnlyFields?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for defining TypeScript definition for an action.
|
||||||
|
*/
|
||||||
|
interface ActionMetadataTypeScriptDefinitionOptions extends ActionParameterDescriptorExtractionOptions, OptionalAutoGeneratedWarningFrameOptions {
|
||||||
|
/**
|
||||||
|
* The name of the constant containing the action's name.
|
||||||
|
*
|
||||||
|
* Defaults to `"ACTION_NAME"`.
|
||||||
|
*/
|
||||||
|
actionNameConstant?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root path of the action.
|
||||||
|
*
|
||||||
|
* Defaults to `"./"`.
|
||||||
|
*/
|
||||||
|
rootPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean indicating whether or not to disable ESLint for the generated TypeScript code.
|
||||||
|
*
|
||||||
|
* - If `true`, a comment disabling ESLint will be added to the output.
|
||||||
|
* - If `false` (or not provided), no changes to ESLint configuration will be made.
|
||||||
|
*
|
||||||
|
* Defaults to `true`.
|
||||||
|
*/
|
||||||
|
disableESLint?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for defining TypeScript definition of the module loader required by an action.
|
||||||
|
*/
|
||||||
|
interface ActionMetadataModuleLoaderTypeScriptDefinitionOptions extends ActionMetadataTypeScriptDefinitionOptions {
|
||||||
|
/**
|
||||||
|
* The name of the module loader function.
|
||||||
|
*/
|
||||||
|
moduleLoaderName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents formatting options for YAML output.
|
||||||
|
*/
|
||||||
|
type YamlFormattingOptions = Exclude<Parameters<typeof stringifyYaml>[2], string | number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent options for formatting an action metadata template.
|
||||||
|
*/
|
||||||
|
type ActionMetadataFormattingOptions = ActionMetadataTemplateProcessingOptions & YamlFormattingOptions & OptionalAutoGeneratedWarningFrameOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents options available for reading, writing, processing, and formatting action metadata template files.
|
||||||
|
*/
|
||||||
|
type ActionMetadataTemplateFileProcessingOptions = ActionMetadataFormattingOptions & AsyncReadFileOptionsObject & AsyncWriteFileOptionsObject;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default root path to use if none is provided.
|
||||||
|
*/
|
||||||
|
const DEFAULT_ROOT_PATH = "./";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default action name constant name to use if none is provided.
|
||||||
|
*/
|
||||||
|
const DEFAULT_ACTION_NAME_CONSTANT_NAME = "ACTION_NAME";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default input type name to use if none is provided.
|
||||||
|
*/
|
||||||
|
const DEFAULT_INPUT_TYPE_NAME = "ActionInputs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default output type name to use if none is provided.
|
||||||
|
*/
|
||||||
|
const DEFAULT_OUTPUT_TYPE_NAME = "ActionOutputs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default module loader name.
|
||||||
|
*/
|
||||||
|
const DEFAULT_MODULE_LOADER_NAME = "ACTION_MODULE_LOADER";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link TypeScriptComment} object representing the comment to disable ESLint.
|
||||||
|
*
|
||||||
|
* Used when `disableESLint` option is set to `true`.
|
||||||
|
*/
|
||||||
|
const DISABLE_ES_LINT_COMMENT = TypeScriptComment.parse("/* eslint-disable */");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided YAML text as {@link ActionMetadata}.
|
||||||
|
*
|
||||||
|
* @param actionYamlText - The YAML text to parse.
|
||||||
|
*
|
||||||
|
* @returns The parsed {@link ActionMetadata} object.
|
||||||
|
* @throws An error if the provided YAML text is invalid.
|
||||||
|
*/
|
||||||
|
export function parseActionMetadataFromString(actionYamlText: string): ActionMetadata | never {
|
||||||
|
return parseYaml(actionYamlText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a YAML file at the provided path, and parses it as {@link ActionMetadata}.
|
||||||
|
*
|
||||||
|
* @param actionFile - The path to the YAML file to read.
|
||||||
|
* @param options - The options to use when reading the file.
|
||||||
|
*
|
||||||
|
* @returns The parsed {@link ActionMetadata} object.
|
||||||
|
* @throws An error if the file cannot be read or the YAML text is invalid.
|
||||||
|
*/
|
||||||
|
export async function parseActionMetadataFromFile(actionFile: AsyncFilePath, options?: AsyncReadFileOptions): Promise<ActionMetadata | never> {
|
||||||
|
const fileContent = (await readFile(actionFile, options)).toString();
|
||||||
|
return parseActionMetadataFromString(fileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes an Action Metadata Template by
|
||||||
|
*
|
||||||
|
* - Sanitizing inputs.
|
||||||
|
* - Grouping inputs/outputs into their respective groups.
|
||||||
|
* - Removing template-only fields, if requested
|
||||||
|
*
|
||||||
|
* @param template - The original action metadata template to be processed.
|
||||||
|
* @param options - An optional set of options used to configure how the template is processed.
|
||||||
|
*
|
||||||
|
* @returns A new action metadata based on the given template.
|
||||||
|
*/
|
||||||
|
export function processActionMetadataTemplate(template: ActionMetadata, options?: ActionMetadataTemplateProcessingOptions): ActionMetadata {
|
||||||
|
const groupDelimiter = options?.groupDelimiter ?? DEFAULT_ACTION_GROUP_DELIMITER;
|
||||||
|
const removeTemplateOnlyFields = options?.removeTemplateOnlyFields ?? true;
|
||||||
|
|
||||||
|
const metadata = { ...template };
|
||||||
|
metadata.inputs = sanitizeActionInputs(metadata.inputs);
|
||||||
|
if (metadata.groups) {
|
||||||
|
metadata.inputs = groupActionParameters(metadata.inputs, metadata.groups.input, groupDelimiter, { default: SYNTHETIC_UNDEFINED });
|
||||||
|
metadata.outputs = groupActionParameters(metadata.outputs, metadata.groups.output, groupDelimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removeTemplateOnlyFields) {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.groups) {
|
||||||
|
metadata.groups.input = removeTemplateOnlyActionFields(metadata.groups.input);
|
||||||
|
metadata.groups.output = removeTemplateOnlyActionFields(metadata.groups.output);
|
||||||
|
}
|
||||||
|
metadata.inputs = removeTemplateOnlyActionFields(metadata.inputs);
|
||||||
|
metadata.outputs = removeTemplateOnlyActionFields(metadata.outputs);
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes an Action Metadata Template YAML string, returning a stringified version of the processed template.
|
||||||
|
*
|
||||||
|
* @param templateYamlText - The YAML string containing the Action Metadata Template to process.
|
||||||
|
* @param options - An optional set of options to apply when processing the template.
|
||||||
|
*
|
||||||
|
* @returns A stringified version of the processed Action Metadata Template.
|
||||||
|
* @throws If parsing or processing the Action Metadata Template fails.
|
||||||
|
*/
|
||||||
|
export function processActionMetadataTemplateString(templateYamlText: string, options?: ActionMetadataFormattingOptions): string | never {
|
||||||
|
const newline = options?.newline ?? DEFAULT_NEWLINE;
|
||||||
|
const generateAutoGeneratedWarningMessage = options?.generateAutoGeneratedWarningMessage ?? true;
|
||||||
|
|
||||||
|
const parsedTemplate = parseActionMetadataFromString(templateYamlText);
|
||||||
|
const processedTemplate = processActionMetadataTemplate(parsedTemplate, options);
|
||||||
|
const stringifiedProcessedTemplate = stringifyYaml(processedTemplate, options);
|
||||||
|
|
||||||
|
const fixedStringifiedProcessedTemplate = newline === UNIX_NEWLINE ? stringifiedProcessedTemplate : stringifiedProcessedTemplate.replaceAll(UNIX_NEWLINE, newline);
|
||||||
|
const warningMessage = generateAutoGeneratedWarningMessage ? generateAutoGeneratedWarningFrame(options) : undefined;
|
||||||
|
const stringifiedProcessedTemplateWithWarning = [warningMessage, fixedStringifiedProcessedTemplate].filter(x => x).join(newline);
|
||||||
|
|
||||||
|
return stringifiedProcessedTemplateWithWarning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an Action Metadata Template YAML file, processes it, and writes the resulting metadata to a file.
|
||||||
|
*
|
||||||
|
* @param inputTemplateFile - The path to the input Action Metadata Template file.
|
||||||
|
* @param outputMetadataFile - The path to the output metadata file.
|
||||||
|
* @param options - An optional set of read/write options and processing options to apply.
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves when the metadata has been written to the output file, or rejects if any step fails.
|
||||||
|
* @throws If reading, parsing, processing, or writing the Action Metadata Template fails.
|
||||||
|
*/
|
||||||
|
export async function processActionMetadataTemplateFile(inputTemplateFile: AsyncFilePath, outputMetadataFile: AsyncFilePath, options?: ActionMetadataTemplateFileProcessingOptions): Promise<void | never> {
|
||||||
|
options = { sourceFileName: basename(inputTemplateFile.toString()), ...options };
|
||||||
|
|
||||||
|
const template = (await readFile(inputTemplateFile, options)).toString();
|
||||||
|
const stringifiedProcessedTemplate = processActionMetadataTemplateString(template, options);
|
||||||
|
await writeFile(outputMetadataFile, stringifiedProcessedTemplate, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups input/output values by their respective action groups, applying any specified group properties.
|
||||||
|
*
|
||||||
|
* @param groups - A dictionary of named action groups containing the list of input/output values to group.
|
||||||
|
* @param parameters - A dictionary of named input/output values to be grouped.
|
||||||
|
* @param groupDelimiter - The delimiter used to separate the group name from the value name in the output dictionary.
|
||||||
|
* @param properties - An optional set of input/output properties to apply to each grouped value.
|
||||||
|
*
|
||||||
|
* @returns A new dictionary of named input/output values grouped by their respective action groups.
|
||||||
|
*/
|
||||||
|
function groupActionParameters<T extends ActionParameter>(parameters: Record<string, T>, groups: Record<string, ActionGroup>, groupDelimiter: string, properties?: Partial<T>): Record<string, T> {
|
||||||
|
if (!groups || !parameters) {
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedValues = { ...parameters };
|
||||||
|
const namedGroups = Object.entries(groups);
|
||||||
|
const groupedValues = $i(Object.entries(parameters)).flatMap(
|
||||||
|
([vName, v]) => $i(namedGroups).map(([gName, g]) => [gName, g, vName, v] as [string, ActionGroup, string, T])
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [groupName, group, valueName, value] of groupedValues) {
|
||||||
|
const isForciblyIncluded = group.include?.includes(valueName);
|
||||||
|
const isForciblyExcluded = group.exclude?.includes(valueName);
|
||||||
|
const isPartOfGroup = namedGroups.some(([gName]) => valueName.startsWith(gName));
|
||||||
|
|
||||||
|
const shouldBeIncluded = (isForciblyIncluded || !value.unique && !isPartOfGroup) && !isForciblyExcluded;
|
||||||
|
if (!shouldBeIncluded) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedValueName = `${groupName}${groupDelimiter}${valueName}`;
|
||||||
|
const groupedRedirectName = value.redirect && `${groupName}${groupDelimiter}${value.redirect}`;
|
||||||
|
processedValues[groupedValueName] = {
|
||||||
|
...value,
|
||||||
|
redirect: groupedRedirectName,
|
||||||
|
...properties,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return processedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes an input dictionary by setting default values for undefined fields.
|
||||||
|
*
|
||||||
|
* @param inputs - A dictionary of named action inputs to be sanitized.
|
||||||
|
*
|
||||||
|
* @returns A new dictionary of sanitized named action inputs.
|
||||||
|
*/
|
||||||
|
function sanitizeActionInputs(inputs: Record<string, ActionInput>): Record<string, ActionInput> {
|
||||||
|
if (!inputs) {
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedInputs = { } as typeof inputs;
|
||||||
|
for (const [name, input] of Object.entries(inputs)) {
|
||||||
|
const copiedInput = { ...input };
|
||||||
|
if (typeof copiedInput.required !== "boolean") {
|
||||||
|
copiedInput.required = false;
|
||||||
|
}
|
||||||
|
if (copiedInput.default === undefined) {
|
||||||
|
copiedInput.default = SYNTHETIC_UNDEFINED;
|
||||||
|
}
|
||||||
|
sanitizedInputs[name] = copiedInput;
|
||||||
|
}
|
||||||
|
return sanitizedInputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes template-only fields from an action input/output/group dictionary.
|
||||||
|
*
|
||||||
|
* @param values - A dictionary of action input/output/group values to be cleaned.
|
||||||
|
*
|
||||||
|
* @returns A new dictionary of action input/output/group values with template-only fields removed.
|
||||||
|
*/
|
||||||
|
function removeTemplateOnlyActionFields<T extends ActionParameter | ActionGroup>(values: Record<string, T>): Record<string, T> {
|
||||||
|
if (!values) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedValues = { } as typeof values;
|
||||||
|
for (const [name, value] of Object.entries(values)) {
|
||||||
|
const copiedValue = { ...value };
|
||||||
|
delete (copiedValue as ActionGroup).include;
|
||||||
|
delete (copiedValue as ActionGroup).exclude;
|
||||||
|
delete (copiedValue as ActionParameter).unique;
|
||||||
|
|
||||||
|
cleanedValues[name] = copiedValue;
|
||||||
|
}
|
||||||
|
return cleanedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TypeScript definition for the given GitHub Action metadata.
|
||||||
|
*
|
||||||
|
* @param metadata - Metadata describing the inputs and outputs of a GitHub Action.
|
||||||
|
* @param options - Configuration options for generating the TypeScript definition.
|
||||||
|
*
|
||||||
|
* @returns The generated TypeScript document.
|
||||||
|
*/
|
||||||
|
export function createTypeScriptDefinitionForActionMetadata(metadata: ActionMetadata, options?: ActionMetadataTypeScriptDefinitionOptions): TypeScriptDocument {
|
||||||
|
const document = TypeScriptDocument.create();
|
||||||
|
|
||||||
|
const inputDescriptors = getActionInputDescriptors(metadata, options);
|
||||||
|
const inputGroups = inputDescriptors.length ? Object.entries(metadata.groups?.input || {}) : [];
|
||||||
|
|
||||||
|
const outputDescriptors = getActionOutputDescriptors(metadata, options);
|
||||||
|
const outputGroups = outputDescriptors.length ? Object.entries(metadata.groups?.output || {}) : [];
|
||||||
|
|
||||||
|
const rootPath = options?.rootPath ?? DEFAULT_ROOT_PATH;
|
||||||
|
const imports = [...inputDescriptors, ...outputDescriptors].map(x => createTypeScriptImportForActionParameter(x, rootPath)).filter(x => x);
|
||||||
|
imports.forEach(i => document.addImport(i));
|
||||||
|
|
||||||
|
const comments = createTypeScriptCommentsForActionMetadata(options);
|
||||||
|
comments.forEach(comment => document.addComment(comment));
|
||||||
|
|
||||||
|
const actionName = createTypeScriptConstantForActionName(metadata, options);
|
||||||
|
document.addExport(actionName);
|
||||||
|
|
||||||
|
const inputsInterface = inputDescriptors.length ? createTypeScriptInterfaceForActionInputs(metadata, inputDescriptors, options) : undefined;
|
||||||
|
const inputGroupAliases = inputGroups.map(([groupName, group]) => createTypeScriptAliasForActionGroup(group, groupName, inputsInterface.name, options));
|
||||||
|
[inputsInterface, ...inputGroupAliases].filter(x => x).forEach(node => document.addExport(node));
|
||||||
|
|
||||||
|
const outputInterface = outputDescriptors.length ? createTypeScriptInterfaceForActionOutputs(metadata, outputDescriptors, options) : undefined;
|
||||||
|
const outputGroupAliases = outputGroups.map(([groupName, group]) => createTypeScriptAliasForActionGroup(group, groupName, outputInterface.name, options));
|
||||||
|
[outputInterface, ...outputGroupAliases].filter(x => x).forEach(node => document.addExport(node));
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TypeScript constant representing the name of a GitHub Action.
|
||||||
|
*
|
||||||
|
* @param metadata - Metadata describing the GitHub Action.
|
||||||
|
* @param options - Configuration options for generating TypeScript constant.
|
||||||
|
*
|
||||||
|
* @returns The generated TypeScript constant representing the name of the GitHub Action..
|
||||||
|
*/
|
||||||
|
function createTypeScriptConstantForActionName(metadata: ActionMetadata, options?: ActionMetadataTypeScriptDefinitionOptions): TypeScriptVariable {
|
||||||
|
const q = getQuotes(options);
|
||||||
|
const name = options.actionNameConstant || DEFAULT_ACTION_NAME_CONSTANT_NAME;
|
||||||
|
|
||||||
|
const actionName = TypeScriptVariable.create(name, TypeScriptTypeLiteral.create(`${q}${metadata.name}${q}`));
|
||||||
|
if (metadata.description) {
|
||||||
|
actionName.addTSDoc(metadata.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates TypeScript comments for the GitHub Action metadata.
|
||||||
|
*
|
||||||
|
* @param options - Configuration options for generating TypeScript comments.
|
||||||
|
*
|
||||||
|
* @returns An array of generated comments.
|
||||||
|
*/
|
||||||
|
function createTypeScriptCommentsForActionMetadata(options?: ActionMetadataTypeScriptDefinitionOptions): TypeScriptComment[] {
|
||||||
|
const disableESLint = options?.disableESLint ?? true;
|
||||||
|
const generateAutoGeneratedWarningMessage = options?.generateAutoGeneratedWarningMessage ?? true;
|
||||||
|
|
||||||
|
const comments = [] as TypeScriptComment[];
|
||||||
|
if (generateAutoGeneratedWarningMessage) {
|
||||||
|
const autoGeneratedWarningMessage = generateAutoGeneratedWarningFrame({ style: JS_MULTILINE_FRAME_STYLE, ...options });
|
||||||
|
const autoGeneratedWarningComment = TypeScriptComment.parse(autoGeneratedWarningMessage);
|
||||||
|
comments.push(autoGeneratedWarningComment);
|
||||||
|
}
|
||||||
|
if (disableESLint) {
|
||||||
|
comments.push(DISABLE_ES_LINT_COMMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TypeScript interface for the inputs of a GitHub Action.
|
||||||
|
*
|
||||||
|
* @param metadata - Metadata describing the inputs of a GitHub Action.
|
||||||
|
* @param inputs - An iterable collection of input descriptors for the GitHub Action.
|
||||||
|
* @param pathExtractionOptions - Configuration options for extracting paths.
|
||||||
|
*
|
||||||
|
* @returns The generated TypeScript interface.
|
||||||
|
*/
|
||||||
|
function createTypeScriptInterfaceForActionInputs(metadata: ActionMetadata, inputs: Iterable<ActionInputDescriptor>, pathExtractionOptions?: ActionParameterDescriptorExtractionOptions): TypeScriptInterface {
|
||||||
|
const inputType = metadata.types?.input;
|
||||||
|
const typeName = (typeof inputType === "string" ? inputType : inputType?.name) || DEFAULT_INPUT_TYPE_NAME;
|
||||||
|
const typeDescription = (typeof inputType === "string" ? undefined : inputType?.description);
|
||||||
|
return createTypeScriptInterfaceForActionParameters(typeName, typeDescription, inputs, metadata.groups?.input, pathExtractionOptions, x => !x.required);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TypeScript interface for the outputs of a GitHub Action.
|
||||||
|
*
|
||||||
|
* @param metadata - Metadata describing the outputs of a GitHub Action.
|
||||||
|
* @param outputs - An iterable collection of output descriptors for the GitHub Action.
|
||||||
|
* @param pathExtractionOptions - Configuration options for extracting paths.
|
||||||
|
*
|
||||||
|
* @returns The generated TypeScript interface.
|
||||||
|
*/
|
||||||
|
function createTypeScriptInterfaceForActionOutputs(metadata: ActionMetadata, outputs: Iterable<ActionOutputDescriptor>, pathExtractionOptions?: ActionParameterDescriptorExtractionOptions): TypeScriptInterface {
|
||||||
|
const outputType = metadata.types?.output;
|
||||||
|
const typeName = (typeof outputType === "string" ? outputType : outputType?.name) || DEFAULT_OUTPUT_TYPE_NAME;
|
||||||
|
const typeDescription = (typeof outputType === "string" ? undefined : outputType?.description);
|
||||||
|
return createTypeScriptInterfaceForActionParameters(typeName, typeDescription, outputs, metadata.groups?.output, pathExtractionOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TypeScript interface for the parameters of a GitHub Action.
|
||||||
|
*
|
||||||
|
* @param name - The name of the interface.
|
||||||
|
* @param description - A description of the interface.
|
||||||
|
* @param parameters - An iterable collection of parameter descriptors of the GitHub Action.
|
||||||
|
* @param groups - A collection of action groups.
|
||||||
|
* @param pathExtractionOptions - Configuration options for extracting paths.
|
||||||
|
* @param isOptionalPredicate - A predicate function for determining if a parameter is optional.
|
||||||
|
*
|
||||||
|
* @returns The generated TypeScript interface.
|
||||||
|
*/
|
||||||
|
function createTypeScriptInterfaceForActionParameters<T extends ActionParameterDescriptor>(name: string, description: string, parameters: Iterable<T>, groups?: Record<string, ActionGroup>, pathExtractionOptions?: ActionParameterDescriptorExtractionOptions, isOptionalPredicate?: (p: T) => boolean): TypeScriptInterface {
|
||||||
|
isOptionalPredicate ||= () => false;
|
||||||
|
|
||||||
|
const tsInterface = TypeScriptInterface.create(name);
|
||||||
|
const tsInterfaceDefinition = tsInterface.definition;
|
||||||
|
if (description) {
|
||||||
|
tsInterface.addTSDoc(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parameter of parameters) {
|
||||||
|
if (parameter.redirect) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = parameter.path;
|
||||||
|
const type = TypeScriptTypeLiteral.create(`${parameter.type.name}${parameter.type.isArray ? "[]" : ""}`);
|
||||||
|
const isOptional = isOptionalPredicate(parameter);
|
||||||
|
|
||||||
|
const property = tsInterfaceDefinition.addNestedProperty(path, type, { isOptional });
|
||||||
|
if (parameter.description) {
|
||||||
|
property.addTSDoc(parameter.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [groupName, group] of Object.entries(groups || {})) {
|
||||||
|
if (!group.description) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = pathExtractionOptions?.pathParser?.(groupName) || [groupName];
|
||||||
|
const groupProperty = tsInterface.definition.getNestedProperty(path);
|
||||||
|
groupProperty?.addTSDoc(group.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tsInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TypeScript import for a parameter of a GitHub Action.
|
||||||
|
*
|
||||||
|
* @param parameter - A descriptor for an input or output parameter of the GitHub Action.
|
||||||
|
* @param rootPath - The root path for the import.
|
||||||
|
*
|
||||||
|
* @returns The generated TypeScript import, or `undefined` if no import is necessary.
|
||||||
|
*/
|
||||||
|
function createTypeScriptImportForActionParameter(parameter: ActionParameterDescriptor, rootPath?: string): TypeScriptImport | undefined {
|
||||||
|
if (!parameter.type.module || parameter.redirect) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modulePath = `${rootPath || ""}${parameter.type.module}`;
|
||||||
|
const tsImport = TypeScriptImport.createEmptyImport(modulePath);
|
||||||
|
if (parameter.type.isDefault) {
|
||||||
|
tsImport.defaultImportName = parameter.type.name;
|
||||||
|
} else {
|
||||||
|
tsImport.addNamedImport(parameter.type.name);
|
||||||
|
}
|
||||||
|
return tsImport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TypeScript type alias for a group of inputs or outputs of a GitHub Action.
|
||||||
|
*
|
||||||
|
* @param group - A group of inputs or outputs for the GitHub Action.
|
||||||
|
* @param groupName - The name of the group.
|
||||||
|
* @param referencedTypeName - The name of the type that the alias references.
|
||||||
|
* @param pathExtractionOptions - Configuration options for extracting paths.
|
||||||
|
*
|
||||||
|
* @returns The generated TypeScript type alias.
|
||||||
|
*/
|
||||||
|
function createTypeScriptAliasForActionGroup(group: ActionGroup, groupName: string, referencedTypeName: string, pathExtractionOptions?: ActionParameterDescriptorExtractionOptions): TypeScriptTypeAlias {
|
||||||
|
const path = pathExtractionOptions?.pathParser?.(groupName) || [groupName];
|
||||||
|
const mappedPath = path.map(x => `["${x}"]`).join("");
|
||||||
|
const groupAlias = TypeScriptTypeAlias.create(group.type, TypeScriptTypeLiteral.create(`${referencedTypeName}${mappedPath}`));
|
||||||
|
if (group.description) {
|
||||||
|
groupAlias.addTSDoc(group.description);
|
||||||
|
}
|
||||||
|
return groupAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TypeScript document containing a module loader function for the given action metadata.
|
||||||
|
*
|
||||||
|
* The module loader function loads modules required to parse the inputs and outputs of the action.
|
||||||
|
*
|
||||||
|
* @param metadata - The action metadata.
|
||||||
|
* @param options - The options for generating the TypeScript document.
|
||||||
|
*
|
||||||
|
* @returns A TypeScript document containing the module loader function and necessary imports.
|
||||||
|
*/
|
||||||
|
export function createModuleLoaderTypeScriptDefinitionForActionMetadata(metadata: ActionMetadata, options?: ActionMetadataModuleLoaderTypeScriptDefinitionOptions): TypeScriptDocument {
|
||||||
|
const document = TypeScriptDocument.create();
|
||||||
|
|
||||||
|
const inputDescriptors = getActionInputDescriptors(metadata, options);
|
||||||
|
const outputDescriptors = getActionOutputDescriptors(metadata, options);
|
||||||
|
const modules = $i<ActionParameterDescriptor>(inputDescriptors).concat(outputDescriptors)
|
||||||
|
.flatMap(x => [x.type.module, x.type.factory?.module])
|
||||||
|
.filter(x => x)
|
||||||
|
.distinct()
|
||||||
|
.map(x => [x, `_${hashString(x, "sha1")}`] as const)
|
||||||
|
.toMap();
|
||||||
|
|
||||||
|
const q = getQuotes(options);
|
||||||
|
const fallback = "return Promise.resolve(undefined);";
|
||||||
|
const conditions = $i(modules)
|
||||||
|
.map(([path, name]) => `if (path === ${q}${path}${q}) return Promise.resolve(${name});`)
|
||||||
|
.push(fallback);
|
||||||
|
|
||||||
|
const newline = getNewline(options);
|
||||||
|
const indent = getIndentation(incrementIndent(options));
|
||||||
|
const formattedConditions = conditions.map(x => `${indent}${x}`).join(newline);
|
||||||
|
const moduleLoaderBody = TypeScriptTypeLiteral.create(
|
||||||
|
`(path: string): Promise<Record<string, unknown>> => {${newline}${formattedConditions}${newline}};`
|
||||||
|
);
|
||||||
|
|
||||||
|
const moduleLoaderName = options?.moduleLoaderName || DEFAULT_MODULE_LOADER_NAME;
|
||||||
|
const moduleLoader = TypeScriptVariable.create(moduleLoaderName, moduleLoaderBody);
|
||||||
|
document.addExport(moduleLoader);
|
||||||
|
|
||||||
|
const rootPath = options?.rootPath ?? DEFAULT_ROOT_PATH;
|
||||||
|
const imports = $i(modules).map(([path, name]) => TypeScriptImport.createWildcardImport(`${rootPath}${path}`, name));
|
||||||
|
imports.forEach(x => document.addImport(x));
|
||||||
|
|
||||||
|
const comments = createTypeScriptCommentsForActionMetadata(options);
|
||||||
|
comments.forEach(comment => document.addComment(comment));
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
Loading…
Reference in a new issue