diff --git a/bin-src/init.ts b/bin-src/init.ts index 5668b6f61..6076dcce6 100644 --- a/bin-src/init.ts +++ b/bin-src/init.ts @@ -104,6 +104,11 @@ const intializeChromatic = async ({ } }; +/** + * The main entrypoint for `chromatic init`. + * + * @param argv A list of arguments passed. + */ export async function main(argv: string[]) { const { flags } = meow( ` diff --git a/bin-src/main.ts b/bin-src/main.ts index 0e800d39a..67042282b 100755 --- a/bin-src/main.ts +++ b/bin-src/main.ts @@ -1,5 +1,10 @@ import { run } from '../node-src'; +/** + * The main entrypoint for the CLI. + * + * @param argv A list of arguments passed. + */ export async function main(argv: string[]) { const { code } = await run({ argv }); diff --git a/bin-src/trace.ts b/bin-src/trace.ts index f5ed2420c..c6c314256 100644 --- a/bin-src/trace.ts +++ b/bin-src/trace.ts @@ -32,6 +32,11 @@ import { Context } from '../node-src/types'; const { STORYBOOK_BASE_DIR, STORYBOOK_CONFIG_DIR, WEBPACK_STATS_FILE } = process.env; +/** + * The main entrypoint for `chromatic trace`. + * + * @param argv A list of arguments passed. + */ export async function main(argv: string[]) { const { flags, input } = meow( ` diff --git a/bin-src/trim-stats-file.ts b/bin-src/trim-stats-file.ts index 523543cf6..1ca30c4d3 100644 --- a/bin-src/trim-stats-file.ts +++ b/bin-src/trim-stats-file.ts @@ -15,10 +15,14 @@ const isUserCode = ({ name, moduleName = name }: { name?: string; moduleName?: s * `.trimmed.json` as file extension. * * Usage examples: - * yarn chromatic trim-stats-file - * yarn chromatic trim-stats-file ./path/to/preview-stats.json + * yarn chromatic trim-stats-file + * yarn chromatic trim-stats-file ./path/to/preview-stats.json + * + * @param argv A list of arguments passed. + * @param argv."0" The stats file location passed in as a positional argument. + * + * @returns The file path to the trimmed stats file. */ - export async function main([statsFile = './storybook-static/preview-stats.json']) { try { const stats = await readStatsFile(statsFile); diff --git a/eslint.config.mjs b/eslint.config.mjs index e9164b449..deac13e00 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,6 @@ import eslint from '@eslint/js'; import comments from '@eslint-community/eslint-plugin-eslint-comments/configs'; -// import jsdoc from 'eslint-plugin-jsdoc'; +import jsdoc from 'eslint-plugin-jsdoc'; import noSecrets from 'eslint-plugin-no-secrets'; import prettier from 'eslint-plugin-prettier'; import security from 'eslint-plugin-security'; @@ -186,31 +186,31 @@ export default [ }, }, // lint jsdoc - // { - // files: ['**/*.js', '**/*.ts'], - // plugins: { - // jsdoc, - // }, - // rules: { - // ...jsdoc.configs['flat/recommended-typescript'].rules, - // 'jsdoc/require-jsdoc': [ - // 'error', - // { - // publicOnly: true, - // require: { ClassDeclaration: true, FunctionDeclaration: true }, - // enableFixer: false, - // }, - // ], - // 'jsdoc/require-returns': ['warn', { enableFixer: true }], - // 'jsdoc/sort-tags': [ - // 'error', - // { - // tagSequence: [{ tags: ['param'] }, { tags: ['returns'] }], - // }, - // ], - // 'jsdoc/tag-lines': ['error', 'any', { startLines: 1 }], - // }, - // }, + { + files: ['**/*.js', '**/*.ts'], + plugins: { + jsdoc, + }, + rules: { + ...jsdoc.configs['flat/recommended-typescript'].rules, + 'jsdoc/require-jsdoc': [ + 'error', + { + publicOnly: true, + require: { ClassDeclaration: true, FunctionDeclaration: true }, + enableFixer: false, + }, + ], + 'jsdoc/require-returns': ['warn', { enableFixer: true }], + 'jsdoc/sort-tags': [ + 'error', + { + tagSequence: [{ tags: ['param'] }, { tags: ['returns'] }], + }, + ], + 'jsdoc/tag-lines': ['error', 'any', { startLines: 1 }], + }, + }, // sort your imports { plugins: { diff --git a/node-src/git/findAncestorBuildWithCommit.ts b/node-src/git/findAncestorBuildWithCommit.ts index f92687bbf..b186d3e02 100644 --- a/node-src/git/findAncestorBuildWithCommit.ts +++ b/node-src/git/findAncestorBuildWithCommit.ts @@ -43,14 +43,15 @@ export interface AncestorBuildsQueryResult { * * The purpose here is to allow us to substitute a build with a known clean commit for TurboSnap. * - * @param {Context} context - * @param {int} number The build number to start searching from - * @param {object} options Page size and limit options - * @param {int} options.page How many builds to fetch each time - * @param {int} options.steps How far back to look + * @param ctx The context set when executing the CLI. + * @param ctx.client The GraphQL client within the context. + * @param buildNumber The build number to start searching from + * @param options Page size and limit options + * @param options.page How many builds to fetch each time + * @param options.limit How many builds to gather per query. * - * @returns {Build | void} A build to be substituted - * */ + * @returns A build to be substituted + */ export async function findAncestorBuildWithCommit( { client }: Pick, buildNumber: number, diff --git a/node-src/git/generateGitRepository.ts b/node-src/git/generateGitRepository.ts index aaafeaf58..63681468e 100644 --- a/node-src/git/generateGitRepository.ts +++ b/node-src/git/generateGitRepository.ts @@ -44,15 +44,22 @@ async function generateCommit( return { hash, committedAt: Number.parseInt(committedAt, 10) }; } -// Take a repository description in the following format: -// [[name, parentNames]], where: -// - name is a string -// - parentNames can be false (no parent), a single string or array of strings -// -// This function will take such a description and create a git repository with commits -// following the structure above. Note commit times are assumed to be increasing down the list. -// -// Returns a map: name => commitHash +/** + * Take a repository description in the following format: + * [[name, parentNames]], where: + * - name is a string + * - parentNames can be false (no parent), a single string or array of strings + * + * This function will take such a description and create a git repository with commits + * following the structure above. Note commit times are assumed to be increasing down the list. + * + * Returns a map: name => commitHash + * + * @param runGit A function for running Git commands. + * @param description A description of the Git history to use. + * + * @returns The commit map of the generated repository. + */ export default async function generateGitRepository(runGit, description) { await runGit(`git init`); await runGit(`git config user.email test@test.com`); diff --git a/node-src/git/getBaselineBuilds.ts b/node-src/git/getBaselineBuilds.ts index 7d5d18995..489a2a4c3 100644 --- a/node-src/git/getBaselineBuilds.ts +++ b/node-src/git/getBaselineBuilds.ts @@ -38,6 +38,16 @@ interface BaselineCommitsQueryResult { }; } +/** + * Get a list of baseline builds from the Index service + * + * @param ctx The context set when executing the CLI. + * @param options Options to pass to the Index query. + * @param options.branch The branch name. + * @param options.parentCommits A list of parent commit hashes. + * + * @returns A list of baseline builds, if available. + */ export async function getBaselineBuilds( ctx: Pick, { branch, parentCommits }: { branch: string; parentCommits: string[] } diff --git a/node-src/git/getBranchFromMergeQueuePullRequestNumber.ts b/node-src/git/getBranchFromMergeQueuePullRequestNumber.ts index babe247c8..bfd40b5d3 100644 --- a/node-src/git/getBranchFromMergeQueuePullRequestNumber.ts +++ b/node-src/git/getBranchFromMergeQueuePullRequestNumber.ts @@ -19,6 +19,15 @@ interface MergeQueueOriginalBranchQueryResult { }; } +/** + * Get branch name from a pull request number via the Index service. + * + * @param ctx The context set when executing the CLI. + * @param options Options to pass to the Index query. + * @param options.number The pull request number. + * + * @returns The branch name, if available. + */ export async function getBranchFromMergeQueuePullRequestNumber( ctx: Pick, { number }: { number: number } diff --git a/node-src/git/getChangedFilesWithReplacement.ts b/node-src/git/getChangedFilesWithReplacement.ts index bae2d3fd4..34a486a9b 100644 --- a/node-src/git/getChangedFilesWithReplacement.ts +++ b/node-src/git/getChangedFilesWithReplacement.ts @@ -17,9 +17,14 @@ interface BuildWithCommitInfo { * If the historical build's commit doesn't exist (for instance if it has been rebased and force- * pushed away), find the nearest ancestor build that *does* have a valid commit, and return * the differences, along with the two builds (for tracking purposes). + * + * @param ctx The context set when executing the CLI. + * @param build The build details for gathering changed files. + * + * @returns A list of changed files for the build, adding a replacement build if necessary. */ export async function getChangedFilesWithReplacement( - context: Context, + ctx: Context, build: BuildWithCommitInfo ): Promise<{ changedFiles: string[]; replacementBuild?: BuildWithCommitInfo }> { try { @@ -30,21 +35,21 @@ export async function getChangedFilesWithReplacement( const changedFiles = await getChangedFiles(build.commit); return { changedFiles }; } catch (err) { - context.log.debug( + ctx.log.debug( `Got error fetching commit for #${build.number}(${build.commit}): ${err.message}` ); if (/(bad object|uncommitted changes)/.test(err.message)) { - const replacementBuild = await findAncestorBuildWithCommit(context, build.number); + const replacementBuild = await findAncestorBuildWithCommit(ctx, build.number); if (replacementBuild) { - context.log.debug( + ctx.log.debug( `Found replacement build for #${build.number}(${build.commit}): #${replacementBuild.number}(${replacementBuild.commit})` ); const changedFiles = await getChangedFiles(replacementBuild.commit); return { changedFiles, replacementBuild }; } - context.log.debug(`Couldn't find replacement for #${build.number}(${build.commit})`); + ctx.log.debug(`Couldn't find replacement for #${build.number}(${build.commit})`); } // If we can't find a replacement or the error doesn't match, just throw diff --git a/node-src/git/getCommitAndBranch.test.ts b/node-src/git/getCommitAndBranch.test.ts index 67b9ec284..d765840b4 100644 --- a/node-src/git/getCommitAndBranch.test.ts +++ b/node-src/git/getCommitAndBranch.test.ts @@ -1,6 +1,8 @@ import envCi from 'env-ci'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Logger } from '../lib/log'; +import type { Context } from '../types'; import * as mergeQueue from './getBranchFromMergeQueuePullRequestNumber'; import getCommitAndBranch from './getCommitAndBranch'; import * as git from './git'; @@ -15,7 +17,8 @@ const hasPreviousCommit = vi.mocked(git.hasPreviousCommit); const getBranchFromMergeQueue = vi.mocked(mergeQueue.getBranchFromMergeQueuePullRequestNumber); const mergeQueueBranchMatch = vi.mocked(git.mergeQueueBranchMatch); -const log = { info: vi.fn(), warn: vi.fn(), debug: vi.fn() }; +const log = { info: vi.fn(), warn: vi.fn(), debug: vi.fn() } as unknown as Logger; +const ctx = { log } as unknown as Context; const processEnvironment = process.env; beforeEach(() => { @@ -47,7 +50,7 @@ const commitInfo = { describe('getCommitAndBranch', () => { it('returns commit and branch info', async () => { - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({ branch: 'main', commit: '48e0c83fadbf504c191bc868040b7a969a4f1feb', @@ -68,7 +71,7 @@ describe('getCommitAndBranch', () => { }); getBranch.mockResolvedValue('HEAD'); getCommit.mockImplementation((commit) => Promise.resolve({ commit, ...commitInfo })); - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({ branch: 'ci-branch', commit: 'ci-commit', @@ -84,49 +87,49 @@ describe('getCommitAndBranch', () => { prBranch: 'ci-pr-branch', }); getBranch.mockResolvedValue('HEAD'); - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({ branch: 'ci-pr-branch' }); }); it('removes origin/ prefix in branch name', async () => { getBranch.mockResolvedValue('origin/master'); - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({ branch: 'master' }); }); it('throws when there is only one commit, CI', async () => { envCi.mockReturnValue({ isCi: true }); hasPreviousCommit.mockResolvedValue(false); - await expect(getCommitAndBranch({ log })).rejects.toThrow('Found only one commit'); + await expect(getCommitAndBranch(ctx)).rejects.toThrow('Found only one commit'); }); it('does NOT throw when there is only one commit, non-CI', async () => { envCi.mockReturnValue({ isCi: false }); hasPreviousCommit.mockResolvedValue(false); - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({}); }); describe('with branchName', () => { it('uses provided branchName as branch', async () => { - const info = await getCommitAndBranch({ log }, { branchName: 'foobar' }); + const info = await getCommitAndBranch(ctx, { branchName: 'foobar' }); expect(info).toMatchObject({ branch: 'foobar' }); }); it('does not remove origin/ prefix in branch name', async () => { - const info = await getCommitAndBranch({ log }, { branchName: 'origin/foobar' }); + const info = await getCommitAndBranch(ctx, { branchName: 'origin/foobar' }); expect(info).toMatchObject({ branch: 'origin/foobar' }); }); }); describe('with patchBaseRef', () => { it('uses provided patchBaseRef as branch', async () => { - const info = await getCommitAndBranch({ log }, { patchBaseRef: 'foobar' }); + const info = await getCommitAndBranch(ctx, { patchBaseRef: 'foobar' }); expect(info).toMatchObject({ branch: 'foobar' }); }); it('prefers branchName over patchBaseRef', async () => { - const info = await getCommitAndBranch({ log }, { branchName: 'foo', patchBaseRef: 'bar' }); + const info = await getCommitAndBranch(ctx, { branchName: 'foo', patchBaseRef: 'bar' }); expect(info).toMatchObject({ branch: 'foo' }); }); }); @@ -137,7 +140,7 @@ describe('getCommitAndBranch', () => { process.env.CHROMATIC_BRANCH = 'feature'; process.env.CHROMATIC_SLUG = 'chromaui/chromatic'; getCommit.mockImplementation((commit) => Promise.resolve({ commit, ...commitInfo })); - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({ branch: 'feature', commit: 'f78db92d', @@ -157,7 +160,7 @@ describe('getCommitAndBranch', () => { .mockRejectedValueOnce( new Error('fatal: bad object 48e0c83fadbf504c191bc868040b7a969a4f1feb') ); - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({ branch: 'feature', commit: 'f78db92d' }); expect(log.warn).toHaveBeenCalledWith(expect.stringMatching('Commit f78db92 does not exist')); }); @@ -165,7 +168,7 @@ describe('getCommitAndBranch', () => { it('does not remove origin/ prefix in branch name', async () => { process.env.CHROMATIC_SHA = 'f78db92d'; process.env.CHROMATIC_BRANCH = 'origin/feature'; - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({ branch: 'origin/feature' }); }); }); @@ -177,7 +180,7 @@ describe('getCommitAndBranch', () => { process.env.GITHUB_REPOSITORY = 'chromaui/github'; process.env.GITHUB_SHA = '3276c796'; getCommit.mockResolvedValue({ commit: 'c11da9a9', ...commitInfo }); - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(getCommit).toHaveBeenCalledWith('github'); expect(info).toMatchObject({ branch: 'github', @@ -190,14 +193,10 @@ describe('getCommitAndBranch', () => { it('throws on missing variable', async () => { process.env.GITHUB_EVENT_NAME = 'pull_request'; process.env.GITHUB_HEAD_REF = 'github'; - await expect(getCommitAndBranch({ log })).rejects.toThrow( - 'Missing GitHub environment variable' - ); + await expect(getCommitAndBranch(ctx)).rejects.toThrow('Missing GitHub environment variable'); process.env.GITHUB_HEAD_REF = ''; process.env.GITHUB_SHA = '3276c796'; - await expect(getCommitAndBranch({ log })).rejects.toThrow( - 'Missing GitHub environment variable' - ); + await expect(getCommitAndBranch(ctx)).rejects.toThrow('Missing GitHub environment variable'); }); it('throws on cross-fork PR (where refs are equal)', async () => { @@ -205,7 +204,7 @@ describe('getCommitAndBranch', () => { process.env.GITHUB_BASE_REF = 'github'; process.env.GITHUB_HEAD_REF = 'github'; process.env.GITHUB_SHA = '3276c796'; - await expect(getCommitAndBranch({ log })).rejects.toThrow('Cross-fork PR builds unsupported'); + await expect(getCommitAndBranch(ctx)).rejects.toThrow('Cross-fork PR builds unsupported'); }); }); @@ -216,7 +215,7 @@ describe('getCommitAndBranch', () => { process.env.TRAVIS_PULL_REQUEST_BRANCH = 'travis'; process.env.TRAVIS_PULL_REQUEST_SLUG = 'chromaui/travis'; getCommit.mockImplementation((commit) => Promise.resolve({ commit, ...commitInfo })); - const info = await getCommitAndBranch({ log }); + const info = await getCommitAndBranch(ctx); expect(info).toMatchObject({ branch: 'travis', commit: 'ef765ac7', @@ -228,9 +227,7 @@ describe('getCommitAndBranch', () => { it('throws on missing variable', async () => { process.env.TRAVIS_EVENT_TYPE = 'pull_request'; process.env.TRAVIS_PULL_REQUEST_SHA = 'ef765ac7'; - await expect(getCommitAndBranch({ log })).rejects.toThrow( - 'Missing Travis environment variable' - ); + await expect(getCommitAndBranch(ctx)).rejects.toThrow('Missing Travis environment variable'); }); }); @@ -238,13 +235,10 @@ describe('getCommitAndBranch', () => { it('uses PRs branchName as branch instead of temporary mergeQueue branch', async () => { mergeQueueBranchMatch.mockResolvedValue(4); getBranchFromMergeQueue.mockResolvedValue('branch-before-merge-queue'); - const info = await getCommitAndBranch( - { log }, - { - branchName: - 'this-is-merge-queue-branch-format/main/pr-4-48e0c83fadbf504c191bc868040b7a969a4f1feb', - } - ); + const info = await getCommitAndBranch(ctx, { + branchName: + 'this-is-merge-queue-branch-format/main/pr-4-48e0c83fadbf504c191bc868040b7a969a4f1feb', + }); expect(info).toMatchObject({ branch: 'branch-before-merge-queue' }); }); }); diff --git a/node-src/git/getCommitAndBranch.ts b/node-src/git/getCommitAndBranch.ts index edb273b55..ea40337b2 100644 --- a/node-src/git/getCommitAndBranch.ts +++ b/node-src/git/getCommitAndBranch.ts @@ -1,5 +1,6 @@ import envCi from 'env-ci'; +import { Context } from '../types'; import forksUnsupported from '../ui/messages/errors/forksUnsupported'; import gitOneCommit from '../ui/messages/errors/gitOneCommit'; import missingGitHubInfo from '../ui/messages/errors/missingGitHubInfo'; @@ -21,10 +22,21 @@ interface CommitInfo { mergeCommit?: string; } +/** + * Gather commit and branch information from Git and the local environment. + * + * @param ctx The context set when executing the CLI. + * @param options Some extra details about the repository. + * @param options.branchName The name of the branch. + * @param options.patchBaseRef The reference to the base side of a patch. + * @param options.ci If we're running in a CI pipeline or not. + * + * @returns Commit and branch information. + */ // TODO: refactor this function // eslint-disable-next-line complexity, max-statements export default async function getCommitAndBranch( - ctx, + ctx: Context, { branchName, patchBaseRef, diff --git a/node-src/git/getParentCommits.ts b/node-src/git/getParentCommits.ts index 0d10520ec..0958b428b 100644 --- a/node-src/git/getParentCommits.ts +++ b/node-src/git/getParentCommits.ts @@ -198,12 +198,20 @@ async function maximallyDescendentCommits({ log }: Pick, commits return maxCommits; } +/** + * Gather the parent commits from Git. + * + * @param ctx The context set when executing the CLI. + * @param options Additional options for changing function flow. + * @param options.ignoreLastBuildOnBranch Ignore the last Chromatic build associated with this + * branch. + * + * @returns A list of parent commits associated with this branch. + */ // TODO: refactor this function -// eslint-disable-next-line complexity -export async function getParentCommits( - { options, client, git, log }: Context, - { ignoreLastBuildOnBranch = false } = {} -) { +// eslint-disable-next-line complexity, max-statements +export async function getParentCommits(ctx: Context, { ignoreLastBuildOnBranch = false } = {}) { + const { options, client, git, log } = ctx; const { branch, committedAt } = git; // Include the latest build from this branch as an ancestor of the current build diff --git a/node-src/git/git.ts b/node-src/git/git.ts index 43728381f..ea60275b0 100644 --- a/node-src/git/git.ts +++ b/node-src/git/git.ts @@ -11,6 +11,13 @@ import gitNotInstalled from '../ui/messages/errors/gitNotInstalled'; const newline = /\r\n|\r|\n/; // Git may return \n even on Windows, so we can't use EOL export const NULL_BYTE = '\0'; // Separator used when running `git ls-files` with `-z` +/** + * Execute a Git command in the local terminal. + * + * @param command The command to execute. + * + * @returns The result of the command from the terminal. + */ export async function execGitCommand(command: string) { try { const { all } = await execaCommand(command, { @@ -39,18 +46,32 @@ export async function execGitCommand(command: string) { } } +/** + * Get the version of Git from the host. + * + * @returns The Git version. + */ export async function getVersion() { const result = await execGitCommand(`git --version`); return result.replace('git version ', ''); } +/** + * Get the user's email from Git. + * + * @returns The user's email. + */ export async function getUserEmail() { return execGitCommand(`git config user.email`); } -// The slug consists of the last two parts of the URL, at least for GitHub, GitLab and Bitbucket, -// and is typically followed by `.git`. The regex matches the last two parts between slashes, and -// ignores the `.git` suffix if it exists, so it matches something like `ownername/reponame`. +/** + * The slug consists of the last two parts of the URL, at least for GitHub, GitLab and Bitbucket, + * and is typically followed by `.git`. The regex matches the last two parts between slashes, and + * ignores the `.git` suffix if it exists, so it matches something like `ownername/reponame`. + * + * @returns The slug of the remote URL. + */ export async function getSlug() { const result = await execGitCommand(`git config --get remote.origin.url`); const downcasedResult = result.toLowerCase(); @@ -62,7 +83,13 @@ export async function getSlug() { // remote and the branch matches with origin/REF, but for now we are naive about // adhoc builds. -// We could cache this, but it's probably pretty quick +/** + * Get commit details from Git. + * + * @param revision The argument to `git log` (usually a commit SHA). + * + * @returns Commit details from Git. + */ export async function getCommit(revision = '') { const result = await execGitCommand( // Technically this yields the author info, not committer info @@ -78,6 +105,11 @@ export async function getCommit(revision = '') { return { commit, committedAt, committerEmail, committerName }; } +/** + * Get the current branch from Git. + * + * @returns The branch name from Git. + */ export async function getBranch() { try { // Git v2.22 and above @@ -99,9 +131,13 @@ export async function getBranch() { } } -// Retrieve the hash of all uncommitted files, which includes staged, unstaged, and untracked files, -// excluding deleted files (which can't be hashed) and ignored files. There is no one single Git -// command to reliably get this information, so we use a combination of commands grouped together. +/** + * Retrieve the hash of all uncommitted files, which includes staged, unstaged, and untracked files, + * excluding deleted files (which can't be hashed) and ignored files. There is no one single Git + * command to reliably get this information, so we use a combination of commands grouped together. + * + * @returns The uncommited hash, if available. + */ export async function getUncommittedHash() { const listStagedFiles = 'git diff --name-only --diff-filter=d --cached'; const listUnstagedFiles = 'git diff --name-only --diff-filter=d'; @@ -121,6 +157,11 @@ export async function getUncommittedHash() { return uncommittedHash === noChangesHash ? '' : uncommittedHash; } +/** + * Determine if the current commit has at least one parent commit. + * + * @returns True if the current commit has at least one parent. + */ export async function hasPreviousCommit() { const result = await execGitCommand(`git --no-pager log -n 1 --skip=1 --format="%H"`); @@ -129,7 +170,13 @@ export async function hasPreviousCommit() { return result.split('\n').some((line: string) => allhex.test(line)); } -// Check if a commit exists in the repository +/** + * Check if a commit exists in the repository + * + * @param commit The commit to check. + * + * @returns True if the commit exists. + */ export async function commitExists(commit: string) { try { await execGitCommand(`git cat-file -e "${commit}^{commit}"`); @@ -139,6 +186,14 @@ export async function commitExists(commit: string) { } } +/** + * Get the changed files of a single commit or between two. + * + * @param baseCommit The base commit to check. + * @param headCommit The head commit to check. + * + * @returns The list of changed files of a single commit or between two commits. + */ export async function getChangedFiles(baseCommit: string, headCommit = '') { // Note that an empty headCommit will include uncommitted (staged or unstaged) changes. const files = await execGitCommand(`git --no-pager diff --name-only ${baseCommit} ${headCommit}`); @@ -148,8 +203,14 @@ export async function getChangedFiles(baseCommit: string, headCommit = '') { /** * Returns a boolean indicating whether the workspace is up-to-date (neither ahead nor behind) with * the remote. Returns true on error, assuming the workspace is up-to-date. + * + * @param ctx The context set when executing the CLI. + * + * @returns True if the workspace is up-to-date. */ -export async function isUpToDate({ log }: Pick) { +export async function isUpToDate(ctx: Pick) { + const { log } = ctx; + try { await execGitCommand(`git remote update`); } catch (err) { @@ -180,6 +241,8 @@ export async function isUpToDate({ log }: Pick) { /** * Returns a boolean indicating whether the workspace is clean (no changes, no untracked files). + * + * @returns True if the workspace has no changes. */ export async function isClean() { const status = await execGitCommand('git status --porcelain'); @@ -189,6 +252,8 @@ export async function isClean() { /** * Returns the "Your branch is behind by n commits (pull to update)" part of the git status message, * omitting any of the other stuff that may be in there. Note we expect the workspace to be clean. + * + * @returns A message indicating how far behind the branch is from the remote. */ export async function getUpdateMessage() { const status = await execGitCommand('git status'); @@ -227,8 +292,10 @@ export async function getUpdateMessage() { * one on the base branch, but if that fails we just pick the first one and hope it works out. * Luckily this is an uncommon scenario. * - * @param {string} headRef Name of the head branch - * @param {string} baseRef Name of the base branch + * @param headReference Name of the head branch + * @param baseReference Name of the base branch + * + * @returns The best common ancestor commit between the two provided. */ export async function findMergeBase(headReference: string, baseReference: string) { const result = await execGitCommand(`git merge-base --all ${headReference} ${baseReference}`); @@ -248,12 +315,29 @@ export async function findMergeBase(headReference: string, baseReference: string return mergeBases[baseReferenceIndex] || mergeBases[0]; } +/** + * + * @param reference The reference to checkout (usually a commit). + * + * @returns The result of the Git checkout call in the terminal. + */ export async function checkout(reference: string) { return execGitCommand(`git checkout ${reference}`); } const fileCache = {}; const limitConcurrency = pLimit(10); + +/** + * Checkout a file at the given reference and write the results to a temporary file. + * + * @param ctx The context set when executing the CLI. + * @param ctx.log The logger found on the context object. + * @param reference The reference (usually a commit or branch) to the file version in Git. + * @param fileName The name of the file to check out. + * + * @returns The temporary file path of the checked out file. + */ export async function checkoutFile( { log }: Pick, reference: string, @@ -273,18 +357,40 @@ export async function checkoutFile( return fileCache[pathspec]; } +/** + * Check out the previous branch in the Git repository. + * + * @returns The result of the `git checkout` command in the terminal. + */ export async function checkoutPrevious() { return execGitCommand(`git checkout -`); } +/** + * Reset any pending changes in the Git repository. + * + * @returns The result of the `git reset` command in the terminal. + */ export async function discardChanges() { return execGitCommand(`git reset --hard`); } +/** + * Gather the root directory of the Git repository. + * + * @returns The root directory of the Git repository. + */ export async function getRepositoryRoot() { return execGitCommand(`git rev-parse --show-toplevel`); } +/** + * Find all files that match the given patterns within the repository. + * + * @param patterns A list of patterns to filter file results. + * + * @returns A list of files matching the pattern. + */ export async function findFilesFromRepositoryRoot(...patterns: string[]) { const repoRoot = await getRepositoryRoot(); @@ -302,7 +408,14 @@ export async function findFilesFromRepositoryRoot(...patterns: string[]) { return files.split(NULL_BYTE).filter(Boolean); } -export async function mergeQueueBranchMatch(branch) { +/** + * Determine if the branch is from a GitHub merge queue. + * + * @param branch The branch name in question. + * + * @returns The pull request number associated for the branch. + */ +export async function mergeQueueBranchMatch(branch: string) { const mergeQueuePattern = new RegExp(/gh-readonly-queue\/.*\/pr-(\d+)-[\da-f]{30}/); const match = branch.match(mergeQueuePattern); diff --git a/node-src/git/mocks/mock-index.ts b/node-src/git/mocks/mock-index.ts index f34b4d799..da3d82eb1 100644 --- a/node-src/git/mocks/mock-index.ts +++ b/node-src/git/mocks/mock-index.ts @@ -74,6 +74,16 @@ const mocks = { }, }; +/** + * Create a sample Index service for running tests. + * + * @param repository Details on the local repository. + * @param repository.commitMap A map of commits in the repository. + * @param buildDescriptions A list of builds for the project. + * @param prDescriptions A list of PRs for the project. + * + * @returns A mock Index service for testing. + */ export default function createMockIndex({ commitMap }, buildDescriptions, prDescriptions = []) { const builds = buildDescriptions.map(([name, branch], index) => { let hash, committedAt; diff --git a/node-src/index.ts b/node-src/index.ts index 09deb7c45..ab445752c 100644 --- a/node-src/index.ts +++ b/node-src/index.ts @@ -42,9 +42,7 @@ import runtimeError from './ui/messages/errors/runtimeError'; import taskError from './ui/messages/errors/taskError'; import intro from './ui/messages/info/intro'; -/** - Make keys of `T` outside of `R` optional. -*/ +// Make keys of `T` outside of `R` optional. type AtLeast = Partial & Pick; interface Output { @@ -84,7 +82,17 @@ export type InitialContext = Omit< const isContext = (ctx: InitialContext): ctx is Context => 'options' in ctx; -// Entry point for the CLI, GitHub Action, and Node API +/** + * Entry point for the CLI, GitHub Action, and Node API + * + * @param args The arguments set by the environment in which the CLI is running (CLI, GitHub Action, + * or Node API) + * @param args.argv The list of arguments passed. + * @param args.flags Any flags that were passed. + * @param args.options Any options that were passed. + * + * @returns An object with details from the result of the new build. + */ // TODO: refactor this function // eslint-disable-next-line complexity export async function run({ @@ -140,7 +148,13 @@ export async function run({ }; } -// Entry point for testing only (typically invoked via `run` above) +/** + * Entry point for testing only (typically invoked via `run` above) + * + * @param ctx The context set when executing the CLI. + * + * @returns A promise that resolves when all steps are completed. + */ export async function runAll(ctx: InitialContext) { ctx.log.info(''); ctx.log.info(intro(ctx)); @@ -282,8 +296,14 @@ export interface GitInfo { repositoryRootDir: string; } -// Although this function may not be used directly in this project, it can be used externally (such -// as https://github.com/chromaui/addon-visual-tests). +/** + * Parse git information from the local repository. + * + * Although this function may not be used directly in this project, it can be used externally (such + * as https://github.com/chromaui/addon-visual-tests). + * + * @returns Any git information we were able to gather. + */ export async function getGitInfo(): Promise { let slug: string; try { diff --git a/node-src/io/GraphQLClient.ts b/node-src/io/GraphQLClient.ts index 11186d3dc..0a2f7e246 100644 --- a/node-src/io/GraphQLClient.ts +++ b/node-src/io/GraphQLClient.ts @@ -14,6 +14,9 @@ export interface GraphQLError { }; } +/** + * Interact with a GraphQL server using fetch and retries. + */ export default class GraphQLClient { endpoint: string; headers: HTTPClientOptions['headers']; diff --git a/node-src/io/HTTPClient.ts b/node-src/io/HTTPClient.ts index 91976a1bd..8593a6803 100644 --- a/node-src/io/HTTPClient.ts +++ b/node-src/io/HTTPClient.ts @@ -7,6 +7,9 @@ import { Context } from '../types'; import getDNSResolveAgent from './getDNSResolveAgent'; import getProxyAgent from './getProxyAgent'; +/** + * A custom HTTP client error. + */ export class HTTPClientError extends Error { response: Response; @@ -36,7 +39,9 @@ export interface HTTPClientFetchOptions { retries?: number; } -// A basic wrapper class for fetch with the ability to retry fetches +/** + * A basic wrapper class for fetch with the ability to retry fetches + */ export default class HTTPClient { env: Context['env']; diff --git a/node-src/io/getDNSResolveAgent.ts b/node-src/io/getDNSResolveAgent.ts index d12c0127c..b99d19390 100644 --- a/node-src/io/getDNSResolveAgent.ts +++ b/node-src/io/getDNSResolveAgent.ts @@ -1,6 +1,9 @@ import dns from 'dns'; import { Agent, AgentOptions } from 'https'; +/** + * A DNS resolver for interacting with a custom DNS server, if provided. + */ export class DNSResolveAgent extends Agent { constructor(options: AgentOptions = {}) { super({ diff --git a/node-src/lib/FileReaderBlob.ts b/node-src/lib/FileReaderBlob.ts index ef4435163..8ca909975 100644 --- a/node-src/lib/FileReaderBlob.ts +++ b/node-src/lib/FileReaderBlob.ts @@ -1,5 +1,8 @@ import { createReadStream, ReadStream } from 'fs'; +/** + * A file reader which offers a callback for tracking progress updates. + */ export class FileReaderBlob { readStream: ReadStream; size: number; diff --git a/node-src/lib/LoggingRenderer.ts b/node-src/lib/LoggingRenderer.ts index 472900c9e..2c302ad7f 100644 --- a/node-src/lib/LoggingRenderer.ts +++ b/node-src/lib/LoggingRenderer.ts @@ -1,5 +1,8 @@ import UpdateRenderer from 'listr-update-renderer'; +/** + * The default Listr renderer to show the TUI. This also updates a log file at the same time. + */ export default class LoggingRenderer { static readonly nonTTY = false; tasks; diff --git a/node-src/lib/NonTTYRenderer.ts b/node-src/lib/NonTTYRenderer.ts index befee183f..c07ce87a0 100644 --- a/node-src/lib/NonTTYRenderer.ts +++ b/node-src/lib/NonTTYRenderer.ts @@ -1,3 +1,6 @@ +/** + * A Listr renderer to handle non-TTY environments. + */ export default class NonTTYRenderer { static readonly nonTTY = true; tasks; diff --git a/node-src/lib/checkForUpdates.ts b/node-src/lib/checkForUpdates.ts index 18c055036..27c9b74d7 100644 --- a/node-src/lib/checkForUpdates.ts +++ b/node-src/lib/checkForUpdates.ts @@ -9,6 +9,13 @@ const rejectIn = (ms: number) => new Promise((_, reject) => setTimeout(reje const withTimeout = (promise: Promise, ms: number): Promise => Promise.race([promise, rejectIn(ms)]); +/** + * Check for a newer version of the CLI. + * + * @param ctx The context set when executing the CLI. + * + * @returns A promise that resolves when we're done checking for new CLI versions. + */ export default async function checkForUpdates(ctx: Context) { if (ctx.options.skipUpdateCheck === true) { ctx.log.info(`Skipping update check`); diff --git a/node-src/lib/checkPackageJson.ts b/node-src/lib/checkPackageJson.ts index e2a95e09d..7fc79d2ef 100644 --- a/node-src/lib/checkPackageJson.ts +++ b/node-src/lib/checkPackageJson.ts @@ -13,12 +13,17 @@ const scriptName = 'chromatic'; const findScript = (scripts: Record) => scripts[scriptName] || Object.values(scripts).find((cmd) => cmd.startsWith(scriptName)); -export default async function checkPackageJson({ - log, - options, - packageJson, - packagePath, -}: Context) { +/** + * Check the local package.json file if it has a script for running Chromatic. + * + * @param ctx The context set when executing the CLI. + * + * @returns A promise that resolves when we're done checking (and maybe adding) the Chromatic script + * to the local package.json + */ +export default async function checkPackageJson(ctx: Context) { + const { log, options, packageJson, packagePath } = ctx; + if (!options.interactive) return; try { diff --git a/node-src/lib/checkStorybookBaseDirectory.ts b/node-src/lib/checkStorybookBaseDirectory.ts index b650c9c87..ac98aa9bf 100644 --- a/node-src/lib/checkStorybookBaseDirectory.ts +++ b/node-src/lib/checkStorybookBaseDirectory.ts @@ -7,6 +7,12 @@ import { Context, Stats } from '../types'; import { invalidStorybookBaseDirectory } from '../ui/messages/errors/invalidStorybookBaseDirectory'; import { exitCodes, setExitCode } from './setExitCode'; +/** + * Ensure the base directory for Storybook is setup correctly before running TurboSnap. + * + * @param ctx The context set when executing the CLI. + * @param stats The stats file information from the project's builder (Webpack, for example). + */ export async function checkStorybookBaseDirectory(ctx: Context, stats: Stats) { const repositoryRoot = await getRepositoryRoot(); diff --git a/node-src/lib/compress.ts b/node-src/lib/compress.ts index fdd34884f..f5f0441b0 100644 --- a/node-src/lib/compress.ts +++ b/node-src/lib/compress.ts @@ -4,6 +4,14 @@ import { file as temporaryFile } from 'tmp-promise'; import { Context, FileDesc } from '../types'; +/** + * Make a zip file with a list of files (usually used to zip build files to upload to Chromatic). + * + * @param ctx The context set when executing the CLI. + * @param files The list of files to add to the zip. + * + * @returns A promise that resolves with details of the created zip. + */ export default async function makeZipFile(ctx: Context, files: FileDesc[]) { const archive = archiver('zip', { zlib: { level: 9 } }); const temporary = await temporaryFile({ postfix: '.zip' }); diff --git a/node-src/lib/e2e.ts b/node-src/lib/e2e.ts index 17b7f207b..7c89bfe19 100644 --- a/node-src/lib/e2e.ts +++ b/node-src/lib/e2e.ts @@ -27,6 +27,14 @@ const parseNexec = ((agent, args) => { return command.replace('{0}', args.map((c) => quote(c)).join(' ')).trim(); }) as Runner; +/** + * + * @param ctx The context set when executing the CLI. + * @param flag The E2E testing tool used for the build. + * @param buildCommandOptions Options to pass to the build command (such as --output-dir). + * + * @returns The command for building the E2E project. + */ export async function getE2EBuildCommand( ctx: Context, flag: 'playwright' | 'cypress', @@ -59,6 +67,13 @@ export async function getE2EBuildCommand( } } +/** + * Determine if the build is an E2E build. + * + * @param options Parsed options when executing the CLI (usually from the context). + * + * @returns true if the build is an E2E build. + */ export function isE2EBuild(options: Options) { return options.playwright || options.cypress; } diff --git a/node-src/lib/emailHash.ts b/node-src/lib/emailHash.ts index ebcca78ac..c91fe7c13 100644 --- a/node-src/lib/emailHash.ts +++ b/node-src/lib/emailHash.ts @@ -1,6 +1,14 @@ import { createHash } from 'crypto'; -// Inspired by https://en.gravatar.com/site/implement/hash +/** + * Create a hash of the provided email address. + * + * Inspired by https://en.gravatar.com/site/implement/hash + * + * @param email The plaintext email address. + * + * @returns A hashed version of the plaintext email address. + */ export function emailHash(email: string) { return createHash('md5').update(email.trim().toLowerCase()).digest('hex'); } diff --git a/node-src/lib/getConfiguration.ts b/node-src/lib/getConfiguration.ts index 73e74cf55..b9846f668 100644 --- a/node-src/lib/getConfiguration.ts +++ b/node-src/lib/getConfiguration.ts @@ -47,6 +47,14 @@ const configurationSchema = z export type Configuration = z.infer; +/** + * Parse configuration details from a local config file (typically chromatic.config.json). + * + * @param configFile The path to a custom config file (outside of the normal chromatic.config.json + * file) + * + * @returns A parsed configration object from the local config file. + */ export async function getConfiguration( configFile?: string ): Promise { diff --git a/node-src/lib/getDependentStoryFiles.ts b/node-src/lib/getDependentStoryFiles.ts index 218ea2ed6..6330ac1a0 100644 --- a/node-src/lib/getDependentStoryFiles.ts +++ b/node-src/lib/getDependentStoryFiles.ts @@ -43,9 +43,15 @@ const getPackageName = (modulePath: string) => { /** * Converts a module path found in the webpack stats to be relative to the (git) root path. Module * paths can be relative (`./module.js`) or absolute (`/path/to/project/module.js`). The webpack - * stats may have been generated in a subdirectory, so we prepend the baseDir if necessary. - * The result is a relative POSIX path compatible with `git diff --name-only`. - * Virtual paths (e.g. Vite) are returned as-is. + * stats may have been generated in a subdirectory, so we prepend the baseDir if necessary. The + * result is a relative POSIX path compatible with `git diff --name-only`. Virtual paths (e.g. Vite) + * are returned as-is. + * + * @param posixPath The POSIX path to the file. + * @param rootPath The project root path. + * @param baseDirectory The base directory to the file. + * + * @returns A normalized path to the file. */ export function normalizePath(posixPath: string, rootPath: string, baseDirectory = '') { if (!posixPath || posixPath.startsWith('/virtual:')) return posixPath; @@ -58,6 +64,15 @@ export function normalizePath(posixPath: string, rootPath: string, baseDirectory * This traverses the webpack module stats to retrieve a set of CSF files that somehow trace back to * the changed git files. The result is a map of Module ID => file path. In the end we'll only send * the Module IDs to Chromatic, the file paths are only for logging purposes. + * + * @param ctx The context set when executing the CLI. + * @param stats The stats file information from the project's builder (Webpack, for example). + * @param statsPath The path to the stats file generated from the project's builder (Webpack, for + * example). + * @param changedFiles A list of changed files. + * @param changedDependencies A list of changed dependencies. + * + * @returns Any story files that are impacted by the list of changed files and dependencies. */ // TODO: refactor this function // eslint-disable-next-line complexity, max-statements diff --git a/node-src/lib/getEnvironment.ts b/node-src/lib/getEnvironment.ts index 6d86dab21..0d2870998 100644 --- a/node-src/lib/getEnvironment.ts +++ b/node-src/lib/getEnvironment.ts @@ -13,7 +13,6 @@ export interface Environment { ENVIRONMENT_WHITELIST: RegExp[]; HTTP_PROXY: string; HTTPS_PROXY: string; - LOGGLY_CUSTOMER_TOKEN: string; STORYBOOK_BUILD_TIMEOUT: number; STORYBOOK_CLI_FLAGS_BY_VERSION: typeof STORYBOOK_CLI_FLAGS_BY_VERSION; STORYBOOK_VERIFY_TIMEOUT: number; @@ -33,7 +32,6 @@ const { CHROMATIC_UPGRADE_TIMEOUT = String(60 * 60 * 1000), HTTP_PROXY = process.env.http_proxy, HTTPS_PROXY = process.env.https_proxy, - LOGGLY_CUSTOMER_TOKEN = 'b5e26204-cdc5-4c78-a9cc-c69eb7fabad3', STORYBOOK_BUILD_TIMEOUT = String(10 * 60 * 1000), STORYBOOK_VERIFY_TIMEOUT = String(3 * 60 * 1000), STORYBOOK_NODE_ENV = 'production', @@ -51,6 +49,11 @@ const CHROMATIC_PROJECT_TOKEN = process.env.CHROMATIC_APP_CODE || // backwards compatibility process.env.CHROMA_APP_CODE; // backwards compatibility +/** + * Parse variables from the process environment. + * + * @returns An object containing parsed environment variables. + */ export default function getEnvironment(): Environment { return { CHROMATIC_DNS_FAILOVER_SERVERS: CHROMATIC_DNS_FAILOVER_SERVERS.split(',') @@ -71,7 +74,6 @@ export default function getEnvironment(): Environment { ENVIRONMENT_WHITELIST, HTTP_PROXY, HTTPS_PROXY, - LOGGLY_CUSTOMER_TOKEN, STORYBOOK_BUILD_TIMEOUT: Number.parseInt(STORYBOOK_BUILD_TIMEOUT, 10), STORYBOOK_CLI_FLAGS_BY_VERSION, STORYBOOK_VERIFY_TIMEOUT: Number.parseInt(STORYBOOK_VERIFY_TIMEOUT, 10), diff --git a/node-src/lib/getOptions.test.ts b/node-src/lib/getOptions.test.ts index 96a3cf888..1421544ea 100644 --- a/node-src/lib/getOptions.test.ts +++ b/node-src/lib/getOptions.test.ts @@ -4,7 +4,6 @@ import { describe, expect, it, vi } from 'vitest'; import { Context } from '../types'; import getEnvironment from './getEnvironment'; import getOptions from './getOptions'; -import getStorybookConfiguration from './getStorybookConfiguration'; import parseArguments from './parseArguments'; import TestLogger from './testLogger'; @@ -211,31 +210,3 @@ describe('getOptions', () => { }); }); }); - -describe('getStorybookConfiguration', () => { - it('handles short names', async () => { - const port = getStorybookConfiguration('start-storybook -p 9001', '-p', '--port'); - expect(port).toBe('9001'); - }); - it('handles long names', async () => { - const port = getStorybookConfiguration('start-storybook --port 9001', '-p', '--port'); - expect(port).toBe('9001'); - }); - it('handles equals', async () => { - const port = getStorybookConfiguration('start-storybook --port=9001', '-p', '--port'); - expect(port).toBe('9001'); - }); - it('handles double space', async () => { - const port = getStorybookConfiguration('start-storybook --port 9001', '-p', '--port'); - expect(port).toBe('9001'); - }); - - it('handles complex scripts', async () => { - const port = getStorybookConfiguration( - "node verify-node-version.js && concurrently --raw --kill-others 'yarn relay --watch' 'start-storybook -s ./public -p 9001'", - '-p', - '--port' - ); - expect(port).toBe('9001'); - }); -}); diff --git a/node-src/lib/getOptions.ts b/node-src/lib/getOptions.ts index b485aacb0..104f28df1 100644 --- a/node-src/lib/getOptions.ts +++ b/node-src/lib/getOptions.ts @@ -32,18 +32,18 @@ const undefinedIfEmpty = (array: T[]) => { const stripUndefined = (object: Partial): Partial => Object.fromEntries(Object.entries(object).filter(([_, v]) => v !== undefined)); +/** + * Parse options set when executing the CLI. + * + * @param ctx The context set when executing the CLI. + * + * @returns An object containing parsed options + */ // TODO: refactor this function // eslint-disable-next-line complexity, max-statements -export default function getOptions({ - argv, - env, - flags, - extraOptions, - configuration, - log, - packageJson, - packagePath, -}: InitialContext): Options { +export default function getOptions(ctx: InitialContext): Options { + const { argv, env, flags, extraOptions, configuration, log, packageJson, packagePath } = ctx; + const defaultOptions = { projectToken: env.CHROMATIC_PROJECT_TOKEN, fromCI: !!process.env.CI, diff --git a/node-src/lib/getStorybookConfiguration.test.ts b/node-src/lib/getStorybookConfiguration.test.ts deleted file mode 100644 index 843f79f3e..000000000 --- a/node-src/lib/getStorybookConfiguration.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import getStorybookConfiguration from './getStorybookConfiguration'; - -describe('getStorybookConfiguration', () => { - it('handles short names', () => { - const port = getStorybookConfiguration('start-storybook -p 9001', '-p', '--port'); - expect(port).toBe('9001'); - }); - it('handles long names', () => { - const port = getStorybookConfiguration('start-storybook --port 9001', '-p', '--port'); - expect(port).toBe('9001'); - }); - it('handles equals', () => { - const port = getStorybookConfiguration('start-storybook --port=9001', '-p', '--port'); - expect(port).toBe('9001'); - }); - it('handles double space', () => { - const port = getStorybookConfiguration('start-storybook --port 9001', '-p', '--port'); - expect(port).toBe('9001'); - }); - - it('handles complex scripts', () => { - const port = getStorybookConfiguration( - "node verify-node-version.js && concurrently --raw --kill-others 'yarn relay --watch' 'start-storybook -s ./public -p 9001'", - '-p', - '--port' - ); - expect(port).toBe('9001'); - }); -}); diff --git a/node-src/lib/getStorybookConfiguration.ts b/node-src/lib/getStorybookConfiguration.ts deleted file mode 100644 index d101afd29..000000000 --- a/node-src/lib/getStorybookConfiguration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* This is not exactly clever but it works most of the time - * we receive the full text of the npm script, and we look if we can find the cli flag - */ -export default function getStorybookConfiguration( - storybookScript: string, - shortName: string, - longName?: string -) { - if (!storybookScript) return; - const parts = storybookScript.split(/[\s"'=]+/); - let index = parts.indexOf(longName); - if (index === -1) { - index = parts.indexOf(shortName); - } - if (index === -1) { - return; - } - return parts[index + 1]; -} diff --git a/node-src/lib/getStorybookInfo.ts b/node-src/lib/getStorybookInfo.ts index 81fdcc89e..2334d76eb 100644 --- a/node-src/lib/getStorybookInfo.ts +++ b/node-src/lib/getStorybookInfo.ts @@ -5,6 +5,14 @@ import { Context } from '../types'; import { getStorybookMetadataFromProjectJson } from './getPrebuiltStorybookMetadata'; import { getStorybookMetadata } from './getStorybookMetadata'; +/** + * Get Storybook information from the user's local project. + * + * @param ctx The context set when executing the CLI. + * + * @returns Any Storybook information we can find from the user's local project (which may be + * nothing). + */ export default async function getStorybookInfo( ctx: Context ): Promise> { diff --git a/node-src/lib/localBuildsSpecifier.ts b/node-src/lib/localBuildsSpecifier.ts index 5d657b5f1..f56bc6494 100644 --- a/node-src/lib/localBuildsSpecifier.ts +++ b/node-src/lib/localBuildsSpecifier.ts @@ -1,6 +1,13 @@ import { Context } from '../types'; import { emailHash } from './emailHash'; +/** + * Include local build information when querying Chromatic for build information. + * + * @param ctx The context set when executing the CLI. + * + * @returns Local build information used in GraphQL queries for build information. + */ export function localBuildsSpecifier(ctx: Pick) { if (ctx.options.isLocalBuild) return { localBuildEmailHash: emailHash(ctx.git.gitUserEmail) }; diff --git a/node-src/lib/parseArguments.ts b/node-src/lib/parseArguments.ts index 44657b76f..4995abcd2 100644 --- a/node-src/lib/parseArguments.ts +++ b/node-src/lib/parseArguments.ts @@ -3,6 +3,13 @@ import meow from 'meow'; import pkg from '../../package.json'; import { Flags } from '../types'; +/** + * Parse arguments passed to the CLI. + * + * @param argv An array of arguments passed from the user. + * + * @returns An object containing the parsed arguments. + */ export default function parseArguments(argv: string[]) { const { input, flags, help } = meow( ` diff --git a/node-src/lib/spawn.ts b/node-src/lib/spawn.ts index e0de1c925..2d37dfb0d 100644 --- a/node-src/lib/spawn.ts +++ b/node-src/lib/spawn.ts @@ -1,5 +1,13 @@ import { spawn as packageCommand } from 'yarn-or-npm'; +/** + * Spawn a subprocess to interact with the user's package manager. + * + * @param args Command arguments to pass to the package manager. + * @param options Options to pass to the package manager. + + * @returns The result from the package manager command. + */ export default function spawn( args: Parameters[0], options: Parameters[1] = {} diff --git a/node-src/lib/testLogger.ts b/node-src/lib/testLogger.ts index 9bfc421b5..61519fb19 100644 --- a/node-src/lib/testLogger.ts +++ b/node-src/lib/testLogger.ts @@ -1,3 +1,6 @@ +/** + * A noop logger used during tests. + */ export default class TestLogger { entries: any[]; diff --git a/node-src/lib/upload.ts b/node-src/lib/upload.ts index eb3a0ddbf..6b579e3fd 100644 --- a/node-src/lib/upload.ts +++ b/node-src/lib/upload.ts @@ -75,6 +75,19 @@ interface UploadBuildMutationResult { }; } +/** + * Upload build files to Chromatic. + * + * @param ctx The context set when executing the CLI. + * @param files The list of files to upload. + * @param options A collection of callbacks for each stage of the upload. + * @param options.onStart A callback for when the build upload starts. + * @param options.onProgress A callback to track progress of the upload. + * @param options.onComplete A callback for when the build upload completes. + * @param options.onError A callback for when the build upload errors. + * + * @returns A promise that resolves when the build is uploaded to Chromatic. + */ // TODO: refactor this function // eslint-disable-next-line complexity, max-statements export async function uploadBuild( @@ -217,6 +230,14 @@ interface UploadMetadataMutationResult { }; } +/** + * Upload metadata files to Chromatic for debugging issues with Chromatic support. + * + * @param ctx The context set when executing the CLI. + * @param files The list of metadata files to upload. + * + * @returns A promise that resolves when all metadata files are uploaded. + */ export async function uploadMetadata(ctx: Context, files: FileDesc[]) { const { uploadMetadata } = await ctx.client.runQuery( UploadMetadataMutation, diff --git a/node-src/lib/uploadFiles.ts b/node-src/lib/uploadFiles.ts index 119f67b73..991dbbf8d 100644 --- a/node-src/lib/uploadFiles.ts +++ b/node-src/lib/uploadFiles.ts @@ -6,6 +6,15 @@ import pLimit from 'p-limit'; import { Context, FileDesc, TargetInfo } from '../types'; import { FileReaderBlob } from './FileReaderBlob'; +/** + * Upload Storybook build files to Chromatic. + * + * @param ctx The context set when executing the CLI. + * @param targets The list of files to upload. + * @param onProgress A callback to report progress on the upload. + + * @returns A promise that resolves when all files are uploaded. + */ export async function uploadFiles( ctx: Context, targets: (FileDesc & TargetInfo)[], diff --git a/node-src/lib/uploadMetadataFiles.ts b/node-src/lib/uploadMetadataFiles.ts index f80c40c83..410c68058 100644 --- a/node-src/lib/uploadMetadataFiles.ts +++ b/node-src/lib/uploadMetadataFiles.ts @@ -12,6 +12,13 @@ import { uploadMetadata } from './upload'; const fileSize = (path: string): Promise => new Promise((resolve) => stat(path, (err, stats) => resolve(err ? 0 : stats.size))); +/** + * Upload metadata files to Chromatic for debugging issues with Chromatic support. + * + * @param ctx The context set when executing the CLI. + * + * @returns A promise that resolves when all metadata files are uploaded. + */ export async function uploadMetadataFiles(ctx: Context) { if (!ctx.announcedBuild) { ctx.log.warn('No build announced, skipping metadata upload.'); diff --git a/node-src/lib/uploadZip.ts b/node-src/lib/uploadZip.ts index f62dbb3b9..1d56239eb 100644 --- a/node-src/lib/uploadZip.ts +++ b/node-src/lib/uploadZip.ts @@ -5,6 +5,15 @@ import { FormData } from 'formdata-node'; import { Context, TargetInfo } from '../types'; import { FileReaderBlob } from './FileReaderBlob'; +/** + * Upload a zip to Chromatic instead of individual files. + * + * @param ctx The context set when executing the CLI. + * @param target The zip information to upload. + * @param onProgress A callback to report progress on the upload. + * + * @returns A promise that resolves when the zip is uploaded. + */ export async function uploadZip( ctx: Context, target: TargetInfo & { contentLength: number; localPath: string }, diff --git a/node-src/lib/waitForSentinel.ts b/node-src/lib/waitForSentinel.ts index a48fb86df..9ce55c9e2 100644 --- a/node-src/lib/waitForSentinel.ts +++ b/node-src/lib/waitForSentinel.ts @@ -7,6 +7,17 @@ import { Context } from '../types'; // completed successfully and 'ERROR' if an error occurred. const SENTINEL_SUCCESS_VALUE = 'OK'; +/** + * Wait for a sentinel file to appear within the provided URL by checking for its existence on a + * loop. + * + * @param ctx The context set when executing the CLI. + * @param file The file information for locating the file. + * @param file.name The name of the sentinel file in question. + * @param file.url The url of the sentinel file in question. + * + * @returns A promise that resolves when the sentinel file is found. + */ export async function waitForSentinel(ctx: Context, { name, url }: { name: string; url: string }) { const { experimental_abortSignal: signal } = ctx.options; diff --git a/node-src/lib/writeChromaticDiagnostics.ts b/node-src/lib/writeChromaticDiagnostics.ts index 6dd1d2c8f..0dc345e80 100644 --- a/node-src/lib/writeChromaticDiagnostics.ts +++ b/node-src/lib/writeChromaticDiagnostics.ts @@ -6,6 +6,27 @@ import { redact } from './utils'; const { writeFile } = jsonfile; +/** + * Extract important information from ctx, sort it and output into a json file + * + * @param ctx The context set when executing the CLI. + */ +export async function writeChromaticDiagnostics(ctx: Context) { + try { + await writeFile(ctx.options.diagnosticsFile, getDiagnostics(ctx), { spaces: 2 }); + ctx.log.info(wroteReport(ctx.options.diagnosticsFile, 'Chromatic diagnostics')); + } catch (error) { + ctx.log.error(error); + } +} + +/** + * Extract diagnostic information for the Chromatic diagnostics file. + * + * @param ctx The context set when executing the CLI. + * + * @returns An object containing all information for the Chromatic diagnostics file. + */ export function getDiagnostics(ctx: Context) { // Drop some fields that are not useful to have and redact sensitive fields const { argv, client, env, help, http, log, pkg, title, ...rest } = ctx; @@ -18,13 +39,3 @@ export function getDiagnostics(ctx: Context) { .map((key) => [key, data[key]]) ); } - -// Extract important information from ctx, sort it and output into a json file -export async function writeChromaticDiagnostics(ctx: Context) { - try { - await writeFile(ctx.options.diagnosticsFile, getDiagnostics(ctx), { spaces: 2 }); - ctx.log.info(wroteReport(ctx.options.diagnosticsFile, 'Chromatic diagnostics')); - } catch (error) { - ctx.log.error(error); - } -} diff --git a/node-src/tasks/index.ts b/node-src/tasks/index.ts index 28619c4a5..c647fdd44 100644 --- a/node-src/tasks/index.ts +++ b/node-src/tasks/index.ts @@ -26,6 +26,13 @@ export const runUploadBuild = [ export const runPatchBuild = [prepareWorkspace, ...runUploadBuild, restoreWorkspace]; +/** + * Prepare the list of tasks to run for a new build. + * + * @param options The context options set when executing the CLI. + * + * @returns The list of tasks to be completed. + */ export default function index(options: Context['options']): Listr.ListrTask[] { const tasks = options.patchHeadRef && options.patchBaseRef ? runUploadBuild : runUploadBuild; diff --git a/node-src/ui/messages/errors/fatalError.ts b/node-src/ui/messages/errors/fatalError.ts index 210b6bb56..3a04f2c92 100644 --- a/node-src/ui/messages/errors/fatalError.ts +++ b/node-src/ui/messages/errors/fatalError.ts @@ -14,6 +14,15 @@ const buildFields = ({ id, number, storybookUrl = undefined, webUrl = undefined ...(webUrl && { webUrl }), }); +/** + * Generate an error message from a fatal error that occurred when executing the CLI. + * + * @param ctx The context set when executing the CLI. + * @param error The error received. + * @param timestamp When the error occurred. + * + * @returns A formatted error message for the provided fatal error. + */ // TODO: refactor this function // eslint-disable-next-line complexity export default function fatalError( diff --git a/node-src/ui/messages/errors/fetchError.ts b/node-src/ui/messages/errors/fetchError.ts index d0034f451..b34709d89 100644 --- a/node-src/ui/messages/errors/fetchError.ts +++ b/node-src/ui/messages/errors/fetchError.ts @@ -6,6 +6,18 @@ import { lcfirst } from '../../../lib/utils'; import { error as icon } from '../../components/icons'; import link from '../../components/link'; +/** + * Generate a failure message for a fetch error. + * + * @param context The context of the error message (which task were we running when the error occurred) + * @param context.title Name of the task when the error occurred. + * @param error The fetch error received. + * @param error.error The full error received from the fetch request. + * @param error.response The response from the fetch request. + * @param error.statusCode The status code from the fetch request. + * + * @returns A message about a fetch error. + */ export default function fetchError( { title }: { title: string }, { diff --git a/node-src/ui/messages/errors/graphqlError.ts b/node-src/ui/messages/errors/graphqlError.ts index e843eea2a..597c56f8f 100644 --- a/node-src/ui/messages/errors/graphqlError.ts +++ b/node-src/ui/messages/errors/graphqlError.ts @@ -7,6 +7,17 @@ import link from '../../components/link'; const lcfirst = (str: string) => `${str.charAt(0).toLowerCase()}${str.slice(1)}`; +/** + * Generate a failure message for a GraphQL error. + * + * @param context The context of the error message (which task were we running when the error occurred) + * @param context.title Name of the task when the error occurred. + * @param error The GraphQL error received. + * @param error.message The error message from GraphQL. + * @param error.extensions Additional details relating to the GraphQL error. + * + * @returns A message about a GraphQL error. + */ export default function graphqlError( { title }: { title: string }, { message, extensions }: GraphQLError diff --git a/node-src/ui/messages/errors/runtimeError.ts b/node-src/ui/messages/errors/runtimeError.ts index 1897a605f..9c6426e95 100644 --- a/node-src/ui/messages/errors/runtimeError.ts +++ b/node-src/ui/messages/errors/runtimeError.ts @@ -5,6 +5,16 @@ import { dedent } from 'ts-dedent'; import { Context } from '../../../types'; import { error, warning } from '../../components/icons'; +/** + * Generate a failure message for a runtime error during execution. + * + * @param details Information relating to a runtime error while running the CLI. + * @param details.options Options specified when running the CLI. + * @param details.runtimeErrors Any runtime errors encountered during execution. + * @param details.runtimeWarnings Any runtime warnings encountered during execution. + * + * @returns A message about a runtime error during execution. + */ // TODO: refactor this function // eslint-disable-next-line complexity export default function runtimeError({ diff --git a/node-src/ui/messages/errors/taskError.ts b/node-src/ui/messages/errors/taskError.ts index 83a45ab43..3e4695346 100644 --- a/node-src/ui/messages/errors/taskError.ts +++ b/node-src/ui/messages/errors/taskError.ts @@ -2,8 +2,19 @@ import chalk from 'chalk'; import { error } from '../../components/icons'; -const lcfirst = (str: string) => `${str.charAt(0).toLowerCase()}${str.slice(1)}`; - +/** + * Generate a failure message for a task that errored. + * + * @param task The task object that received an error. + * @param task.title The title of the errored task. + * @param err The error message received from the task. + * + * @returns A message about a failed task. + */ export default function taskError({ title }: { title: string }, err: Error) { return [chalk`${error} {bold Failed to ${lcfirst(title)}}`, err.message].join('\n'); } + +function lcfirst(str: string) { + return `${str.charAt(0).toLowerCase()}${str.slice(1)}`; +} diff --git a/node-src/ui/messages/errors/uploadFailed.ts b/node-src/ui/messages/errors/uploadFailed.ts index 18c5f9bca..aafb22847 100644 --- a/node-src/ui/messages/errors/uploadFailed.ts +++ b/node-src/ui/messages/errors/uploadFailed.ts @@ -4,12 +4,15 @@ import { dedent } from 'ts-dedent'; import { FileDesc, TargetInfo } from '../../../types'; import { error as icon } from '../../components/icons'; -const encode = (path: string) => - path - .split('/') - .map((component) => encodeURIComponent(component)) - .join('/'); - +/** + * Generate a failure message for a file that failed to upload. + * + * @param file Information about the file that failed to upload. + * @param file.target Path information for the file. + * @param debug Enable debug output. + * + * @returns A message about a file that failed to upload. + */ export function uploadFailed({ target }: { target: FileDesc & TargetInfo }, debug = false) { const diagnosis = encode(target.targetPath) === target.targetPath @@ -22,3 +25,10 @@ export function uploadFailed({ target }: { target: FileDesc & TargetInfo }, debu `); return debug ? message + JSON.stringify(target, undefined, 2) : message; } + +function encode(path: string) { + return path + .split('/') + .map((component) => encodeURIComponent(component)) + .join('/'); +} diff --git a/package.json b/package.json index 818c70e2e..063659b71 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "eslint": "^9.10.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsdoc": "^48.2.6", "eslint-plugin-json": "^3.1.0", "eslint-plugin-no-secrets": "^1.0.2", "eslint-plugin-react": "^7.33.2", diff --git a/test-stories/A.js b/test-stories/A.js index 65b3aaf88..3fe79c929 100644 --- a/test-stories/A.js +++ b/test-stories/A.js @@ -10,6 +10,14 @@ const style = { backgroundColor: 'darkkhaki', }; +/** + * A div used for test stories. + * + * @param param0 Additional properties for a
element. + * @param param0.backgroundColor The desired background color for the div. + * + * @returns A stsyled div element. + */ export default function A({ backgroundColor, ...props }) { let computedStyle = style; if (backgroundColor) { diff --git a/test-stories/B.js b/test-stories/B.js index cd6e52454..a2a50fbe6 100644 --- a/test-stories/B.js +++ b/test-stories/B.js @@ -9,6 +9,13 @@ const style = { backgroundColor: 'blueviolet', }; +/** + * A span used for test stories. + * + * @param props Additional properties for a element. + * + * @returns A styled span element. + */ export default function B(props) { return ; } diff --git a/test-stories/Star.js b/test-stories/Star.js index 1ddc7bbd2..497254e58 100644 --- a/test-stories/Star.js +++ b/test-stories/Star.js @@ -1,5 +1,10 @@ import React from 'react'; +/** + * A star svg used for test stories. + * + * @returns A star svg. + */ export default function Star() { return ( diff --git a/yarn.lock b/yarn.lock index e415a6edc..29e9038b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1753,6 +1753,17 @@ __metadata: languageName: node linkType: hard +"@es-joy/jsdoccomment@npm:~0.46.0": + version: 0.46.0 + resolution: "@es-joy/jsdoccomment@npm:0.46.0" + dependencies: + comment-parser: "npm:1.4.1" + esquery: "npm:^1.6.0" + jsdoc-type-pratt-parser: "npm:~4.0.0" + checksum: 10c0/a7a67936ebf6d9aaf74af018c3ac744769af3552b05ad9b88fca96b2ffdca16e724b0ff497f53634ec4cca81e98d8c471b6b6bde0fa5b725af4222ad9a0707f0 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.20.2": version: 0.20.2 resolution: "@esbuild/aix-ppc64@npm:0.20.2" @@ -5870,6 +5881,13 @@ __metadata: languageName: node linkType: hard +"are-docs-informative@npm:^0.0.2": + version: 0.0.2 + resolution: "are-docs-informative@npm:0.0.2" + checksum: 10c0/f0326981bd699c372d268b526b170a28f2e1aec2cf99d7de0686083528427ecdf6ae41fef5d9988e224a5616298af747ad8a76e7306b0a7c97cc085a99636d60 + languageName: node + linkType: hard + "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -6909,6 +6927,7 @@ __metadata: eslint: "npm:^9.10.0" eslint-config-prettier: "npm:^9.0.0" eslint-plugin-import: "npm:^2.28.1" + eslint-plugin-jsdoc: "npm:^48.2.6" eslint-plugin-json: "npm:^3.1.0" eslint-plugin-no-secrets: "npm:^1.0.2" eslint-plugin-react: "npm:^7.33.2" @@ -7291,6 +7310,13 @@ __metadata: languageName: node linkType: hard +"comment-parser@npm:1.4.1": + version: 1.4.1 + resolution: "comment-parser@npm:1.4.1" + checksum: 10c0/d6c4be3f5be058f98b24f2d557f745d8fe1cc9eb75bebbdccabd404a0e1ed41563171b16285f593011f8b6a5ec81f564fb1f2121418ac5cbf0f49255bf0840dd + languageName: node + linkType: hard + "commondir@npm:^1.0.1": version: 1.0.1 resolution: "commondir@npm:1.0.1" @@ -8444,6 +8470,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.5.3": + version: 1.5.4 + resolution: "es-module-lexer@npm:1.5.4" + checksum: 10c0/300a469488c2f22081df1e4c8398c78db92358496e639b0df7f89ac6455462aaf5d8893939087c1a1cbcbf20eed4610c70e0bcb8f3e4b0d80a5d2611c539408c + languageName: node + linkType: hard + "es-object-atoms@npm:^1.0.0": version: 1.0.0 resolution: "es-object-atoms@npm:1.0.0" @@ -8908,6 +8941,27 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jsdoc@npm:^48.2.6": + version: 48.11.0 + resolution: "eslint-plugin-jsdoc@npm:48.11.0" + dependencies: + "@es-joy/jsdoccomment": "npm:~0.46.0" + are-docs-informative: "npm:^0.0.2" + comment-parser: "npm:1.4.1" + debug: "npm:^4.3.5" + escape-string-regexp: "npm:^4.0.0" + espree: "npm:^10.1.0" + esquery: "npm:^1.6.0" + parse-imports: "npm:^2.1.1" + semver: "npm:^7.6.3" + spdx-expression-parse: "npm:^4.0.0" + synckit: "npm:^0.9.1" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + checksum: 10c0/f78bac109e62f838c14f90ebd572a06a865f2896a16201c9324cb92be25b5ba8deb54ee1d8ea36232ee53a41c177d5d5ac80662c0fe2479d1e1e1e7633385659 + languageName: node + linkType: hard + "eslint-plugin-json-files@npm:^4.1.0": version: 4.2.0 resolution: "eslint-plugin-json-files@npm:4.2.0" @@ -9190,7 +9244,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.5.0": +"esquery@npm:^1.5.0, esquery@npm:^1.6.0": version: 1.6.0 resolution: "esquery@npm:1.6.0" dependencies: @@ -11654,6 +11708,13 @@ __metadata: languageName: node linkType: hard +"jsdoc-type-pratt-parser@npm:~4.0.0": + version: 4.0.0 + resolution: "jsdoc-type-pratt-parser@npm:4.0.0" + checksum: 10c0/b23ef7bbbe2f56d72630d1c5a233dc9fecaff399063d373c57bef136908c1b05e723dac107177303c03ccf8d75aa51507510b282aa567600477479c5ea0c36d1 + languageName: node + linkType: hard + "jsesc@npm:^2.5.1": version: 2.5.2 resolution: "jsesc@npm:2.5.2" @@ -14450,6 +14511,16 @@ __metadata: languageName: node linkType: hard +"parse-imports@npm:^2.1.1": + version: 2.2.1 + resolution: "parse-imports@npm:2.2.1" + dependencies: + es-module-lexer: "npm:^1.5.3" + slashes: "npm:^3.0.12" + checksum: 10c0/bc541ce4ef2ff77d53247de39a956e0ee7a1a4b9b175c3e0f898222fe7994595f011491154db4ed408cbaf5049ede9d0b6624125565be208e973a54420cbe069 + languageName: node + linkType: hard + "parse-json@npm:^4.0.0": version: 4.0.0 resolution: "parse-json@npm:4.0.0" @@ -16416,7 +16487,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.6.1": +"semver@npm:^7.6.1, semver@npm:^7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -16639,6 +16710,13 @@ __metadata: languageName: node linkType: hard +"slashes@npm:^3.0.12": + version: 3.0.12 + resolution: "slashes@npm:3.0.12" + checksum: 10c0/71ca2a1fcd1ab6814b0fdb8cf9c33a3d54321deec2aa8d173510f0086880201446021a9b9e6a18561f7c472b69a2145977c6a8fb9c53a8ff7be31778f203d175 + languageName: node + linkType: hard + "slice-ansi@npm:0.0.4": version: 0.0.4 resolution: "slice-ansi@npm:0.0.4" @@ -16890,6 +16968,16 @@ __metadata: languageName: node linkType: hard +"spdx-expression-parse@npm:^4.0.0": + version: 4.0.0 + resolution: "spdx-expression-parse@npm:4.0.0" + dependencies: + spdx-exceptions: "npm:^2.1.0" + spdx-license-ids: "npm:^3.0.0" + checksum: 10c0/965c487e77f4fb173f1c471f3eef4eb44b9f0321adc7f93d95e7620da31faa67d29356eb02523cd7df8a7fc1ec8238773cdbf9e45bd050329d2b26492771b736 + languageName: node + linkType: hard + "spdx-license-ids@npm:^3.0.0": version: 3.0.17 resolution: "spdx-license-ids@npm:3.0.17" @@ -17377,6 +17465,16 @@ __metadata: languageName: node linkType: hard +"synckit@npm:^0.9.1": + version: 0.9.1 + resolution: "synckit@npm:0.9.1" + dependencies: + "@pkgr/core": "npm:^0.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/d8b89e1bf30ba3ffb469d8418c836ad9c0c062bf47028406b4d06548bc66af97155ea2303b96c93bf5c7c0f0d66153a6fbd6924c76521b434e6a9898982abc2e + languageName: node + linkType: hard + "table-layout@npm:^1.0.2": version: 1.0.2 resolution: "table-layout@npm:1.0.2"