mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-22 08:20:58 -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