diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 7f75c88..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - env: { - browser: true, - commonjs: true, - es2021: true, - jest: true, - }, - extends: [ - 'airbnb-base', - ], - parserOptions: { - ecmaVersion: 12, - }, - rules: { - 'no-use-before-define': 'off', - 'no-restricted-syntax': 'off', - }, -}; diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3f0bacb --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "parser": "@typescript-eslint/parser", + "env": { + "node": true, + "jest": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/strict" + ], + "parserOptions": { + "project": "./tsconfig.json", + "ecmaVersion": 2015 + }, + "rules": { + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/explicit-module-boundary-types": "error", + "@typescript-eslint/strict-boolean-expressions": "error", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/explicit-function-return-type": "error", + "no-nested-ternary": "error", + "no-use-before-define": "off", + "no-restricted-syntax": "off" + } +} diff --git a/jest.config.json b/jest.config.json new file mode 100644 index 0000000..b979fb1 --- /dev/null +++ b/jest.config.json @@ -0,0 +1,11 @@ +{ + "preset": "ts-jest", + "testEnvironment": "node", + "transform": { + "^.+\\.ts$": "ts-jest" + }, + "transformIgnorePatterns": [ + "/node_modules/" + ], + "moduleFileExtensions": ["ts", "js", "json", "node"] +} diff --git a/lib/comment.js b/lib/comment.js deleted file mode 100644 index 2fc1df9..0000000 --- a/lib/comment.js +++ /dev/null @@ -1,80 +0,0 @@ -const Mustache = require('mustache'); - -function commentMetadata(snippetIds) { - return ``; -} - -function extractCommentMetadata(commentBody) { - // snippet id regex plus a comma - const regex = //; - const match = regex.exec(commentBody); - - if (match) { - return match[1].split(',').map((s) => s.trim()).filter((s) => s !== ''); - } - return null; -} - -function assembleCommentBody(snippetIds, commentConfig, templateVariables = {}) { - let strings = [ - commentConfig.get('header'), - ...commentConfig.get('snippets').map((snippet) => { - if (snippetIds.includes(snippet.get('id'))) { - return snippet.get('body'); - } - return null; - }), - commentConfig.get('footer'), - commentMetadata(snippetIds), - ]; - - strings = strings.filter((s) => !!s); - - const rawCommentBody = strings.join('\n\n'); - - return Mustache.render(rawCommentBody, templateVariables); -} - -function newCommentDifferentThanPreviousComment(previousComment, snippetIds) { - const previousSnippetIds = extractCommentMetadata(previousComment.body); - - return previousSnippetIds.join(',') !== snippetIds.join(','); -} - -function newCommentWouldHaveContent(snippetIds) { - return snippetIds.length > 0; -} - -function shouldPostNewComment(previousComment, snippetIds, commentConfig) { - const isNotEmpty = newCommentWouldHaveContent(snippetIds); - const isCreating = !previousComment && commentConfig.get('onCreate') === 'create'; - const isUpdating = !!previousComment - && commentConfig.get('onUpdate') === 'recreate' - && newCommentDifferentThanPreviousComment(previousComment, snippetIds); - - return isNotEmpty && (isCreating || isUpdating); -} - -function shouldDeletePreviousComment(previousComment, snippetIds, commentConfig) { - return !!previousComment && ( - shouldPostNewComment(previousComment, snippetIds, commentConfig) - || (!newCommentWouldHaveContent(snippetIds) && commentConfig.get('onUpdate') !== 'nothing') - ); -} - -function shouldEditPreviousComment(previousComment, snippetIds, commentConfig) { - return newCommentWouldHaveContent(snippetIds) && ( - !!previousComment - && commentConfig.get('onUpdate') === 'edit' - && newCommentDifferentThanPreviousComment(previousComment, snippetIds) - ); -} - -module.exports = { - commentMetadata, - assembleCommentBody, - extractCommentMetadata, - shouldPostNewComment, - shouldDeletePreviousComment, - shouldEditPreviousComment, -}; diff --git a/lib/comment.ts b/lib/comment.ts new file mode 100644 index 0000000..865c31f --- /dev/null +++ b/lib/comment.ts @@ -0,0 +1,114 @@ +import * as Mustache from 'mustache'; +import {MinimatchOptions} from 'minimatch/dist/esm'; +import { Snippet } from './snippets'; +import { TemplateVariables } from "./config"; + +export type Comment = { + id: number; + url: string; + created_at: string; + body?: string; + body_text?: string; + body_html?: string; +} + +export interface CommentObject extends Map{ + get(key: 'header' | 'footer' | 'onCreate' | 'onUpdate'): string; + get(key: 'snippets'): Snippet[], + get(key: 'on-create' | 'on-update' | 'globOptions'): Map[]; + get(key: 'globOptions'): MinimatchOptions | undefined; +} + +function commentMetadata(snippetIds: string[]): string { + return ``; +} + +function extractCommentMetadata(commentBody: string): string[] | null { + // snippet id regex plus a comma + const regex = //; + const match = regex.exec(commentBody); + + if (match) { + return match[1].split(',').map((s: string) => s.trim()).filter((s) => s !== ''); + } + return null; +} + +function assembleCommentBody( + snippetIds: string[], + commentConfig: CommentObject, + templateVariables: TemplateVariables = {}): string { + let strings = [ + commentConfig.get('header'), + ...(commentConfig.get('snippets')).map(snippet => { + if (snippetIds.includes(snippet.get('id'))) { + return snippet.get('body'); + } + return null; + }), + commentConfig.get('footer'), + commentMetadata(snippetIds), + ]; + + strings = strings.filter((s) => Boolean(s)); + + const rawCommentBody = strings.join('\n\n'); + + return Mustache.render(rawCommentBody, templateVariables); +} + +function newCommentDifferentThanPreviousComment(previousComment: Comment, snippetIds: string[]): boolean { + const previousSnippetIds = (previousComment.body != null) ? extractCommentMetadata(previousComment.body) : null; + + return previousSnippetIds !== null && previousSnippetIds.join(',') !== snippetIds.join(','); +} + +function newCommentWouldHaveContent(snippetIds: string[]): boolean { + return snippetIds.length > 0; +} + +function shouldPostNewComment( + previousComment: Comment | null, + snippetIds: string[], + commentConfig: CommentObject +): boolean { + const isNotEmpty = newCommentWouldHaveContent(snippetIds); + const isCreating = !previousComment && commentConfig.get('onCreate') === 'create'; + const isUpdating = !!previousComment + && commentConfig.get('onUpdate') === 'recreate' + && newCommentDifferentThanPreviousComment(previousComment, snippetIds); + + return isNotEmpty && (isCreating || isUpdating); +} + +function shouldDeletePreviousComment( + previousComment: Comment | null, + snippetIds: string[], + commentConfig: CommentObject +): boolean { + return !!previousComment && ( + shouldPostNewComment(previousComment, snippetIds, commentConfig) + || (!newCommentWouldHaveContent(snippetIds) && commentConfig.get('onUpdate') !== 'nothing') + ); +} + +function shouldEditPreviousComment( + previousComment: Comment | null, + snippetIds: string[], + commentConfig: CommentObject +): boolean { + return newCommentWouldHaveContent(snippetIds) && ( + !!previousComment + && commentConfig.get('onUpdate') === 'edit' + && newCommentDifferentThanPreviousComment(previousComment, snippetIds) + ); +} + +export { + commentMetadata, + assembleCommentBody, + extractCommentMetadata, + shouldPostNewComment, + shouldDeletePreviousComment, + shouldEditPreviousComment, +}; diff --git a/lib/config.js b/lib/config.js deleted file mode 100644 index 16308d8..0000000 --- a/lib/config.js +++ /dev/null @@ -1,153 +0,0 @@ -const Mustache = require('mustache'); - -function validateCommentConfig(configObject, templateVariables) { - const configMap = new Map(); - - if (typeof configObject.comment !== 'object') { - throw Error( - `found unexpected value type '${typeof configObject.comment}' under key '.comment' (should be an object)`, - ); - } - - if (configObject.comment.header === undefined || configObject.comment.header === null || typeof configObject.comment.header === 'string') { - configMap.set('header', configObject.comment.header); - } else { - throw Error( - `found unexpected value type '${typeof configObject.comment.header}' under key '.comment.header' (should be a string)`, - ); - } - - const allowedOnCreateValues = ['create', 'nothing']; - if (configObject.comment['on-create'] === undefined || configObject.comment['on-create'] === null) { - configMap.set('onCreate', allowedOnCreateValues[0]); - } else if (typeof configObject.comment['on-create'] === 'string') { - const onCreate = Mustache.render(configObject.comment['on-create'], templateVariables); - - if (allowedOnCreateValues.includes(onCreate)) { - configMap.set('onCreate', onCreate); - } else { - throw Error( - `found unexpected value '${onCreate}' under key '.comment.on-create' (should be one of: ${allowedOnCreateValues.join(', ')})`, - ); - } - } else { - throw Error( - `found unexpected value type '${typeof configObject.comment['on-create']}' under key '.comment.on-create' (should be a string)`, - ); - } - - const allowedOnUpdateValues = ['recreate', 'edit', 'nothing']; - if (configObject.comment['on-update'] === undefined || configObject.comment['on-update'] === null) { - configMap.set('onUpdate', allowedOnUpdateValues[0]); - } else if (typeof configObject.comment['on-update'] === 'string') { - const onUpdate = Mustache.render(configObject.comment['on-update'], templateVariables); - - if (allowedOnUpdateValues.includes(onUpdate)) { - configMap.set('onUpdate', onUpdate); - } else { - throw Error( - `found unexpected value '${onUpdate}' under key '.comment.on-update' (should be one of: ${allowedOnUpdateValues.join(', ')})`, - ); - } - } else { - throw Error( - `found unexpected value type '${typeof configObject.comment['on-update']}' under key '.comment.on-update' (should be a string)`, - ); - } - - if (configObject.comment['glob-options'] && typeof configObject.comment['glob-options'] === 'object') { - configMap.set('globOptions', configObject.comment['glob-options']); - } - - if (configObject.comment.footer === undefined || configObject.comment.footer === null || typeof configObject.comment.footer === 'string') { - configMap.set('footer', configObject.comment.footer); - } else { - throw Error( - `found unexpected value type '${typeof configObject.comment.footer}' under key '.comment.footer' (should be a string)`, - ); - } - - if (Array.isArray(configObject.comment.snippets) && configObject.comment.snippets.length > 0) { - configMap.set('snippets', configObject.comment.snippets.map((snippetObject, index) => { - const snippetMap = new Map(); - - if (typeof snippetObject.id === 'string') { - const id = Mustache.render(snippetObject.id, templateVariables); - const regex = /^[A-Za-z0-9\-_,]*$/; - if (regex.exec(id)) { - snippetMap.set('id', id); - } else { - throw Error( - `found invalid snippet id '${id}' (snippet ids must contain only letters, numbers, dashes, and underscores)`, - ); - } - } else { - throw Error( - `found unexpected value type '${typeof snippetObject.id}' under key '.comment.snippets.${index}.id' (should be a string)`, - ); - } - - if (typeof snippetObject.body === 'string') { - snippetMap.set('body', snippetObject.body); - } else { - throw Error( - `found unexpected value type '${typeof snippetObject.body}' under key '.comment.snippets.${index}.body' (should be a string)`, - ); - } - - const isValidMatcher = (matcher) => { - const isAnyValid = !matcher.any - || (Array.isArray(matcher.any) && matcher.any.length > 0 && matcher.any.every((f) => typeof f === 'string')); - - const isAllValid = !matcher.all - || (Array.isArray(matcher.all) && matcher.all.length > 0 && matcher.all.every((f) => typeof f === 'string')); - - const isAtLeastOnePresent = (!!matcher.any || !!matcher.all); - - return typeof matcher === 'string' || (matcher && isAnyValid && isAllValid && isAtLeastOnePresent); - }; - const isValidFileList = (list) => Array.isArray(list) && list.length > 0; - - if (isValidFileList(snippetObject.files)) { - const list = snippetObject.files.map((matcher, matcherIndex) => { - if (isValidMatcher(matcher)) { - if (typeof matcher === 'string') { - return matcher; - } - const obj = {}; - if (matcher.any) { obj.any = matcher.any; } - if (matcher.all) { obj.all = matcher.all; } - return obj; - } - throw Error( - `found unexpected value type under key '.comment.snippets.${index}.files.${matcherIndex}' (should be a string or an object with keys 'all' and/or 'any')`, - ); - }); - snippetMap.set('files', list); - } else { - throw Error( - `found unexpected value type under key '.comment.snippets.${index}.files' (should be a non-empty array)`, - ); - } - - return snippetMap; - })); - - const snippetIds = configMap.get('snippets').map((s) => s.get('id')); - snippetIds.forEach((value, index, self) => { - if (self.indexOf(value) !== index) { - throw Error( - `found duplicate snippet id '${value}'`, - ); - } - }); - } else { - throw Error( - 'found unexpected value type under key \'.comment.snippets\' (should be a non-empty array)', - ); - } - - return configMap; -} - -module.exports = { validateCommentConfig }; diff --git a/lib/config.ts b/lib/config.ts new file mode 100644 index 0000000..7728689 --- /dev/null +++ b/lib/config.ts @@ -0,0 +1,185 @@ +import * as Mustache from 'mustache'; +import { CommentObject } from './comment'; +import { Snippet } from "./snippets"; + +export type MatchConfig = { + any?: string[]; + all?: string[]; +}; + +export type TemplateVariables = { + [key: string]: unknown; +} + +export type Config = { + comment: CommentConfig; +}; + +export interface CommentConfig extends CommentObject { + header: string | null; + footer: string | null; + snippets: Snippet[]; + 'on-create'?: string | null; + 'on-update'?: string | null; + 'glob-options'?: object; +} + +function validateCommentConfig(configObject: Config, templateVariables?: TemplateVariables): CommentObject { + const configMap: CommentObject = new Map(); + const comment: CommentConfig = configObject.comment; + + if (typeof comment !== 'object') { + throw Error( + `found unexpected value type '${typeof comment}' under key '.comment' (should be an object)`, + ); + } + + if (comment.header === undefined || comment.header === null || typeof comment.header === 'string') { + configMap.set('header', comment.header); + } else { + throw Error( + `found unexpected value type '${typeof comment.header}' under key '.comment.header' (should be a string)`, + ); + } + + const allowedOnCreateValues = ['create', 'nothing']; + if (comment['on-create'] === undefined || comment['on-create'] === null) { + configMap.set('onCreate', allowedOnCreateValues[0]); + } else if (typeof comment['on-create'] === 'string') { + const onCreate = Mustache.render(comment['on-create'], templateVariables); + + if (allowedOnCreateValues.includes(onCreate)) { + configMap.set('onCreate', onCreate); + } else { + throw Error( + `found unexpected value '${onCreate}' under key '.comment.on-create' (should be one of: ${allowedOnCreateValues.join(', ')})`, + ); + } + } else { + throw Error( + `found unexpected value type '${typeof comment['on-create']}' under key '.comment.on-create' (should be a string)`, + ); + } + + const allowedOnUpdateValues = ['recreate', 'edit', 'nothing']; + if (comment['on-update'] === undefined || comment['on-update'] === null) { + configMap.set('onUpdate', allowedOnUpdateValues[0]); + } else if (typeof comment['on-update'] === 'string') { + const onUpdate = Mustache.render(comment['on-update'], templateVariables); + + if (allowedOnUpdateValues.includes(onUpdate)) { + configMap.set('onUpdate', onUpdate); + } else { + throw Error( + `found unexpected value '${onUpdate}' under key '.comment.on-update' (should be one of: ${allowedOnUpdateValues.join(', ')})`, + ); + } + } else { + throw Error( + `found unexpected value type '${typeof comment['on-update']}' under key '.comment.on-update' (should be a string)`, + ); + } + + if (comment['glob-options'] && typeof comment['glob-options'] === 'object') { + configMap.set('globOptions', comment['glob-options']); + } + + if (comment.footer === undefined || comment.footer === null || typeof comment.footer === 'string') { + configMap.set('footer', comment.footer); + } else { + throw Error( + `found unexpected value type '${typeof comment.footer}' under key '.comment.footer' (should be a string)`, + ); + } + + if (Array.isArray(comment.snippets) && comment.snippets.length > 0) { + configMap.set('snippets', comment.snippets.map((snippetObject, index) => { + const snippetMap = new Map(); + + if (typeof snippetObject.id === 'string') { + const id = Mustache.render(snippetObject.id, templateVariables); + const regex = /^[A-Za-z0-9\-_,]*$/; + if (regex.exec(id)) { + snippetMap.set('id', id); + } else { + throw Error( + `found invalid snippet id '${id}' (snippet ids must contain only letters, numbers, dashes, and underscores)`, + ); + } + } else { + throw Error( + `found unexpected value type '${typeof snippetObject.id}' under key '.comment.snippets.${index}.id' (should be a string)` + ); + } + + if (typeof snippetObject.body === 'string') { + snippetMap.set('body', snippetObject.body); + } else { + throw Error( + `found unexpected value type '${typeof snippetObject.body}' under key '.comment.snippets.${index}.body' (should be a string)`, + ); + } + + const isValidMatcher = (matcher: string | MatchConfig): boolean => { + if (typeof matcher !== "string") { + const isAnyValid = !(matcher.any) || (Array.isArray(matcher.any) && matcher.any.length > 0 && matcher.any.every((f) => typeof f === 'string')); + + const isAllValid = !(matcher.all) || (Array.isArray(matcher.all) && matcher.all.length > 0 && matcher.all.every((f) => typeof f === 'string')); + + const isAtLeastOnePresent = ((Boolean(matcher.any)) || (Boolean(matcher.all))); + + return isAnyValid && isAllValid && isAtLeastOnePresent; + } + + return true; + }; + const isValidFileList = (list: (string | MatchConfig)[]): boolean => Array.isArray(list) && list.length > 0; + + if (isValidFileList(snippetObject.files)) { + const list = snippetObject.files.map((matcher, matcherIndex) => { + if (isValidMatcher(matcher)) { + if (typeof matcher === 'string') { + return matcher; + } + const obj: MatchConfig = {}; + if (matcher.any) { + obj.any = matcher.any; + } + if (matcher.all) { + obj.all = matcher.all; + } + return obj; + } else { + throw Error( + `found unexpected value type under key '.comment.snippets.${index}.files.${matcherIndex}' (should be a string or an object with keys 'all' and/or 'any')`, + ); + } + }); + snippetMap.set('files', list); + } else { + throw Error( + `found unexpected value type under key '.comment.snippets.${index}.files' (should be a non-empty array)`, + ); + } + + return snippetMap; + })); + + const snippetIds = (configMap.get('snippets')).map((s) => s.get('id')); + snippetIds.forEach((value: string, index: number, self: string[]) => { + if (self.indexOf(value) !== index) { + throw Error( + `found duplicate snippet id '${value}'`, + ); + } + }); + } else { + throw Error( + 'found unexpected value type under key \'.comment.snippets\' (should be a non-empty array)', + ); + } + + return configMap; +} + +export { validateCommentConfig }; diff --git a/lib/github.js b/lib/github.ts similarity index 53% rename from lib/github.js rename to lib/github.ts index 15c6ae0..27ec267 100644 --- a/lib/github.js +++ b/lib/github.ts @@ -1,16 +1,26 @@ -const github = require('@actions/github'); -const core = require('@actions/core'); +import * as github from '@actions/github'; +import * as core from '@actions/core'; +import { Comment } from './comment'; -async function deleteComment(client, comment) { - return client.rest.issues.deleteComment({ +export type OctokitClient = ReturnType; + +type RemoteDefinition = { + owner: string; + repo: string; + path: string; + ref: string; +} + +async function deleteComment(client: OctokitClient, comment: Comment): Promise { + await client.rest.issues.deleteComment({ owner: github.context.repo.owner, repo: github.context.repo.repo, comment_id: comment.id, }); } -async function editComment(client, comment, newBody) { - return client.rest.issues.updateComment({ +async function editComment(client: OctokitClient, comment: Comment, newBody: string): Promise { + await client.rest.issues.updateComment({ owner: github.context.repo.owner, repo: github.context.repo.repo, comment_id: comment.id, @@ -18,8 +28,8 @@ async function editComment(client, comment, newBody) { }); } -async function createComment(client, prNumber, body) { - return client.rest.issues.createComment({ +async function createComment(client: OctokitClient, prNumber: number, body: string): Promise { + await client.rest.issues.createComment({ owner: github.context.repo.owner, repo: github.context.repo.repo, issue_number: prNumber, @@ -27,14 +37,14 @@ async function createComment(client, prNumber, body) { }); } -async function getChangedFiles(client, prNumber) { +async function getChangedFiles(client: OctokitClient, prNumber: number): Promise { const listFilesOptions = client.rest.pulls.listFiles.endpoint.merge({ owner: github.context.repo.owner, repo: github.context.repo.repo, pull_number: prNumber, }); - const listFilesResponse = await client.paginate(listFilesOptions); + const listFilesResponse = await client.paginate<{filename: string}>(listFilesOptions); const changedFiles = listFilesResponse.map((f) => f.filename); core.debug('found changed files:'); @@ -45,8 +55,8 @@ async function getChangedFiles(client, prNumber) { return changedFiles; } -async function getFileContent(client, repoPath) { - let remoteDefn = { +async function getFileContent(client: OctokitClient, repoPath: string): Promise { + let remoteDefn: RemoteDefinition = { owner: github.context.repo.owner, repo: github.context.repo.repo, path: repoPath, @@ -59,7 +69,7 @@ async function getFileContent(client, repoPath) { if (match) { // eslint-disable-next-line no-unused-vars - const [_, org, repo, ref, path] = match; + const [, org, repo, ref, path] = match; remoteDefn = { owner: org, repo, @@ -73,10 +83,14 @@ async function getFileContent(client, repoPath) { const response = await client.rest.repos.getContent(remoteDefn); - return Buffer.from(response.data.content, response.data.encoding).toString(); + if ('content' in response.data && 'encoding' in response.data) { + return Buffer.from(response.data.content, response.data.encoding as BufferEncoding).toString(); + } + + throw new Error('Unexpected response format from getContent'); } -async function getComments(client, prNumber) { +async function getComments(client: OctokitClient, prNumber: number): Promise { const { data: comments } = await client.rest.issues.listComments({ owner: github.context.repo.owner, repo: github.context.repo.repo, @@ -86,7 +100,7 @@ async function getComments(client, prNumber) { return comments; } -module.exports = { +export { deleteComment, editComment, createComment, diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 97271b2..0000000 --- a/lib/index.js +++ /dev/null @@ -1,11 +0,0 @@ -const core = require('@actions/core'); -const { run } = require('./run'); - -(async () => { - try { - await run(); - } catch (error) { - core.error(error); - core.setFailed(error.message); - } -})(); diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..de2c67c --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,13 @@ +import * as core from '@actions/core'; +import { run } from './run'; + +(async (): Promise => { + try { + await run(); + } catch (error: unknown) { + if (error instanceof Error) { + core.error(error); + core.setFailed(error.message); + } + } +})(); diff --git a/lib/run.js b/lib/run.js deleted file mode 100644 index a717cf4..0000000 --- a/lib/run.js +++ /dev/null @@ -1,116 +0,0 @@ -const core = require('@actions/core'); -const github = require('@actions/github'); -const yaml = require('js-yaml'); -const { validateCommentConfig } = require('./config'); -const { getMatchingSnippetIds } = require('./snippets'); - -const { - assembleCommentBody, - extractCommentMetadata, - shouldPostNewComment, - shouldDeletePreviousComment, - shouldEditPreviousComment, -} = require('./comment'); - -const { - deleteComment, - editComment, - createComment, - getChangedFiles, - getFileContent, - getComments, -} = require('./github'); - -async function run() { - const token = core.getInput('github-token', { required: true }); - const configPath = core.getInput('config-file', { required: true }); - const templateVariablesJSONString = core.getInput('template-variables', { required: false }); - - const prNumber = getPrNumber(); - if (!prNumber) { - // eslint-disable-next-line no-console - console.log('Could not get pull request number from context, exiting'); - return; - } - - // eslint-disable-next-line new-cap - const client = new github.getOctokit(token); - - core.debug(`fetching changed files for pr #${prNumber}`); - const changedFiles = await getChangedFiles(client, prNumber); - const previousComment = await getPreviousPRComment(client, prNumber); - - let templateVariables = {}; - if (templateVariablesJSONString) { - core.debug('Input template-variables was passed'); - core.debug(templateVariablesJSONString); - - try { - templateVariables = JSON.parse(templateVariablesJSONString); - } catch (error) { - core.warning('Failed to parse template-variables input as JSON. Continuing without template variables.'); - } - } else { - core.debug('Input template-variables was not passed'); - } - - const commentConfig = await getCommentConfig(client, configPath, templateVariables); - const snippetIds = getMatchingSnippetIds(changedFiles, commentConfig); - - if (shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) { - core.info('removing previous comment'); - await deleteComment(client, previousComment); - } - - const commentBody = assembleCommentBody(snippetIds, commentConfig, templateVariables); - - if (shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) { - core.info('updating previous comment'); - await editComment(client, previousComment, commentBody); - } - - if (shouldPostNewComment(previousComment, snippetIds, commentConfig)) { - core.info('creating a new comment'); - await createComment(client, prNumber, commentBody); - } -} - -function getPrNumber() { - const pullRequest = github.context.payload.pull_request; - if (!pullRequest) { - return undefined; - } - - return pullRequest.number; -} - -async function getCommentConfig(client, configurationPath, templateVariables) { - const configurationContent = await getFileContent(client, configurationPath); - const configObject = yaml.load(configurationContent); - - // transform object to a map or throw if yaml is malformed: - const configMap = validateCommentConfig(configObject, templateVariables); - return configMap; -} - -async function getPreviousPRComment(client, prNumber) { - const comments = await getComments(client, prNumber); - core.debug(`there are ${comments.length} comments on the PR #${prNumber}`); - - const newestFirst = (c1, c2) => c2.created_at.localeCompare(c1.created_at); - const sortedComments = comments.sort(newestFirst); - const previousComment = sortedComments.find((c) => extractCommentMetadata(c.body) !== null); - - if (previousComment) { - const previousSnippetIds = extractCommentMetadata(previousComment.body); - - core.info(`found previous comment made by pr-commenter: ${previousComment.url}`); - core.info(`extracted snippet ids from previous comment: ${previousSnippetIds.join(', ')}`); - - return previousComment; - } - - return null; -} - -module.exports = { run }; diff --git a/lib/run.ts b/lib/run.ts new file mode 100644 index 0000000..f5bb888 --- /dev/null +++ b/lib/run.ts @@ -0,0 +1,122 @@ +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import * as yaml from 'js-yaml'; +import { Comment, CommentObject } from './comment'; +import { Config, TemplateVariables, validateCommentConfig } from './config'; +import { getMatchingSnippetIds } from './snippets'; +import { OctokitClient } from './github'; +import { + assembleCommentBody, + extractCommentMetadata, + shouldPostNewComment, + shouldDeletePreviousComment, + shouldEditPreviousComment, +} from './comment'; + +import { + deleteComment, + editComment, + createComment, + getChangedFiles, + getFileContent, + getComments, +} from './github'; + +async function run(): Promise { + try { + const token = core.getInput('github-token', {required: true}); + const configPath = core.getInput('config-file', {required: true}); + const templateVariablesJSONString = core.getInput('template-variables', {required: false}); + + const prNumber = getPrNumber(); + if (prNumber === undefined) { + console.log('Could not get pull request number from context, exiting'); + return; + } + + // eslint-disable-next-line new-cap + const client = github.getOctokit(token); + + core.debug(`fetching changed files for pr #${prNumber}`); + const changedFiles = await getChangedFiles(client, prNumber); + const previousComment = await getPreviousPRComment(client, prNumber); + + let templateVariables: TemplateVariables = {}; + if (templateVariablesJSONString) { + core.debug('Input template-variables was passed'); + core.debug(templateVariablesJSONString); + + try { + templateVariables = JSON.parse(templateVariablesJSONString); + } catch (error) { + core.warning('Failed to parse template-variables input as JSON. Continuing without template variables.'); + } + } else { + core.debug('Input template-variables was not passed'); + } + + const commentConfig = (await getCommentConfig(client, configPath, templateVariables)); + const snippetIds = getMatchingSnippetIds(changedFiles, commentConfig); + + if (previousComment && shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) { + core.info('removing previous comment'); + await deleteComment(client, previousComment); + } + + const commentBody = assembleCommentBody(snippetIds, commentConfig, templateVariables); + + if (previousComment && shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) { + core.info('updating previous comment'); + await editComment(client, previousComment, commentBody); + } + + if (shouldPostNewComment(previousComment, snippetIds, commentConfig)) { + core.info('creating a new comment'); + await createComment(client, prNumber, commentBody); + } + } catch (error: unknown) { + if (error instanceof Error) { + core.error(error); + core.setFailed((error).message); + } + } +} + +function getPrNumber(): number | undefined { + const pullRequest = github.context.payload.pull_request; + if (!pullRequest) { + return undefined; + } + + return pullRequest.number; +} + +async function getCommentConfig(client: OctokitClient, configurationPath: string, templateVariables: TemplateVariables): Promise { + const configurationContent = await getFileContent(client, configurationPath); + const configObject = yaml.load(configurationContent) as Config; + + // transform object to a map or throw if yaml is malformed: + return validateCommentConfig(configObject, templateVariables); +} + +async function getPreviousPRComment(client: OctokitClient, prNumber: number): Promise { + const comments = await getComments(client, prNumber); + core.debug(`there are ${comments.length} comments on the PR #${prNumber}`); + + const newestFirst = (a: Comment, b: Comment): number => b.created_at.localeCompare(a.created_at); + const sortedComments = comments.sort(newestFirst); + const previousComment = sortedComments.find((c) => c.body != null && extractCommentMetadata(c.body) !== null); + + if (previousComment?.body != null) { + const previousSnippetIds = extractCommentMetadata(previousComment.body); + + if (previousSnippetIds) { + core.info(`found previous comment made by pr-commenter: ${previousComment.url}`); + core.info(`extracted snippet ids from previous comment: ${previousSnippetIds.join(', ')}`); + } + } + + return previousComment ?? null; +} + +export { run }; diff --git a/lib/snippets.js b/lib/snippets.ts similarity index 65% rename from lib/snippets.js rename to lib/snippets.ts index 1ca0235..244b24b 100644 --- a/lib/snippets.js +++ b/lib/snippets.ts @@ -1,8 +1,20 @@ -const core = require('@actions/core'); -const { Minimatch } = require('minimatch'); - -function getMatchingSnippetIds(changedFiles, commentConfig) { - const snippetIds = commentConfig.get('snippets').reduce((acc, snippet) => { +import * as core from '@actions/core'; +import { Minimatch, MinimatchOptions } from "minimatch"; +import { CommentObject } from "./comment"; +import { MatchConfig } from "./config"; + +type files = (string | MatchConfig)[]; + +export type Snippet = { + id: string; + body: string; + get(key: 'id' | 'body'): string; + files: files; + get(key: 'files'): files; +}; + +function getMatchingSnippetIds(changedFiles: string[], commentConfig: CommentObject): string[] { + const snippetIds = commentConfig.get('snippets').reduce((acc, snippet): string[] => { core.debug(`processing snippet ${snippet.get('id')}`); if (checkGlobs(changedFiles, snippet.get('files'), commentConfig.get('globOptions') || {})) { @@ -16,7 +28,7 @@ function getMatchingSnippetIds(changedFiles, commentConfig) { return snippetIds; } -function toMatchConfig(config) { +function toMatchConfig(config: string | MatchConfig): MatchConfig { if (typeof config === 'string') { return { any: [config], @@ -26,11 +38,11 @@ function toMatchConfig(config) { return config; } -function printPattern(matcher) { +function printPattern(matcher: Minimatch): string { return (matcher.negate ? '!' : '') + matcher.pattern; } -function checkGlobs(changedFiles, globs, opts) { +function checkGlobs(changedFiles: string[], globs: (string | MatchConfig)[], opts: MinimatchOptions): boolean { for (const glob of globs) { core.debug(` checking pattern ${JSON.stringify(glob)}`); const matchConfig = toMatchConfig(glob); @@ -41,7 +53,7 @@ function checkGlobs(changedFiles, globs, opts) { return false; } -function isMatch(changedFile, matchers) { +function isMatch(changedFile: string, matchers: Minimatch[]): boolean { core.debug(` matching patterns against file ${changedFile}`); for (const matcher of matchers) { core.debug(` - ${printPattern(matcher)}`); @@ -56,7 +68,7 @@ function isMatch(changedFile, matchers) { } // equivalent to "Array.some()" but expanded for debugging and clarity -function checkAny(changedFiles, globs, opts) { +function checkAny(changedFiles: string[], globs: string[], opts: MinimatchOptions): boolean { const matchers = globs.map((g) => new Minimatch(g, opts)); core.debug(' checking "any" patterns'); for (const changedFile of changedFiles) { @@ -71,7 +83,7 @@ function checkAny(changedFiles, globs, opts) { } // equivalent to "Array.every()" but expanded for debugging and clarity -function checkAll(changedFiles, globs, opts) { +function checkAll(changedFiles: string[], globs: string[], opts: MinimatchOptions): boolean { const matchers = globs.map((g) => new Minimatch(g, opts)); core.debug(' checking "all" patterns'); for (const changedFile of changedFiles) { @@ -85,7 +97,7 @@ function checkAll(changedFiles, globs, opts) { return true; } -function checkMatch(changedFiles, matchConfig, opts) { +function checkMatch(changedFiles: string[], matchConfig: MatchConfig, opts: MinimatchOptions): boolean { if (matchConfig.all !== undefined) { if (!checkAll(changedFiles, matchConfig.all, opts)) { return false; @@ -101,4 +113,4 @@ function checkMatch(changedFiles, matchConfig, opts) { return true; } -module.exports = { getMatchingSnippetIds }; +export { getMatchingSnippetIds }; diff --git a/package-lock.json b/package-lock.json index 4aded49..f65fe57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pr-commenter-action", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pr-commenter-action", - "version": "1.5.0", + "version": "1.5.1", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", @@ -16,11 +16,20 @@ "mustache": "^4.2.0" }, "devDependencies": { + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.12", + "@types/js-yaml": "^4.0.9", + "@types/mustache": "^4.2.5", + "@types/node": "^22.5.1", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "@vercel/ncc": "^0.38.0", - "eslint": "^8.49.0 ", + "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.28.1", - "jest": "^29.7.0" + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "typescript": "^5.5.4" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -74,89 +83,18 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", @@ -203,14 +141,14 @@ "dev": true }, "node_modules/@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -233,63 +171,29 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.17.tgz", - "integrity": "sha512-XouDDhQESrLHTpnBtCKExJdyY4gJCdrvH2Pyv8r8kovX2U8G0dRUOT45T9XlbLtuu9CLXP15eusnkprhoPV5iQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.15" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" @@ -299,60 +203,49 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz", - "integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "engines": { "node": ">=6.9.0" @@ -373,14 +266,15 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", - "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -458,10 +352,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -530,12 +427,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -632,12 +529,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -647,34 +544,31 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.17.tgz", - "integrity": "sha512-xK4Uwm0JnAMvxYZxOVecss85WxTEIbTa7bnGyf/+EgCL5Zt3U7htUpEOWv9detPlamGKuRzCqw74xVglDWpPdg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.16", - "@babel/types": "^7.22.17", - "debug": "^4.1.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -691,13 +585,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", - "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.15", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -726,18 +620,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", - "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -780,22 +674,23 @@ } }, "node_modules/@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -838,9 +733,10 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -1230,14 +1126,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -1253,9 +1149,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1268,9 +1164,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1511,18 +1407,43 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "node_modules/@types/node": { - "version": "20.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", - "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==", + "node_modules/@types/mustache": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==", "dev": true }, + "node_modules/@types/node": { + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", + "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -1544,6 +1465,209 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vercel/ncc": { "version": "0.38.0", "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.0.tgz", @@ -1554,9 +1678,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1691,6 +1815,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", @@ -1767,6 +1900,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1948,6 +2087,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -2261,6 +2412,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2273,6 +2436,21 @@ "node": ">=6.0.0" } }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.519", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.519.tgz", @@ -2421,18 +2599,19 @@ } }, "node_modules/eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -2789,6 +2968,34 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2831,6 +3038,27 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3066,9 +3294,9 @@ } }, "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3095,6 +3323,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -3216,9 +3464,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -3685,6 +3933,46 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -4399,6 +4687,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4462,6 +4756,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -4477,6 +4777,15 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -4500,9 +4809,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4851,6 +5160,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4991,9 +5309,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -5535,6 +5853,78 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -5674,6 +6064,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -5689,6 +6092,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", diff --git a/package.json b/package.json index 45ec198..5d728aa 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "build": "ncc build lib/index.js --license licenses.txt", - "lint": "eslint lib/**/*.js test/**/*.js", + "build": "ncc build lib/index.ts --license licenses.txt", + "lint": "eslint lib/**/*.ts test/**/*.ts", "test": "jest" }, "repository": { @@ -27,10 +27,18 @@ "mustache": "^4.2.0" }, "devDependencies": { + "@types/jest": "^29.5.12", + "@types/js-yaml": "^4.0.9", + "@types/mustache": "^4.2.5", + "@types/node": "^22.5.1", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "@vercel/ncc": "^0.38.0", - "eslint": "^8.49.0 ", + "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.28.1", - "jest": "^29.7.0" + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "typescript": "^5.5.4" } } diff --git a/test/comment.test.js b/test/comment.test.ts similarity index 66% rename from test/comment.test.js rename to test/comment.test.ts index e39bce1..3144699 100644 --- a/test/comment.test.js +++ b/test/comment.test.ts @@ -1,22 +1,23 @@ -const comment = require('../lib/comment'); +import * as comment from '../lib/comment'; +import {CommentObject} from "../lib/comment"; describe('comment', () => { describe('assembleCommentBody', () => { test('header, footer, and many snippets', () => { - const commentConfig = new Map([ + const commentConfig = new Map([ ['header', 'hello'], ['footer', 'bye'], ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['body', 'A list:\n- one\n- two\n- three'], ]), - new Map([ + new Map([ ['id', 'snippet2'], ['body', 'Do not forget to be awesome!'], ]), ]], - ]); + ]) as CommentObject; expect(comment.assembleCommentBody(['snippet1'], commentConfig)).toEqual( 'hello\n\n' @@ -35,20 +36,20 @@ describe('comment', () => { }); test('no header', () => { - const commentConfig = new Map([ + const commentConfig = new Map([ ['header', undefined], ['footer', 'bye'], ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['body', 'A list:\n- one\n- two\n- three'], ]), - new Map([ + new Map([ ['id', 'snippet2'], ['body', 'Do not forget to be awesome!'], ]), ]], - ]); + ]) as CommentObject; expect(comment.assembleCommentBody(['snippet1', 'snippet2'], commentConfig)).toEqual( 'A list:\n- one\n- two\n- three\n\n' @@ -59,20 +60,20 @@ describe('comment', () => { }); test('no footer', () => { - const commentConfig = new Map([ + const commentConfig = new Map([ ['header', 'hello'], ['footer', undefined], ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['body', 'A list:\n- one\n- two\n- three'], ]), - new Map([ + new Map([ ['id', 'snippet2'], ['body', 'Do not forget to be awesome!'], ]), ]], - ]); + ]) as CommentObject; expect(comment.assembleCommentBody(['snippet1', 'snippet2'], commentConfig)).toEqual( 'hello\n\n' @@ -83,20 +84,20 @@ describe('comment', () => { }); test('no header and no footer', () => { - const commentConfig = new Map([ + const commentConfig = new Map([ ['header', undefined], ['footer', undefined], ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['body', 'A list:\n- one\n- two\n- three'], ]), - new Map([ + new Map([ ['id', 'snippet2'], ['body', 'Do not forget to be awesome!'], ]), ]], - ]); + ]) as CommentObject; expect(comment.assembleCommentBody(['snippet1', 'snippet2'], commentConfig)).toEqual( 'A list:\n- one\n- two\n- three\n\n' @@ -106,16 +107,16 @@ describe('comment', () => { }); test('supports Mustache templates', () => { - const commentConfig = new Map([ + const commentConfig = new Map([ ['header', 'hello {{user.firstName}}'], ['footer', 'bye {{user.firstName}}'], ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['body', 'A list:\n- one\n- two\n{{#shouldIncludeThirdItem}}- three\n{{/shouldIncludeThirdItem}}'], ]), ]], - ]); + ]) as CommentObject; const templateVariables = { user: { @@ -143,7 +144,7 @@ describe('comment', () => { test('finds an empty array in the middle of a comment', () => { const commentBody = 'hello\nthere\n\nblabla'; - const expectedResult = []; + const expectedResult: unknown = []; expect(comment.extractCommentMetadata(commentBody)).toEqual(expectedResult); }); @@ -186,42 +187,54 @@ describe('comment', () => { describe('comment create/delete/edit logic', () => { test('no previous comment, no snippets detected', () => { - const previousComment = undefined; - const snippetIds = []; + const previousComment: unknown = undefined; + const snippetIds: string[] = []; - let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'nothing']]); + commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'nothing']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); }); @@ -230,39 +243,51 @@ describe('comment', () => { const previousComment = undefined; const snippetIds = ['snippet1', 'snippet2']; - let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'nothing']]); + commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'nothing']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); }); @@ -271,39 +296,51 @@ describe('comment', () => { const previousComment = { body: comment.commentMetadata(['snippet1', 'snippet2']) }; const snippetIds = ['snippet1', 'snippet2']; - let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'recreate']]); + commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'recreate']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); }); @@ -312,80 +349,104 @@ describe('comment', () => { const previousComment = { body: comment.commentMetadata(['snippet2']) }; const snippetIds = ['snippet1', 'snippet2']; - let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(true); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'recreate']]); + commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'recreate']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); }); test('a previous comment, new comment would have no snippets', () => { const previousComment = { body: comment.commentMetadata(['snippet2']) }; - const snippetIds = []; + const snippetIds: string[] = []; - let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + let commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'recreate']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'edit']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + commentConfig = new Map([['onCreate', 'create'], ['onUpdate', 'nothing']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); - commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'edit']]); + commentConfig = new Map([['onCreate', 'nothing'], ['onUpdate', 'edit']]); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldPostNewComment(previousComment, snippetIds, commentConfig)) .toEqual(false); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldDeletePreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(true); + // @ts-expect-error: Intentional type violation for testing expect(comment.shouldEditPreviousComment(previousComment, snippetIds, commentConfig)) .toEqual(false); }); diff --git a/test/config.test.js b/test/config.test.ts similarity index 85% rename from test/config.test.js rename to test/config.test.ts index 128179f..b24dd48 100644 --- a/test/config.test.js +++ b/test/config.test.ts @@ -1,4 +1,5 @@ -const config = require('../lib/config'); +import * as config from '../lib/config'; +import {Config} from "../lib/config"; describe('validateCommentConfig', () => { const snippet1Object = { @@ -43,19 +44,19 @@ describe('validateCommentConfig', () => { }], }; - const snippet1Map = new Map([ + const snippet1Map = new Map([ ['id', 'snippet1'], ['body', 'let me tell you something...'], ['files', ['/foo/bar.txt', '/foo/baz/*']], ]); - const snippet2Map = new Map([ + const snippet2Map = new Map([ ['id', 'snippet2'], ['body', 'once upon a time...'], ['files', ['/pizzazz/**/*.html']], ]); - const snippet3Map = new Map([ + const snippet3Map = new Map([ ['id', 'snippet3'], ['body', 'there was a wolf...'], ['files', [{ @@ -63,7 +64,7 @@ describe('validateCommentConfig', () => { }]], ]); - const snippet4Map = new Map([ + const snippet4Map = new Map([ ['id', 'snippet4'], ['body', 'something something...'], ['files', [{ @@ -71,7 +72,7 @@ describe('validateCommentConfig', () => { }]], ]); - const snippet5Map = new Map([ + const snippet5Map = new Map([ ['id', 'snippet5'], ['body', 'something something...'], ['files', [{ @@ -89,9 +90,9 @@ describe('validateCommentConfig', () => { footer: 'bye', snippets: [snippet1Object, snippet2Object, snippet3Object, snippet4Object, snippet5Object], }, - }; + } as Config; - const output = new Map([ + const output = new Map([ ['onCreate', 'nothing'], ['onUpdate', 'nothing'], ['header', 'hello'], @@ -103,6 +104,7 @@ describe('validateCommentConfig', () => { }); test('removes unknown keys', () => { + // @ts-expect-error: Intentional type violation for testing const input = { comment: { 'on-update': 'nothing', @@ -112,9 +114,9 @@ describe('validateCommentConfig', () => { id: '342', snippets: [snippet1Object, snippet2Object], }, - }; + } as Config; - const output = new Map([ + const output = new Map([ ['onCreate', 'create'], ['onUpdate', 'nothing'], ['header', 'hello'], @@ -133,9 +135,9 @@ describe('validateCommentConfig', () => { footer: 'bye', snippets: [snippet2Object], }, - }; + } as Config; - const output = new Map([ + const output = new Map([ ['onCreate', 'create'], ['onUpdate', 'edit'], ['header', 'hi'], @@ -155,7 +157,7 @@ describe('validateCommentConfig', () => { }, }; - const output = new Map([ + const output = new Map([ ['onCreate', 'create'], ['onUpdate', 'edit'], ['header', undefined], @@ -163,6 +165,7 @@ describe('validateCommentConfig', () => { ['snippets', [snippet2Map]], ]); + // @ts-expect-error: Intentional type violation for testing expect(config.validateCommentConfig(input)).toEqual(output); }); @@ -175,7 +178,7 @@ describe('validateCommentConfig', () => { }, }; - const output = new Map([ + const output = new Map([ ['onCreate', 'create'], ['onUpdate', 'edit'], ['header', 'hello'], @@ -183,6 +186,7 @@ describe('validateCommentConfig', () => { ['snippets', [snippet2Map]], ]); + // @ts-expect-error: Intentional type violation for testing expect(config.validateCommentConfig(input)).toEqual(output); }); @@ -193,6 +197,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type 'number' under key '\.comment\.header' \(should be a string\)/); }); @@ -203,6 +208,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type 'number' under key '\.comment\.footer' \(should be a string\)/); }); @@ -213,6 +219,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value 'sup' under key '\.comment\.on-create' \(should be one of: create, nothing\)/); }); @@ -223,6 +230,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type 'object' under key '\.comment\.on-create' \(should be a string\)/); }); @@ -234,11 +242,11 @@ describe('validateCommentConfig', () => { footer: 'bye', snippets: [snippet2Object], }, - }; + } as Config; const templateVariables = { onCreate: 'nothing' }; - const output = new Map([ + const output = new Map([ ['onCreate', 'nothing'], ['onUpdate', 'recreate'], ['header', 'hi'], @@ -258,6 +266,7 @@ describe('validateCommentConfig', () => { const templateVariables = { onCreate: '1234' }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input, templateVariables)).toThrow(/found unexpected value '1234' under key '\.comment\.on-create' \(should be one of: create, nothing\)/); }); @@ -268,6 +277,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value 'whatever' under key '\.comment\.on-update' \(should be one of: recreate, edit, nothing\)/); }); @@ -278,6 +288,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type 'number' under key '\.comment\.on-update' \(should be a string\)/); }); @@ -289,11 +300,11 @@ describe('validateCommentConfig', () => { footer: 'bye', snippets: [snippet2Object], }, - }; + } as Config; const templateVariables = { onUpdate: 'nothing' }; - const output = new Map([ + const output = new Map([ ['onCreate', 'create'], ['onUpdate', 'nothing'], ['header', 'hi'], @@ -313,6 +324,7 @@ describe('validateCommentConfig', () => { const templateVariables = { onUpdate: 'cat' }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input, templateVariables)).toThrow(/found unexpected value 'cat' under key '\.comment\.on-update' \(should be one of: recreate, edit, nothing\)/); }); @@ -327,7 +339,7 @@ describe('validateCommentConfig', () => { }, }; - const output = new Map([ + const output = new Map([ ['onCreate', 'create'], ['onUpdate', 'recreate'], ['header', undefined], @@ -339,6 +351,7 @@ describe('validateCommentConfig', () => { ['snippets', [snippet1Map]], ]); + // @ts-expect-error: Intentional type violation for testing expect(config.validateCommentConfig(input)).toEqual(output); }); @@ -347,6 +360,7 @@ describe('validateCommentConfig', () => { comment: {}, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type under key '\.comment\.snippets' \(should be a non-empty array\)/); }); @@ -357,6 +371,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type under key '\.comment\.snippets' \(should be a non-empty array\)/); }); @@ -367,6 +382,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type under key '\.comment\.snippets' \(should be a non-empty array\)/); }); @@ -382,6 +398,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type 'undefined' under key '\.comment\.snippets\.0\.id' \(should be a string\)/); }); @@ -398,6 +415,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found invalid snippet id 'can this have spaces\?' \(snippet ids must contain only letters, numbers, dashes, and underscores\)/); }); @@ -416,13 +434,13 @@ describe('validateCommentConfig', () => { const templateVariables = { date: '2021-08-27' }; - const snippets = [new Map([ + const snippets = [new Map([ ['id', 'snippet-2021-08-27'], ['body', 'something something...'], ['files', ['foo.txt']], ])]; - const expected = new Map([ + const expected = new Map([ ['footer', undefined], ['header', undefined], ['onCreate', 'create'], @@ -430,6 +448,7 @@ describe('validateCommentConfig', () => { ['snippets', snippets], ]); + // @ts-expect-error: Intentional type violation for testing expect(config.validateCommentConfig(input, templateVariables)).toEqual(expected); }); @@ -448,6 +467,7 @@ describe('validateCommentConfig', () => { const templateVariables = { date: '2021 08 27' }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input, templateVariables)).toThrow(/found invalid snippet id 'snippet-2021 08 27' \(snippet ids must contain only letters, numbers, dashes, and underscores\)/); }); @@ -473,6 +493,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type 'undefined' under key '\.comment\.snippets\.1\.id' \(should be a string\)/); }); @@ -488,6 +509,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type 'undefined' under key '\.comment\.snippets\.0\.body' \(should be a string\)/); }); @@ -503,6 +525,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type under key '\.comment\.snippets\.0\.files' \(should be a non-empty array\)/); }); @@ -519,6 +542,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type under key '\.comment\.snippets\.0\.files' \(should be a non-empty array\)/); }); @@ -535,6 +559,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type under key '\.comment\.snippets\.0\.files\.1' \(should be a string or an object with keys 'all' and\/or 'any'\)/); }); @@ -553,6 +578,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type under key '\.comment\.snippets\.0\.files\.0' \(should be a string or an object with keys 'all' and\/or 'any'\)/); }); @@ -573,6 +599,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found unexpected value type under key '\.comment\.snippets\.0\.files\.2' \(should be a string or an object with keys 'all' and\/or 'any'\)/); }); @@ -594,6 +621,7 @@ describe('validateCommentConfig', () => { }, }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input)).toThrow(/found duplicate snippet id 'foo'/); }); @@ -617,6 +645,7 @@ describe('validateCommentConfig', () => { const variableTemplates = { number: 5, id: 5 }; + // @ts-expect-error: Intentional type violation for testing expect(() => config.validateCommentConfig(input, variableTemplates)).toThrow(/found duplicate snippet id 'snippet-5'/); }); }); diff --git a/test/run.test.js b/test/run.test.ts similarity index 76% rename from test/run.test.js rename to test/run.test.ts index 6d71407..405ed55 100644 --- a/test/run.test.js +++ b/test/run.test.ts @@ -1,9 +1,9 @@ -const core = require('@actions/core'); -const github = require('@actions/github'); -const localGithub = require('../lib/github'); -const comment = require('../lib/comment'); +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import * as localGithub from '../lib/github'; +import * as comment from '../lib/comment'; -const { run } = require('../lib/run'); +import { run } from '../lib/run'; const fakePRNumber = 432; @@ -16,8 +16,12 @@ jest.mock('@actions/github', () => ({ jest.mock('../lib/github'); -describe('run', () => { - test('fully-mocked recreating a comment happy path', async () => { +describe('run', (): void => { + beforeEach(() => { + github.context.payload = { pull_request: { number: fakePRNumber } }; // Ensure the mock context is correct + }); + + test('fully-mocked recreating a comment happy path', async (): Promise => { const fakeToken = 'github-token-123456'; const fakeConfigPath = 'foo/config-file.yml'; const fakeConfig = 'comment:\n' @@ -40,7 +44,7 @@ describe('run', () => { + ' files:\n' + ' - static/**.css\n'; - core.getInput.mockImplementation((argument) => { + (core.getInput as jest.Mock).mockImplementation((argument: string): string | null => { if (argument === 'github-token') { return fakeToken; } @@ -74,14 +78,14 @@ describe('run', () => { + 'Bye!\n\n'}${ comment.commentMetadata(['snippet1', 'snippet3'])}`; - localGithub.getChangedFiles.mockResolvedValue(['static/foo.html', 'README.md', 'static/foo.css']); - localGithub.getFileContent.mockResolvedValue(fakeConfig); - localGithub.getComments.mockResolvedValue(existingPRComments); + (localGithub.getChangedFiles as jest.Mock).mockResolvedValue(['static/foo.html', 'README.md', 'static/foo.css']); + (localGithub.getFileContent as jest.Mock).mockResolvedValue(fakeConfig); + (localGithub.getComments as jest.Mock).mockResolvedValue(existingPRComments); await run(); expect(github.getOctokit).toHaveBeenCalledTimes(1); - expect(github.getOctokit.mock.calls[0][0]).toEqual(fakeToken); + expect((github.getOctokit as jest.Mock).mock.calls[0][0]).toEqual(fakeToken); expect(localGithub.getChangedFiles).toHaveBeenCalledTimes(1); expect(localGithub.getChangedFiles).toHaveBeenCalledWith({ name: 'fake-client' }, fakePRNumber); diff --git a/test/snippets.test.js b/test/snippets.test.ts similarity index 85% rename from test/snippets.test.js rename to test/snippets.test.ts index 511154b..c454c76 100644 --- a/test/snippets.test.js +++ b/test/snippets.test.ts @@ -1,4 +1,5 @@ -const snippets = require('../lib/snippets'); +import * as snippets from '../lib/snippets'; +import {CommentConfig} from "../lib/config"; jest.mock('@actions/core'); @@ -6,14 +7,14 @@ describe('getMatchingSnippetIds', () => { test('no matches', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['foo/**/*.txt']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['foo/bar/baz.html', 'foo/bar.html', 'notfoo/foo/banana.txt']; - const expectedResult = []; + const expectedResult: unknown = []; const actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -22,12 +23,12 @@ describe('getMatchingSnippetIds', () => { test('a match, one pattern', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['foo/**/*.txt']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['foo/bar/baz.txt', 'foo/bar.html', 'notfoo/foo/banana.txt']; const expectedResult = ['snippet1']; @@ -38,12 +39,12 @@ describe('getMatchingSnippetIds', () => { test('negated pattern - at least one changed file must not match', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['!**/*.txt']], ]), ]], - ]); + ]) as CommentConfig; let actualResult = snippets.getMatchingSnippetIds(['foo/bar.txt', 'foo/bar/baz.txt'], config); expect(actualResult).toEqual([]); @@ -55,14 +56,14 @@ describe('getMatchingSnippetIds', () => { test('single * does not match nested dirs', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['*.txt']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['foo/bar.txt', 'foo/bar/baz.txt']; - const expectedResult = []; + const expectedResult: unknown = []; const actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -71,14 +72,14 @@ describe('getMatchingSnippetIds', () => { test('double ** matches files in root dir', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['**/*.txt']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['bar.txt']; - const expectedResult = ['snippet1']; + const expectedResult: unknown = ['snippet1']; const actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -87,14 +88,14 @@ describe('getMatchingSnippetIds', () => { test('* does not match hidden files by default, unless an option is passed', () => { let config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['*.txt']], ]), ]], - ]); + ]) as CommentConfig; let changedFiles = ['.hidden.txt']; - let expectedResult = []; + let expectedResult: unknown = []; let actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -111,14 +112,14 @@ describe('getMatchingSnippetIds', () => { test('a match, many patterns', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['README.md', 'foo/*.html', 'foo/**/*.txt']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['foo/bar/baz.txt', 'foo/bar.html', 'notfoo/foo/banana.txt']; - const expectedResult = ['snippet1']; + const expectedResult: unknown = ['snippet1']; const actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -127,22 +128,22 @@ describe('getMatchingSnippetIds', () => { test('a match, many patterns, many snippets', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['README.md']], ]), - new Map([ + new Map([ ['id', 'snippet2'], ['files', ['**/*.html', '**/*.css']], ]), - new Map([ + new Map([ ['id', 'snippet3'], ['files', ['foo/**/*']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['bar/1.txt', 'bar/2.txt', 'bar/index.html']; - const expectedResult = ['snippet2']; + const expectedResult: unknown = ['snippet2']; const actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -151,22 +152,22 @@ describe('getMatchingSnippetIds', () => { test('a match, many patterns, many snippets - the match matches multiple patterns', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['README.md']], ]), - new Map([ + new Map([ ['id', 'snippet2'], ['files', ['**/*.html', '**/*.css']], ]), - new Map([ + new Map([ ['id', 'snippet3'], ['files', ['foo/**/*']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['foo/index.html']; - const expectedResult = ['snippet2', 'snippet3']; + const expectedResult: unknown = ['snippet2', 'snippet3']; const actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -175,22 +176,22 @@ describe('getMatchingSnippetIds', () => { test('many matches, many patterns, many snippets', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['README.md']], ]), - new Map([ + new Map([ ['id', 'snippet2'], ['files', ['**/*.html', '**/*.css']], ]), - new Map([ + new Map([ ['id', 'snippet3'], ['files', ['foo/**/*']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['README.md', 'bar/1.txt', 'bar/2.txt', 'foo/HELLO.md']; - const expectedResult = ['snippet1', 'snippet3']; + const expectedResult: unknown = ['snippet1', 'snippet3']; const actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -199,22 +200,22 @@ describe('getMatchingSnippetIds', () => { test('snippets are always returned in the same order as they are in the config', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', ['README.md']], ]), - new Map([ + new Map([ ['id', 'snippet2'], ['files', ['**/*.md']], ]), - new Map([ + new Map([ ['id', 'snippet3'], ['files', ['foo/**/*']], ]), ]], - ]); + ]) as CommentConfig; const changedFiles = ['foo/HELLO.txt', 'README.md']; - const expectedResult = ['snippet1', 'snippet2', 'snippet3']; + const expectedResult: unknown = ['snippet1', 'snippet2', 'snippet3']; const actualResult = snippets.getMatchingSnippetIds(changedFiles, config); expect(actualResult).toEqual(expectedResult); @@ -223,12 +224,12 @@ describe('getMatchingSnippetIds', () => { test('patterns using the "all" option - ALL changed files must match ALL of the patterns', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', [{ all: ['**/*.html', 'static/*'] }]], ]), ]], - ]); + ]) as CommentConfig; expect(snippets.getMatchingSnippetIds(['static/index.html'], config)).toEqual(['snippet1']); expect(snippets.getMatchingSnippetIds(['static/about.html'], config)).toEqual(['snippet1']); @@ -240,12 +241,12 @@ describe('getMatchingSnippetIds', () => { test('negated pattern using the "all" option - NONE changed files can match', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', [{ all: ['!**/*.css'] }]], ]), ]], - ]); + ]) as CommentConfig; expect(snippets.getMatchingSnippetIds(['static/index.html'], config)).toEqual(['snippet1']); expect(snippets.getMatchingSnippetIds(['static/about.html'], config)).toEqual(['snippet1']); @@ -257,12 +258,12 @@ describe('getMatchingSnippetIds', () => { test('patterns using the "any" option - ONE of the changed files must match ALL of the patterns', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', [{ any: ['**/*.html', 'static/*'] }]], ]), ]], - ]); + ]) as CommentConfig; expect(snippets.getMatchingSnippetIds(['static/index.html'], config)).toEqual(['snippet1']); expect(snippets.getMatchingSnippetIds(['static/index.css'], config)).toEqual([]); @@ -274,7 +275,7 @@ describe('getMatchingSnippetIds', () => { test('patterns using both the "all" and "any" option', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', [ { @@ -284,7 +285,7 @@ describe('getMatchingSnippetIds', () => { ]], ]), ]], - ]); + ]) as CommentConfig; expect(snippets.getMatchingSnippetIds(['static/foo/index.html'], config)).toEqual(['snippet1']); expect(snippets.getMatchingSnippetIds(['static/foo/bar/index.html'], config)).toEqual(['snippet1']); @@ -296,7 +297,7 @@ describe('getMatchingSnippetIds', () => { test('patterns using both the "all" and "any" option or a simple matcher', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', [ 'README.md', @@ -307,7 +308,7 @@ describe('getMatchingSnippetIds', () => { ]], ]), ]], - ]); + ]) as CommentConfig; expect(snippets.getMatchingSnippetIds(['static/foo/index.html'], config)).toEqual(['snippet1']); expect(snippets.getMatchingSnippetIds(['static/foo/bar/index.html'], config)).toEqual(['snippet1']); @@ -325,7 +326,7 @@ describe('getMatchingSnippetIds', () => { test('patterns using the "all" and "any" option can be modified with globOptions', () => { const config = new Map([ ['snippets', [ - new Map([ + new Map([ ['id', 'snippet1'], ['files', [ { @@ -336,7 +337,7 @@ describe('getMatchingSnippetIds', () => { ]), ]], ['globOptions', { nocase: true }], - ]); + ]) as CommentConfig; expect(snippets.getMatchingSnippetIds(['static/foo/bar/index.html'], config)).toEqual(['snippet1']); expect(snippets.getMatchingSnippetIds(['static/FOO/bar/index.html'], config)).toEqual(['snippet1']); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e20f466 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "node16", + "moduleResolution": "node16", + "target": "es2015", + "sourceMap": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "noImplicitThis": true, + "alwaysStrict": true + }, + "include": ["**/*.ts"], + "exclude": [ + "node_modules" + ], +}