diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..daef242 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,69 @@ +name: Coverage + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'pnpm' + + - name: Install dependencies + run: | + pnpm install --frozen-lockfile || pnpm install --no-frozen-lockfile + + - name: Run tests with coverage + run: pnpm test:coverage + + - name: Generate coverage badge + run: node scripts/generate-coverage-badge.js + + - name: Coverage Badge + uses: tj-actions/coverage-badge-js@v1 + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + with: + coverage-file: './coverage/coverage-summary.json' + + - name: Verify Changed files + uses: tj-actions/verify-changed-files@v12 + id: verify-changed-files + with: + files: coverage-badge.json + + - name: Create Coverage Badge + if: steps.verify-changed-files.outputs.files_changed == 'true' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "docs: update coverage badge" + title: "๐Ÿ“Š Update Coverage Badge" + body: | + ## Coverage Report Update + + This PR updates the coverage badge based on the latest test results. + + **Coverage Summary:** + - Statements: ${{ env.COVERAGE_STATEMENTS }}% + - Branches: ${{ env.COVERAGE_BRANCHES }}% + - Functions: ${{ env.COVERAGE_FUNCTIONS }}% + - Lines: ${{ env.COVERAGE_LINES }}% + + Auto-generated by GitHub Actions. + branch: update-coverage-badge diff --git a/.gitignore b/.gitignore index c453cb4..f60ee75 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,11 @@ node_modules .DS_Store .vscode keep/* -!keep/README.md \ No newline at end of file +!keep/README.md + +# Coverage reports +coverage/ +*.lcov + +# Generated files +coverage-badge.json \ No newline at end of file diff --git a/README.md b/README.md index 4fb3fc8..a4e9b0f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # create commit ![](https://img.shields.io/badge/node-%5E14-brightgreen.svg) +![Coverage](https://img.shields.io/badge/coverage-83%25-brightgreen) +![Tests](https://img.shields.io/badge/tests-49%20passed-brightgreen) create commit in human way. @@ -20,6 +22,28 @@ bun install -g OwlTing/cz > Add `export PATH="$(yarn global bin):$PATH"` to your `~./zshrc` if you installed it by yarn global +## Development + +### Running Tests +```shell +# Run tests in watch mode +pnpm test + +# Run tests once +pnpm test:run + +# Run tests with coverage +pnpm test:coverage + +# Generate coverage badge +pnpm coverage:badge +``` + +### Pre-commit Hooks +This project uses [lefthook](https://github.com/evilmartians/lefthook) for pre-commit hooks that automatically run: +- Unit tests (`pnpm test:run`) +- TypeScript compilation check (`pnpm build`) + ## Update version ```shell # npm @@ -179,7 +203,7 @@ input Jira issue ID name: 'storybook', emoji: '๐Ÿ“š', description: 'New storybook', - value: 'story' + value: 'storybook' }, { name: 'revert', @@ -193,5 +217,5 @@ input Jira issue ID ## Todo - [x] adapt for other projects prefix -- [ ] unit test +- [x] unit test - [ ] CLI diff --git a/dist/index.cjs b/dist/index.cjs index 88f5762..93e483e 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -88,7 +88,7 @@ const commitTypes = [ name: "storybook", emoji: "\u{1F4DA}", description: "New storybook", - value: "story" + value: "storybook" }, { name: "revert", @@ -128,100 +128,147 @@ const projects = [ const __dirname$3 = node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))); const rootPath$1 = node_path.resolve(__dirname$3, "../"); -let defaultProjectValue = ""; -try { - const filePath = node_path.resolve(rootPath$1, "keep/cz_config.json"); - const config = fs__default.readFileSync(filePath); - defaultProjectValue = JSON.parse(config).defaultProject; -} catch (e) { - console.log(picocolors__default.yellow(picocolors__default.italic(" \u{1F4A1} You can try `cz -i` to choose a default project prefix. "))); - defaultProjectValue = ""; -} -const typesList = commitTypes.map((type) => ({ - title: type.name, - description: `${type.emoji} ${type.description}`, - value: type.value, - emoji: type.emoji -})); -const step_type$1 = { - type: "autocomplete", - name: "commit_type", - message: "Pick a commit type.", - choices: typesList, - fallback: "No matched type." -}; -const step_message = { - type: "text", - name: "commit_message", - message: (prev) => { - const target = typesList.find((type) => type.value === prev); - return `${target.emoji} ${target.title}`; - }, - validate: (value) => { - if (!value) { - return "Commit message is required."; - } - return true; +const loadProjectConfig = (filePath) => { + try { + const config = fs__default.readFileSync(filePath, "utf8"); + return JSON.parse(config); + } catch (e) { + return null; } }; -const step_description = { - type: "text", - name: "commit_description", - message: "Commit description (optional)", - initial: "", - validate: (value) => { - if (value.length > 100) { - return "Description is too long."; - } - return true; - } +const getDefaultProjectValue = (config) => { + return config?.defaultProject || ""; }; -const step_is_jira = { - type: "confirm", - name: "is_jira", - message: "Tag Jira issue ?", - initial: false +const buildCommitTypesList = () => { + return commitTypes.map((type) => ({ + title: type.name, + description: `${type.emoji} ${type.description}`, + value: type.value, + emoji: type.emoji + })); }; -const projectsList$1 = projects.map((project) => ({ - title: project.name, - description: `[${project.prefix}-13845] title`, - value: project.value -})); -const defaultProject = projectsList$1.find((project) => project.value === defaultProjectValue); -const step_is_default_project = { - type: (prev) => prev ? "confirm" : null, - name: "is_default_project", - message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`, - initial: true +const buildProjectsList = () => { + return projects.map((project) => ({ + title: project.name, + description: `[${project.prefix}-13845] title`, + value: project.value + })); }; -const step_project_type = { - type: (prev, { is_jira }) => { - return is_jira ? defaultProject?.value && prev ? null : "autocomplete" : null; - }, - name: "project_type", - message: "Pick a project type.", - choices: projectsList$1, - initial: "owlpay", - fallback: "No matched project." +const findCommitType = (commitTypeValue) => { + return commitTypes.find((type) => type.value === commitTypeValue); }; -const step_jira_id = { - type: (prev) => prev ? "number" : null, - name: "jira_id", - message: "Jira issue id", - onRender() { - this.msg = picocolors__default.bgCyan(picocolors__default.white(" Jira issue ID ")); - }, - validate: (value) => { - if (!value) { - return "Jira issue ID is required."; - } - return true; +const findProject = (projectValue) => { + return projects.find((project) => project.value === projectValue); +}; +const buildCommitTitle = (commitType, message) => { + const type = findCommitType(commitType); + if (!type) { + throw new Error(`Invalid commit type: ${commitType}`); } + return `${type.emoji} ${commitType}: ${message}`; }; -const cli = async () => { - let isCanceled = false; - const order = [ - step_type$1, +const buildFinalCommitMessage = (commitTitle, response, defaultProjectValue) => { + const { is_jira, is_default_project, project_type, jira_id } = response; + if (!is_jira) { + return commitTitle; + } + const typeResponse = is_default_project ? defaultProjectValue : project_type; + const projectType = findProject(typeResponse); + if (!projectType) { + throw new Error(`Invalid project type: ${typeResponse}`); + } + if (!jira_id) { + throw new Error("Jira ID is required when using Jira integration"); + } + return `[${projectType.prefix}-${jira_id}] ${commitTitle}`; +}; +const buildGitCommands = (commitMessage, description) => { + if (description) { + return ["commit", "-m", commitMessage, "-m", description]; + } + return ["commit", "-m", commitMessage]; +}; +const parseCommitResult = (stdout) => { + const branchHashName = stdout.match(/\[(.*?)\]/)?.pop(); + if (!branchHashName) { + throw new Error("Could not parse commit result"); + } + const [branchName, branchHash] = branchHashName.split(" "); + return { branch: branchName, hash: branchHash }; +}; +const buildPromptSteps = (defaultProjectValue, projectsList, typesList) => { + const defaultProject = projectsList.find((project) => project.value === defaultProjectValue); + const step_type = { + type: "autocomplete", + name: "commit_type", + message: "Pick a commit type.", + choices: typesList, + fallback: "No matched type." + }; + const step_message = { + type: "text", + name: "commit_message", + message: (prev) => { + const target = typesList.find((type) => type.value === prev); + return `${target.emoji} ${target.title}`; + }, + validate: (value) => { + if (!value) { + return "Commit message is required."; + } + return true; + } + }; + const step_description = { + type: "text", + name: "commit_description", + message: "Commit description (optional)", + initial: "", + validate: (value) => { + if (value.length > 100) { + return "Description is too long."; + } + return true; + } + }; + const step_is_jira = { + type: "confirm", + name: "is_jira", + message: "Tag Jira issue ?", + initial: false + }; + const step_is_default_project = { + type: (prev) => prev ? "confirm" : null, + name: "is_default_project", + message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`, + initial: true + }; + const step_project_type = { + type: (prev, { is_jira }) => { + return is_jira ? defaultProject?.value && prev ? null : "autocomplete" : null; + }, + name: "project_type", + message: "Pick a project type.", + choices: projectsList, + initial: "owlpay", + fallback: "No matched project." + }; + const step_jira_id = { + type: (prev) => prev ? "number" : null, + name: "jira_id", + message: "Jira issue id", + onRender() { + this.msg = picocolors__default.bgCyan(picocolors__default.white(" Jira issue ID ")); + }, + validate: (value) => { + if (!value) { + return "Jira issue ID is required."; + } + return true; + } + }; + return [ + step_type, step_message, step_description, step_is_jira, @@ -229,7 +276,52 @@ const cli = async () => { step_project_type, step_jira_id ].filter(Boolean); - const response = await prompts__default(order, { +}; +const showConfigMissingWarning = () => { + console.log(picocolors__default.yellow(picocolors__default.italic(" \u{1F4A1} You can try `cz -i` to choose a default project prefix. "))); +}; +const showCancelMessage = () => { + console.log(picocolors__default.magenta(" commit abort. ")); +}; +const showCommitResult = (result) => { + console.log("-----------------------------------------------------------"); + console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Title "))} ${picocolors__default.green(result.title)}`); + if (result.description) { + console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Description "))} ${picocolors__default.green(result.description)}`); + } + if (result.hash && result.branch) { + console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Commit hash "))} ${picocolors__default.bold(picocolors__default.cyan(` ${result.hash} `))} (${picocolors__default.italic(picocolors__default.green(result.branch))})`); + } +}; +const showGitOutput = (stdout, stderr) => { + console.log("-----------------------------------------------------------"); + console.log(picocolors__default.dim(stdout)); + if (stderr && stderr !== "") { + console.log("-----------------------------------------------------------"); + console.log(picocolors__default.dim(stderr)); + } + console.log("-----------------------------------------------------------"); +}; +const showGitError = (error) => { + console.log(picocolors__default.red(error.stderr)); + if (error.exitCode === 1) { + console.log(picocolors__default.bgRed(" No changes added to commit. ")); + } else { + console.error(error); + } +}; +const cli = async () => { + const configPath = node_path.resolve(rootPath$1, "keep/cz_config.json"); + const config = loadProjectConfig(configPath); + const defaultProjectValue = getDefaultProjectValue(config); + if (!config) { + showConfigMissingWarning(); + } + const typesList = buildCommitTypesList(); + const projectsList = buildProjectsList(); + const steps = buildPromptSteps(defaultProjectValue, projectsList, typesList); + let isCanceled = false; + const response = await prompts__default(steps, { onSubmit: (prompt, answers) => { if (answers === void 0) { isCanceled = true; @@ -242,38 +334,26 @@ const cli = async () => { } }); if (isCanceled) { - console.log(picocolors__default.magenta(" commit abort. ")); + showCancelMessage(); return false; } - const { commit_type, commit_message, commit_description, is_jira, is_default_project, project_type, jira_id } = response; - const type = typesList.find((type2) => type2.value === commit_type); - const commitTitle = `${type.emoji} ${commit_type}: ${commit_message}`; - const typeResponse = is_default_project ? defaultProject?.value : project_type; - const projectType = projects.find((project) => project.value === typeResponse); - const result = is_jira ? `[${projectType.prefix}-${jira_id}] ${commitTitle}` : commitTitle; try { - const commands = commit_description ? ["commit", "-m", result, "-m", commit_description] : ["commit", "-m", result]; - const commitResult = await execa__default("git", commands); - const branchHashName = commitResult.stdout.match(/\[(.*?)\]/).pop(); - const [branchName, branchHash] = branchHashName.split(" "); - console.log("-----------------------------------------------------------"); - console.log(picocolors__default.dim(commitResult.stdout)); - if (commitResult.stderr !== "") { - console.log("-----------------------------------------------------------"); - console.log(picocolors__default.dim(commitResult.stderr)); - } - console.log("-----------------------------------------------------------"); - console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Title "))} ${picocolors__default.green(result)}`); - if (commit_description) { - console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Description "))} ${picocolors__default.green(commit_description)}`); - } - console.log(`${picocolors__default.bgGreen(picocolors__default.bold(" Commit hash "))} ${picocolors__default.bold(picocolors__default.cyan(` ${branchHash} `))} (${picocolors__default.italic(picocolors__default.green(branchName))})`); + const commitTitle = buildCommitTitle(response.commit_type, response.commit_message); + const finalCommitMessage = buildFinalCommitMessage(commitTitle, response, defaultProjectValue); + const gitCommands = buildGitCommands(finalCommitMessage, response.commit_description); + const commitResult = await execa__default("git", gitCommands); + const { branch, hash } = parseCommitResult(commitResult.stdout); + showGitOutput(commitResult.stdout, commitResult.stderr); + showCommitResult({ + title: finalCommitMessage, + description: response.commit_description, + hash, + branch + }); + return true; } catch (error) { - console.log(picocolors__default.red(error.stderr)); - if (error.exitCode === 1) - console.log(picocolors__default.bgRed(" No changes added to commit. ")); - else - console.error(error); + showGitError(error); + return false; } }; diff --git a/dist/index.mjs b/dist/index.mjs index 179bf82..6c2010d 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -77,7 +77,7 @@ const commitTypes = [ name: "storybook", emoji: "\u{1F4DA}", description: "New storybook", - value: "story" + value: "storybook" }, { name: "revert", @@ -117,100 +117,147 @@ const projects = [ const __dirname$2 = dirname(fileURLToPath(import.meta.url)); const rootPath$1 = resolve(__dirname$2, "../"); -let defaultProjectValue = ""; -try { - const filePath = resolve(rootPath$1, "keep/cz_config.json"); - const config = fs.readFileSync(filePath); - defaultProjectValue = JSON.parse(config).defaultProject; -} catch (e) { - console.log(picocolors.yellow(picocolors.italic(" \u{1F4A1} You can try `cz -i` to choose a default project prefix. "))); - defaultProjectValue = ""; -} -const typesList = commitTypes.map((type) => ({ - title: type.name, - description: `${type.emoji} ${type.description}`, - value: type.value, - emoji: type.emoji -})); -const step_type$1 = { - type: "autocomplete", - name: "commit_type", - message: "Pick a commit type.", - choices: typesList, - fallback: "No matched type." -}; -const step_message = { - type: "text", - name: "commit_message", - message: (prev) => { - const target = typesList.find((type) => type.value === prev); - return `${target.emoji} ${target.title}`; - }, - validate: (value) => { - if (!value) { - return "Commit message is required."; - } - return true; +const loadProjectConfig = (filePath) => { + try { + const config = fs.readFileSync(filePath, "utf8"); + return JSON.parse(config); + } catch (e) { + return null; } }; -const step_description = { - type: "text", - name: "commit_description", - message: "Commit description (optional)", - initial: "", - validate: (value) => { - if (value.length > 100) { - return "Description is too long."; - } - return true; - } +const getDefaultProjectValue = (config) => { + return config?.defaultProject || ""; }; -const step_is_jira = { - type: "confirm", - name: "is_jira", - message: "Tag Jira issue ?", - initial: false +const buildCommitTypesList = () => { + return commitTypes.map((type) => ({ + title: type.name, + description: `${type.emoji} ${type.description}`, + value: type.value, + emoji: type.emoji + })); }; -const projectsList$1 = projects.map((project) => ({ - title: project.name, - description: `[${project.prefix}-13845] title`, - value: project.value -})); -const defaultProject = projectsList$1.find((project) => project.value === defaultProjectValue); -const step_is_default_project = { - type: (prev) => prev ? "confirm" : null, - name: "is_default_project", - message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`, - initial: true +const buildProjectsList = () => { + return projects.map((project) => ({ + title: project.name, + description: `[${project.prefix}-13845] title`, + value: project.value + })); }; -const step_project_type = { - type: (prev, { is_jira }) => { - return is_jira ? defaultProject?.value && prev ? null : "autocomplete" : null; - }, - name: "project_type", - message: "Pick a project type.", - choices: projectsList$1, - initial: "owlpay", - fallback: "No matched project." +const findCommitType = (commitTypeValue) => { + return commitTypes.find((type) => type.value === commitTypeValue); }; -const step_jira_id = { - type: (prev) => prev ? "number" : null, - name: "jira_id", - message: "Jira issue id", - onRender() { - this.msg = picocolors.bgCyan(picocolors.white(" Jira issue ID ")); - }, - validate: (value) => { - if (!value) { - return "Jira issue ID is required."; - } - return true; +const findProject = (projectValue) => { + return projects.find((project) => project.value === projectValue); +}; +const buildCommitTitle = (commitType, message) => { + const type = findCommitType(commitType); + if (!type) { + throw new Error(`Invalid commit type: ${commitType}`); } + return `${type.emoji} ${commitType}: ${message}`; }; -const cli = async () => { - let isCanceled = false; - const order = [ - step_type$1, +const buildFinalCommitMessage = (commitTitle, response, defaultProjectValue) => { + const { is_jira, is_default_project, project_type, jira_id } = response; + if (!is_jira) { + return commitTitle; + } + const typeResponse = is_default_project ? defaultProjectValue : project_type; + const projectType = findProject(typeResponse); + if (!projectType) { + throw new Error(`Invalid project type: ${typeResponse}`); + } + if (!jira_id) { + throw new Error("Jira ID is required when using Jira integration"); + } + return `[${projectType.prefix}-${jira_id}] ${commitTitle}`; +}; +const buildGitCommands = (commitMessage, description) => { + if (description) { + return ["commit", "-m", commitMessage, "-m", description]; + } + return ["commit", "-m", commitMessage]; +}; +const parseCommitResult = (stdout) => { + const branchHashName = stdout.match(/\[(.*?)\]/)?.pop(); + if (!branchHashName) { + throw new Error("Could not parse commit result"); + } + const [branchName, branchHash] = branchHashName.split(" "); + return { branch: branchName, hash: branchHash }; +}; +const buildPromptSteps = (defaultProjectValue, projectsList, typesList) => { + const defaultProject = projectsList.find((project) => project.value === defaultProjectValue); + const step_type = { + type: "autocomplete", + name: "commit_type", + message: "Pick a commit type.", + choices: typesList, + fallback: "No matched type." + }; + const step_message = { + type: "text", + name: "commit_message", + message: (prev) => { + const target = typesList.find((type) => type.value === prev); + return `${target.emoji} ${target.title}`; + }, + validate: (value) => { + if (!value) { + return "Commit message is required."; + } + return true; + } + }; + const step_description = { + type: "text", + name: "commit_description", + message: "Commit description (optional)", + initial: "", + validate: (value) => { + if (value.length > 100) { + return "Description is too long."; + } + return true; + } + }; + const step_is_jira = { + type: "confirm", + name: "is_jira", + message: "Tag Jira issue ?", + initial: false + }; + const step_is_default_project = { + type: (prev) => prev ? "confirm" : null, + name: "is_default_project", + message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`, + initial: true + }; + const step_project_type = { + type: (prev, { is_jira }) => { + return is_jira ? defaultProject?.value && prev ? null : "autocomplete" : null; + }, + name: "project_type", + message: "Pick a project type.", + choices: projectsList, + initial: "owlpay", + fallback: "No matched project." + }; + const step_jira_id = { + type: (prev) => prev ? "number" : null, + name: "jira_id", + message: "Jira issue id", + onRender() { + this.msg = picocolors.bgCyan(picocolors.white(" Jira issue ID ")); + }, + validate: (value) => { + if (!value) { + return "Jira issue ID is required."; + } + return true; + } + }; + return [ + step_type, step_message, step_description, step_is_jira, @@ -218,7 +265,52 @@ const cli = async () => { step_project_type, step_jira_id ].filter(Boolean); - const response = await prompts(order, { +}; +const showConfigMissingWarning = () => { + console.log(picocolors.yellow(picocolors.italic(" \u{1F4A1} You can try `cz -i` to choose a default project prefix. "))); +}; +const showCancelMessage = () => { + console.log(picocolors.magenta(" commit abort. ")); +}; +const showCommitResult = (result) => { + console.log("-----------------------------------------------------------"); + console.log(`${picocolors.bgGreen(picocolors.bold(" Title "))} ${picocolors.green(result.title)}`); + if (result.description) { + console.log(`${picocolors.bgGreen(picocolors.bold(" Description "))} ${picocolors.green(result.description)}`); + } + if (result.hash && result.branch) { + console.log(`${picocolors.bgGreen(picocolors.bold(" Commit hash "))} ${picocolors.bold(picocolors.cyan(` ${result.hash} `))} (${picocolors.italic(picocolors.green(result.branch))})`); + } +}; +const showGitOutput = (stdout, stderr) => { + console.log("-----------------------------------------------------------"); + console.log(picocolors.dim(stdout)); + if (stderr && stderr !== "") { + console.log("-----------------------------------------------------------"); + console.log(picocolors.dim(stderr)); + } + console.log("-----------------------------------------------------------"); +}; +const showGitError = (error) => { + console.log(picocolors.red(error.stderr)); + if (error.exitCode === 1) { + console.log(picocolors.bgRed(" No changes added to commit. ")); + } else { + console.error(error); + } +}; +const cli = async () => { + const configPath = resolve(rootPath$1, "keep/cz_config.json"); + const config = loadProjectConfig(configPath); + const defaultProjectValue = getDefaultProjectValue(config); + if (!config) { + showConfigMissingWarning(); + } + const typesList = buildCommitTypesList(); + const projectsList = buildProjectsList(); + const steps = buildPromptSteps(defaultProjectValue, projectsList, typesList); + let isCanceled = false; + const response = await prompts(steps, { onSubmit: (prompt, answers) => { if (answers === void 0) { isCanceled = true; @@ -231,38 +323,26 @@ const cli = async () => { } }); if (isCanceled) { - console.log(picocolors.magenta(" commit abort. ")); + showCancelMessage(); return false; } - const { commit_type, commit_message, commit_description, is_jira, is_default_project, project_type, jira_id } = response; - const type = typesList.find((type2) => type2.value === commit_type); - const commitTitle = `${type.emoji} ${commit_type}: ${commit_message}`; - const typeResponse = is_default_project ? defaultProject?.value : project_type; - const projectType = projects.find((project) => project.value === typeResponse); - const result = is_jira ? `[${projectType.prefix}-${jira_id}] ${commitTitle}` : commitTitle; try { - const commands = commit_description ? ["commit", "-m", result, "-m", commit_description] : ["commit", "-m", result]; - const commitResult = await execa("git", commands); - const branchHashName = commitResult.stdout.match(/\[(.*?)\]/).pop(); - const [branchName, branchHash] = branchHashName.split(" "); - console.log("-----------------------------------------------------------"); - console.log(picocolors.dim(commitResult.stdout)); - if (commitResult.stderr !== "") { - console.log("-----------------------------------------------------------"); - console.log(picocolors.dim(commitResult.stderr)); - } - console.log("-----------------------------------------------------------"); - console.log(`${picocolors.bgGreen(picocolors.bold(" Title "))} ${picocolors.green(result)}`); - if (commit_description) { - console.log(`${picocolors.bgGreen(picocolors.bold(" Description "))} ${picocolors.green(commit_description)}`); - } - console.log(`${picocolors.bgGreen(picocolors.bold(" Commit hash "))} ${picocolors.bold(picocolors.cyan(` ${branchHash} `))} (${picocolors.italic(picocolors.green(branchName))})`); + const commitTitle = buildCommitTitle(response.commit_type, response.commit_message); + const finalCommitMessage = buildFinalCommitMessage(commitTitle, response, defaultProjectValue); + const gitCommands = buildGitCommands(finalCommitMessage, response.commit_description); + const commitResult = await execa("git", gitCommands); + const { branch, hash } = parseCommitResult(commitResult.stdout); + showGitOutput(commitResult.stdout, commitResult.stderr); + showCommitResult({ + title: finalCommitMessage, + description: response.commit_description, + hash, + branch + }); + return true; } catch (error) { - console.log(picocolors.red(error.stderr)); - if (error.exitCode === 1) - console.log(picocolors.bgRed(" No changes added to commit. ")); - else - console.error(error); + showGitError(error); + return false; } }; diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..f271f2c --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,15 @@ +pre-commit: + parallel: true + commands: + tests: + run: pnpm test:run + fail_text: "โŒ Tests failed! Please fix the issues before committing." + stage_fixed: false + + type-check: + run: pnpm build + fail_text: "โŒ TypeScript compilation failed! Please fix type errors before committing." + stage_fixed: false + +# Configuration +assert_lefthook_installed: true diff --git a/package.json b/package.json index d991559..369f049 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "owlting_cz", - "version": "3.1.0", + "version": "3.2.0", "description": "create-commit for OwlTing", "type": "module", "dependencies": { @@ -14,7 +14,15 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "scripts": { - "build": "unbuild" + "build": "unbuild", + "test": "vitest", + "test:run": "vitest run", + "test:watch": "vitest --watch", + "test:coverage": "vitest run --coverage", + "test:ui": "vitest --ui", + "coverage:badge": "pnpm test:coverage && node scripts/generate-coverage-badge.js", + "hooks:install": "lefthook install", + "hooks:uninstall": "lefthook uninstall" }, "bin": { "cz": "bin/index.mjs" @@ -27,9 +35,15 @@ "commit" ], "author": "Nick", + "engines": { + "node": ">=14.0.0" + }, "devDependencies": { "@types/node": "^22.1.0", "@types/prompts": "^2.4.9", - "@types/yargs": "^17.0.33" + "@types/yargs": "^17.0.33", + "@vitest/coverage-v8": "^2.0.0", + "lefthook": "^1.11.13", + "vitest": "^2.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7e2e8a..0f79d16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,15 @@ importers: '@types/yargs': specifier: ^17.0.33 version: 17.0.33 + '@vitest/coverage-v8': + specifier: ^2.0.0 + version: 2.1.9(vitest@2.1.9(@types/node@22.1.0)) + lefthook: + specifier: ^1.11.13 + version: 1.11.13 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@22.1.0) packages: @@ -78,10 +87,18 @@ packages: resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.24.7': resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.24.8': resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} engines: {node: '>=6.9.0'} @@ -99,6 +116,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.27.5': + resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/standalone@7.25.3': resolution: {integrity: sha512-uR+EoBqIIIvKGCG7fOj7HKupu3zVObiMfdEwoPZfVCPpcWJaZ1PkshaP5/6cl6BKAm1Zcv25O1rf+uoQ7V8nqA==} engines: {node: '>=6.9.0'} @@ -115,12 +137,25 @@ packages: resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.27.6': + resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.23.0': resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} engines: {node: '>=18'} @@ -133,6 +168,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.23.0': resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} engines: {node: '>=18'} @@ -145,6 +186,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.23.0': resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} engines: {node: '>=18'} @@ -157,6 +204,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.23.0': resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} engines: {node: '>=18'} @@ -169,6 +222,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.23.0': resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} engines: {node: '>=18'} @@ -181,6 +240,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.23.0': resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} engines: {node: '>=18'} @@ -193,6 +258,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.23.0': resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} engines: {node: '>=18'} @@ -205,6 +276,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.23.0': resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} engines: {node: '>=18'} @@ -217,6 +294,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.23.0': resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} engines: {node: '>=18'} @@ -229,6 +312,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.23.0': resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} engines: {node: '>=18'} @@ -241,6 +330,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.23.0': resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} engines: {node: '>=18'} @@ -253,6 +348,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.23.0': resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} engines: {node: '>=18'} @@ -265,6 +366,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.23.0': resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} engines: {node: '>=18'} @@ -277,6 +384,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.23.0': resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} engines: {node: '>=18'} @@ -289,6 +402,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.23.0': resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} engines: {node: '>=18'} @@ -301,6 +420,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.23.0': resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} engines: {node: '>=18'} @@ -313,6 +438,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.23.0': resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} engines: {node: '>=18'} @@ -325,6 +456,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.23.0': resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} engines: {node: '>=18'} @@ -343,6 +480,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.23.0': resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} engines: {node: '>=18'} @@ -355,6 +498,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.23.0': resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} engines: {node: '>=18'} @@ -367,6 +516,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.23.0': resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} engines: {node: '>=18'} @@ -379,6 +534,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.23.0': resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} engines: {node: '>=18'} @@ -391,12 +552,26 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.23.0': resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -427,6 +602,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@rollup/plugin-alias@5.1.0': resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==} engines: {node: '>=14.0.0'} @@ -481,6 +660,106 @@ packages: rollup: optional: true + '@rollup/rollup-android-arm-eabi@4.42.0': + resolution: {integrity: sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.42.0': + resolution: {integrity: sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.42.0': + resolution: {integrity: sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.42.0': + resolution: {integrity: sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.42.0': + resolution: {integrity: sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.42.0': + resolution: {integrity: sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.42.0': + resolution: {integrity: sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.42.0': + resolution: {integrity: sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.42.0': + resolution: {integrity: sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.42.0': + resolution: {integrity: sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.42.0': + resolution: {integrity: sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.42.0': + resolution: {integrity: sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.42.0': + resolution: {integrity: sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.42.0': + resolution: {integrity: sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.42.0': + resolution: {integrity: sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.42.0': + resolution: {integrity: sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.42.0': + resolution: {integrity: sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.42.0': + resolution: {integrity: sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.42.0': + resolution: {integrity: sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.42.0': + resolution: {integrity: sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==} + cpu: [x64] + os: [win32] + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -488,6 +767,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/node@22.1.0': resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==} @@ -503,6 +785,44 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + peerDependencies: + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + acorn@8.12.1: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} @@ -512,6 +832,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -520,6 +844,14 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -549,12 +881,20 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} caniuse-lite@1.0.30001651: resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -563,6 +903,10 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} @@ -607,6 +951,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css-declaration-sorter@7.2.0: resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} engines: {node: ^14 || ^16 || >=18} @@ -664,6 +1012,19 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -688,21 +1049,35 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.5: resolution: {integrity: sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + esbuild@0.19.12: resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} engines: {node: '>=12'} hasBin: true + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.23.0: resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} engines: {node: '>=18'} @@ -723,10 +1098,17 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + expect-type@1.2.1: + resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + engines: {node: '>=12.0.0'} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -738,6 +1120,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -768,6 +1154,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} @@ -785,6 +1175,10 @@ packages: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -792,6 +1186,9 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -844,6 +1241,25 @@ packages: isexe@2.0.0: resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true @@ -865,6 +1281,60 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + lefthook-darwin-arm64@1.11.13: + resolution: {integrity: sha512-gHwHofXupCtzNLN+8esdWfFTnAEkmBxE/WKA0EwxPPJXdZYa1GUsiG5ipq/CdG/0j8ekYyM9Hzyrrk5BqJ42xw==} + cpu: [arm64] + os: [darwin] + + lefthook-darwin-x64@1.11.13: + resolution: {integrity: sha512-zYxkWNUirmTidhskY9J9AwxvdMi3YKH+TqZ3AQ1EOqkOwPBWJQW5PbnzsXDrd3YnrtZScYm/tE/moXJpEPPIpQ==} + cpu: [x64] + os: [darwin] + + lefthook-freebsd-arm64@1.11.13: + resolution: {integrity: sha512-gJzWnllcMcivusmPorEkXPpEciKotlBHn7QxWwYaSjss/U3YdZu+NTjDO30b3qeiVlyq4RAZ4BPKJODXxHHwUA==} + cpu: [arm64] + os: [freebsd] + + lefthook-freebsd-x64@1.11.13: + resolution: {integrity: sha512-689XdchgtDvZQWFFx1szUvm/mqrq/v6laki0odq5FAfcSgUeLu3w+z6UicBS5l55eFJuQTDNKARFqrKJ04e+Vw==} + cpu: [x64] + os: [freebsd] + + lefthook-linux-arm64@1.11.13: + resolution: {integrity: sha512-ujCLbaZg5S/Ao8KZAcNSb+Y3gl898ZEM0YKyiZmZo22dFFpm/5gcV46pF3xaqIw5IpH+3YYDTKDU+qTetmARyQ==} + cpu: [arm64] + os: [linux] + + lefthook-linux-x64@1.11.13: + resolution: {integrity: sha512-O5WdodeBtFOXQlvPcckqp4W/yqVM9DbVQBkvOxwSJlmsxO4sGYK1TqdxH9ihLB85B2kPPssZj9ze36/oizzhVQ==} + cpu: [x64] + os: [linux] + + lefthook-openbsd-arm64@1.11.13: + resolution: {integrity: sha512-SyBpciUfvY/lUDbZu7L6MtL/SVG2+yMTckBgb4PdJQhJlisY0IsyOYdlTw2icPPrY7JnwdsFv8UW0EJOB76W4g==} + cpu: [arm64] + os: [openbsd] + + lefthook-openbsd-x64@1.11.13: + resolution: {integrity: sha512-6+/0j6O2dzo9cjTWUKfL2J6hRR7Krna/ssqnW8cWh8QHZKO9WJn34epto9qgjeHwSysou8byI7Mwv5zOGthLCQ==} + cpu: [x64] + os: [openbsd] + + lefthook-windows-arm64@1.11.13: + resolution: {integrity: sha512-w5TwZ8bsZ17uOMtYGc5oEb4tCHjNTSeSXRy6H9Yic8E7IsPZtZLkaZGnIIwgXFuhhrcCdc6FuTvKt2tyV7EW2g==} + cpu: [arm64] + os: [win32] + + lefthook-windows-x64@1.11.13: + resolution: {integrity: sha512-7lvwnIs8CNOXKU4y3i1Pbqna+QegIORkSD2VCuHBNpIJ8H84NpjoG3tKU91IM/aI1a2eUvCk+dw+1rfMRz7Ytg==} + cpu: [x64] + os: [win32] + + lefthook@1.11.13: + resolution: {integrity: sha512-SDTk3D4nW1XRpR9u9fdYQ/qj1xeZVIwZmIFdJUnyq+w9ZLdCCvIrOmtD8SFiJowSevISjQDC+f9nqyFXUxc0SQ==} + hasBin: true + lilconfig@3.1.2: resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} @@ -875,12 +1345,28 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} @@ -906,6 +1392,14 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + mkdist@1.5.4: resolution: {integrity: sha512-GEmKYJG5K1YGFIq3t0K3iihZ8FTgXphLf/4UjbmpXIAtBFn4lEjXk3pXNTSfy7EtcEXhp2Nn1vzw5pIus6RY3g==} hasBin: true @@ -931,6 +1425,14 @@ packages: ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -957,6 +1459,9 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -964,6 +1469,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -971,12 +1480,19 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -1163,6 +1679,10 @@ packages: resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.4: + resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} + engines: {node: ^10 || ^12 || >=14} + pretty-bytes@6.1.1: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -1198,6 +1718,11 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true + rollup@4.42.0: + resolution: {integrity: sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1221,9 +1746,16 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.5: resolution: {integrity: sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -1235,14 +1767,32 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -1257,6 +1807,10 @@ packages: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -1266,6 +1820,28 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.0: + resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -1307,15 +1883,85 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -1409,8 +2055,12 @@ snapshots: '@babel/helper-string-parser@7.24.8': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-option@7.24.8': {} '@babel/helpers@7.25.0': @@ -1429,6 +2079,10 @@ snapshots: dependencies: '@babel/types': 7.25.2 + '@babel/parser@7.27.5': + dependencies: + '@babel/types': 7.27.6 + '@babel/standalone@7.25.3': {} '@babel/template@7.25.0': @@ -1455,111 +2109,172 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@babel/types@7.27.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + '@esbuild/aix-ppc64@0.19.12': optional: true + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.23.0': optional: true '@esbuild/android-arm64@0.19.12': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.23.0': optional: true '@esbuild/android-arm@0.19.12': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.23.0': optional: true '@esbuild/android-x64@0.19.12': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.23.0': optional: true '@esbuild/darwin-arm64@0.19.12': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.23.0': optional: true '@esbuild/darwin-x64@0.19.12': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.23.0': optional: true '@esbuild/freebsd-arm64@0.19.12': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.23.0': optional: true '@esbuild/freebsd-x64@0.19.12': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.23.0': optional: true '@esbuild/linux-arm64@0.19.12': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.23.0': optional: true '@esbuild/linux-arm@0.19.12': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.23.0': optional: true '@esbuild/linux-ia32@0.19.12': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.23.0': optional: true '@esbuild/linux-loong64@0.19.12': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.23.0': optional: true '@esbuild/linux-mips64el@0.19.12': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.23.0': optional: true '@esbuild/linux-ppc64@0.19.12': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.23.0': optional: true '@esbuild/linux-riscv64@0.19.12': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.23.0': optional: true '@esbuild/linux-s390x@0.19.12': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.23.0': optional: true '@esbuild/linux-x64@0.19.12': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.23.0': optional: true '@esbuild/netbsd-x64@0.19.12': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.23.0': optional: true @@ -1569,33 +2284,59 @@ snapshots: '@esbuild/openbsd-x64@0.19.12': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.23.0': optional: true '@esbuild/sunos-x64@0.19.12': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.23.0': optional: true '@esbuild/win32-arm64@0.19.12': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.23.0': optional: true '@esbuild/win32-ia32@0.19.12': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.23.0': optional: true '@esbuild/win32-x64@0.19.12': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.23.0': optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -1625,6 +2366,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@pkgjs/parseargs@0.11.0': + optional: true + '@rollup/plugin-alias@5.1.0(rollup@3.29.4)': dependencies: slash: 4.0.0 @@ -1674,10 +2418,72 @@ snapshots: optionalDependencies: rollup: 3.29.4 + '@rollup/rollup-android-arm-eabi@4.42.0': + optional: true + + '@rollup/rollup-android-arm64@4.42.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.42.0': + optional: true + + '@rollup/rollup-darwin-x64@4.42.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.42.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.42.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.42.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.42.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.42.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.42.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.42.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.42.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.42.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.42.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.42.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.42.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.42.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.42.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.42.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.42.0': + optional: true + '@trysound/sax@0.2.0': {} '@types/estree@1.0.5': {} + '@types/estree@1.0.7': {} + '@types/node@22.1.0': dependencies: undici-types: 6.13.0 @@ -1695,10 +2501,70 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.1.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.17 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@22.1.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.1.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@22.1.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.17 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.3 + tinyrainbow: 1.2.0 + acorn@8.12.1: {} ansi-regex@5.0.1: {} + ansi-regex@6.1.0: {} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 @@ -1707,6 +2573,10 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.1: {} + + assertion-error@2.0.1: {} + autoprefixer@10.4.20(postcss@8.4.41): dependencies: browserslist: 4.23.3 @@ -1738,6 +2608,8 @@ snapshots: builtin-modules@3.3.0: {} + cac@6.7.14: {} + caniuse-api@3.0.0: dependencies: browserslist: 4.23.3 @@ -1747,6 +2619,14 @@ snapshots: caniuse-lite@1.0.30001651: {} + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -1755,6 +2635,8 @@ snapshots: chalk@5.3.0: {} + check-error@2.1.1: {} + citty@0.1.6: dependencies: consola: 3.2.3 @@ -1795,6 +2677,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css-declaration-sorter@7.2.0(postcss@8.4.41): dependencies: postcss: 8.4.41 @@ -1873,6 +2761,12 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + deepmerge@4.3.1: {} defu@6.1.4: {} @@ -1899,12 +2793,18 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.5: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + entities@4.5.0: {} + es-module-lexer@1.7.0: {} + esbuild@0.19.12: optionalDependencies: '@esbuild/aix-ppc64': 0.19.12 @@ -1931,6 +2831,32 @@ snapshots: '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.23.0: optionalDependencies: '@esbuild/aix-ppc64': 0.23.0 @@ -1966,6 +2892,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 + execa@5.1.1: dependencies: cross-spawn: 7.0.3 @@ -1978,6 +2908,8 @@ snapshots: signal-exit: 3.0.5 strip-final-newline: 2.0.0 + expect-type@1.2.1: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1994,6 +2926,11 @@ snapshots: dependencies: to-regex-range: 5.0.1 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fraction.js@4.3.7: {} fs.realpath@1.0.0: {} @@ -2013,6 +2950,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@8.1.0: dependencies: fs.realpath: 1.0.0 @@ -2033,12 +2979,16 @@ snapshots: has-flag@3.0.0: {} + has-flag@4.0.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 hookable@5.5.3: {} + html-escaper@2.0.2: {} + human-signals@2.1.0: {} ignore@5.3.1: {} @@ -2078,6 +3028,33 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@1.21.6: {} js-tokens@4.0.0: {} @@ -2088,12 +3065,59 @@ snapshots: kleur@3.0.3: {} + lefthook-darwin-arm64@1.11.13: + optional: true + + lefthook-darwin-x64@1.11.13: + optional: true + + lefthook-freebsd-arm64@1.11.13: + optional: true + + lefthook-freebsd-x64@1.11.13: + optional: true + + lefthook-linux-arm64@1.11.13: + optional: true + + lefthook-linux-x64@1.11.13: + optional: true + + lefthook-openbsd-arm64@1.11.13: + optional: true + + lefthook-openbsd-x64@1.11.13: + optional: true + + lefthook-windows-arm64@1.11.13: + optional: true + + lefthook-windows-x64@1.11.13: + optional: true + + lefthook@1.11.13: + optionalDependencies: + lefthook-darwin-arm64: 1.11.13 + lefthook-darwin-x64: 1.11.13 + lefthook-freebsd-arm64: 1.11.13 + lefthook-freebsd-x64: 1.11.13 + lefthook-linux-arm64: 1.11.13 + lefthook-linux-x64: 1.11.13 + lefthook-openbsd-arm64: 1.11.13 + lefthook-openbsd-x64: 1.11.13 + lefthook-windows-arm64: 1.11.13 + lefthook-windows-x64: 1.11.13 + lilconfig@3.1.2: {} lodash.memoize@4.1.2: {} lodash.uniq@4.5.0: {} + loupe@3.1.3: {} + + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2102,6 +3126,20 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 + source-map-js: 1.2.0 + + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + mdn-data@2.0.28: {} mdn-data@2.0.30: {} @@ -2121,6 +3159,12 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + mkdist@1.5.4(typescript@5.5.4): dependencies: autoprefixer: 10.4.20(postcss@8.4.41) @@ -2150,6 +3194,10 @@ snapshots: ms@2.1.2: {} + ms@2.1.3: {} + + nanoid@3.3.11: {} + nanoid@3.3.7: {} node-releases@2.0.18: {} @@ -2172,18 +3220,29 @@ snapshots: dependencies: mimic-fn: 2.1.0 + package-json-from-dist@1.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + path-type@4.0.0: {} pathe@1.1.2: {} + pathval@2.0.0: {} + picocolors@1.0.0: {} picocolors@1.0.1: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} pkg-types@1.1.3: @@ -2359,6 +3418,12 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + postcss@8.5.4: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + pretty-bytes@6.1.1: {} prompts@2.4.2: @@ -2390,6 +3455,32 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + rollup@4.42.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.42.0 + '@rollup/rollup-android-arm64': 4.42.0 + '@rollup/rollup-darwin-arm64': 4.42.0 + '@rollup/rollup-darwin-x64': 4.42.0 + '@rollup/rollup-freebsd-arm64': 4.42.0 + '@rollup/rollup-freebsd-x64': 4.42.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.42.0 + '@rollup/rollup-linux-arm-musleabihf': 4.42.0 + '@rollup/rollup-linux-arm64-gnu': 4.42.0 + '@rollup/rollup-linux-arm64-musl': 4.42.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.42.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.42.0 + '@rollup/rollup-linux-riscv64-gnu': 4.42.0 + '@rollup/rollup-linux-riscv64-musl': 4.42.0 + '@rollup/rollup-linux-s390x-gnu': 4.42.0 + '@rollup/rollup-linux-x64-gnu': 4.42.0 + '@rollup/rollup-linux-x64-musl': 4.42.0 + '@rollup/rollup-win32-arm64-msvc': 4.42.0 + '@rollup/rollup-win32-ia32-msvc': 4.42.0 + '@rollup/rollup-win32-x64-msvc': 4.42.0 + fsevents: 2.3.3 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -2406,24 +3497,44 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@3.0.5: {} + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} slash@4.0.0: {} source-map-js@1.2.0: {} + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + strip-final-newline@2.0.0: {} stylehacks@7.0.2(postcss@8.4.41): @@ -2436,6 +3547,10 @@ snapshots: dependencies: has-flag: 3.0.0 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} svgo@3.3.2: @@ -2448,6 +3563,22 @@ snapshots: csso: 5.0.5 picocolors: 1.0.0 + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.0: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -2513,16 +3644,89 @@ snapshots: util-deprecate@1.0.2: {} + vite-node@2.1.9(@types/node@22.1.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@22.1.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.19(@types/node@22.1.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.4 + rollup: 4.42.0 + optionalDependencies: + '@types/node': 22.1.0 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.1.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@22.1.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.1 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.0 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@22.1.0) + vite-node: 2.1.9(@types/node@22.1.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.1.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} y18n@5.0.8: {} diff --git a/scripts/generate-coverage-badge.js b/scripts/generate-coverage-badge.js new file mode 100644 index 0000000..c2c5e9a --- /dev/null +++ b/scripts/generate-coverage-badge.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +try { + // ่ฎ€ๅ– coverage summary + const coverageFile = join(process.cwd(), 'coverage', 'coverage-summary.json'); + const coverageData = JSON.parse(readFileSync(coverageFile, 'utf8')); + + // ๅ–ๅพ—็ธฝ้ซ”่ฆ†่“‹็އ + const totalCoverage = coverageData.total; + const statements = totalCoverage.statements.pct; + const branches = totalCoverage.branches.pct; + const functions = totalCoverage.functions.pct; + const lines = totalCoverage.lines.pct; + + // ่จˆ็ฎ—ๅนณๅ‡่ฆ†่“‹็އ + const avgCoverage = Math.round((statements + branches + functions + lines) / 4); + + // ๆฑบๅฎš badge ้ก่‰ฒ + let color = 'red'; + if (avgCoverage >= 80) color = 'brightgreen'; + else if (avgCoverage >= 60) color = 'yellow'; + else if (avgCoverage >= 40) color = 'orange'; + + // ็”Ÿๆˆ badge URL + const badgeUrl = `https://img.shields.io/badge/coverage-${avgCoverage}%25-${color}`; + + // ็”Ÿๆˆ coverage ๅ ฑๅ‘Š + const coverageReport = { + timestamp: new Date().toISOString(), + coverage: { + statements: `${statements}%`, + branches: `${branches}%`, + functions: `${functions}%`, + lines: `${lines}%`, + average: `${avgCoverage}%` + }, + badge: { + url: badgeUrl, + markdown: `![Coverage](${badgeUrl})`, + html: `Coverage Badge` + }, + details: { + statements: totalCoverage.statements, + branches: totalCoverage.branches, + functions: totalCoverage.functions, + lines: totalCoverage.lines + } + }; + + // ่ผธๅ‡บๅˆฐๆช”ๆกˆ + writeFileSync('coverage-badge.json', JSON.stringify(coverageReport, null, 2)); + + // ่ผธๅ‡บๅˆฐ console + console.log('๐Ÿ“Š Coverage Report Generated:'); + console.log(`๐Ÿ“ˆ Statements: ${statements}%`); + console.log(`๐ŸŒฟ Branches: ${branches}%`); + console.log(`โšก Functions: ${functions}%`); + console.log(`๐Ÿ“ Lines: ${lines}%`); + console.log(`๐ŸŽฏ Average: ${avgCoverage}%`); + console.log(`\n๐Ÿท๏ธ Badge Markdown: ${coverageReport.badge.markdown}`); + console.log(`๐Ÿ“„ Coverage badge saved to: coverage-badge.json`); +} catch (error) { + console.error('โŒ Error generating coverage badge:', error.message); + process.exit(1); +} diff --git a/src/commands/index.ts b/src/commands/index.ts index fe8d12a..d09c37c 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,6 +1,13 @@ import prompts from 'prompts' import execa from 'execa' import { projects, commitTypes } from '../helper' +import type { + ProjectConfig, + CommitResponse, + CommitResult, + ProjectType, + ParsedCommitResult +} from '../types' import picocolors from 'picocolors' import fs from 'fs' import { dirname, resolve } from 'node:path' @@ -9,115 +16,180 @@ import { fileURLToPath } from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) const rootPath = resolve(__dirname, '../') -let defaultProjectValue = '' +// Pure functions for business logic +export const loadProjectConfig = (filePath: string): ProjectConfig | null => { + try { + const config = fs.readFileSync(filePath, 'utf8') + return JSON.parse(config) + } catch (e) { + return null + } +} -try { - const filePath = resolve(rootPath, 'keep/cz_config.json') - const config = fs.readFileSync(filePath) - defaultProjectValue = JSON.parse(config as any).defaultProject -} catch (e) { - console.log(picocolors.yellow(picocolors.italic(' ๐Ÿ’ก You can try `cz -i` to choose a default project prefix. '))) - defaultProjectValue = '' -} - -const typesList = commitTypes.map(type => ({ - title: type.name, - description: `${type.emoji} ${type.description}`, - value: type.value, - emoji: type.emoji -})) - -const step_type = { - type: 'autocomplete', - name: 'commit_type', - message: 'Pick a commit type.', - choices: typesList, - fallback: 'No matched type.' -} - -const step_message = { - type: 'text', - name: 'commit_message', - message: (prev: string) => { - const target = typesList.find(type => type.value === prev)! - return `${target.emoji} ${target.title}` - }, - validate: (value: string) => { - if (!value) { - return 'Commit message is required.' - } - return true +export const getDefaultProjectValue = (config: ProjectConfig | null): string => { + return config?.defaultProject || '' +} + +export const buildCommitTypesList = () => { + return commitTypes.map(type => ({ + title: type.name, + description: `${type.emoji} ${type.description}`, + value: type.value, + emoji: type.emoji + })) +} + +export const buildProjectsList = () => { + return projects.map(project => ({ + title: project.name, + description: `[${project.prefix}-13845] title`, + value: project.value + })) +} + +export const findCommitType = (commitTypeValue: string) => { + return commitTypes.find(type => type.value === commitTypeValue) +} + +export const findProject = (projectValue: string): ProjectType | undefined => { + return projects.find(project => project.value === projectValue) +} + +export const buildCommitTitle = (commitType: string, message: string): string => { + const type = findCommitType(commitType) + if (!type) { + throw new Error(`Invalid commit type: ${commitType}`) } + return `${type.emoji} ${commitType}: ${message}` } -const step_description = { - type: 'text', - name: 'commit_description', - message: 'Commit description (optional)', - initial: '', - validate: (value: string) => { - if (value.length > 100) { - return 'Description is too long.' - } - return true +export const buildFinalCommitMessage = ( + commitTitle: string, + response: CommitResponse, + defaultProjectValue: string +): string => { + const { is_jira, is_default_project, project_type, jira_id } = response + + if (!is_jira) { + return commitTitle + } + + const typeResponse = is_default_project ? defaultProjectValue : project_type + const projectType = findProject(typeResponse!) + + if (!projectType) { + throw new Error(`Invalid project type: ${typeResponse}`) + } + + if (!jira_id) { + throw new Error('Jira ID is required when using Jira integration') } + + return `[${projectType.prefix}-${jira_id}] ${commitTitle}` } -const step_is_jira = { - type: 'confirm', - name: 'is_jira', - message: 'Tag Jira issue ?', - initial: false -} - -const projectsList = projects.map(project => ({ - title: project.name, - description: `[${project.prefix}-13845] title`, - value: project.value -})) - -const defaultProject = projectsList.find(project => project.value === defaultProjectValue)! - -const step_is_default_project = { - type: (prev: boolean) => prev ? 'confirm' : null, - name: 'is_default_project', - message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`, - initial: true -} - -const step_project_type = { - type: (prev: string, { is_jira }: { is_jira: boolean }) => { - return is_jira - ? defaultProject?.value && prev - ? null - : 'autocomplete' - : null - }, - name: 'project_type', - message: 'Pick a project type.', - choices: projectsList, - initial: 'owlpay', - fallback: 'No matched project.' -} - -const step_jira_id = { - type: (prev: boolean) => prev ? 'number' : null, - name: 'jira_id', - message: 'Jira issue id', - onRender () { - (this as any).msg = picocolors.bgCyan(picocolors.white(' Jira issue ID ')) // TODO: fix type - }, - validate: (value: number) => { - if (!value) { - return 'Jira issue ID is required.' - } - return true +export const buildGitCommands = (commitMessage: string, description?: string): string[] => { + if (description) { + return ['commit', '-m', commitMessage, '-m', description] } + return ['commit', '-m', commitMessage] } -export default async () => { - let isCanceled = false - const order = [ +export const parseCommitResult = (stdout: string): ParsedCommitResult => { + const branchHashName = stdout.match(/\[(.*?)\]/)?.pop() + if (!branchHashName) { + throw new Error('Could not parse commit result') + } + + const [branchName, branchHash] = branchHashName.split(' ') + return { branch: branchName, hash: branchHash } +} + +// Step builders for better organization +export const buildPromptSteps = (defaultProjectValue: string, projectsList: any[], typesList: any[]) => { + const defaultProject = projectsList.find(project => project.value === defaultProjectValue) + + const step_type = { + type: 'autocomplete', + name: 'commit_type', + message: 'Pick a commit type.', + choices: typesList, + fallback: 'No matched type.' + } + + const step_message = { + type: 'text', + name: 'commit_message', + message: (prev: string) => { + const target = typesList.find(type => type.value === prev)! + return `${target.emoji} ${target.title}` + }, + validate: (value: string) => { + if (!value) { + return 'Commit message is required.' + } + return true + } + } + + const step_description = { + type: 'text', + name: 'commit_description', + message: 'Commit description (optional)', + initial: '', + validate: (value: string) => { + if (value.length > 100) { + return 'Description is too long.' + } + return true + } + } + + const step_is_jira = { + type: 'confirm', + name: 'is_jira', + message: 'Tag Jira issue ?', + initial: false + } + + const step_is_default_project = { + type: (prev: boolean) => prev ? 'confirm' : null, + name: 'is_default_project', + message: `use '${defaultProject?.title}' pattern? e.g. ${defaultProject?.description}`, + initial: true + } + + const step_project_type = { + type: (prev: string, { is_jira }: { is_jira: boolean }) => { + return is_jira + ? defaultProject?.value && prev + ? null + : 'autocomplete' + : null + }, + name: 'project_type', + message: 'Pick a project type.', + choices: projectsList, + initial: 'owlpay', + fallback: 'No matched project.' + } + + const step_jira_id = { + type: (prev: boolean) => prev ? 'number' : null, + name: 'jira_id', + message: 'Jira issue id', + onRender () { + (this as any).msg = picocolors.bgCyan(picocolors.white(' Jira issue ID ')) + }, + validate: (value: number) => { + if (!value) { + return 'Jira issue ID is required.' + } + return true + } + } + + return [ step_type, step_message, step_description, @@ -125,8 +197,67 @@ export default async () => { defaultProject?.value ? step_is_default_project : null, step_project_type, step_jira_id - ].filter(Boolean) as any // TODO: fix type - const response = await prompts(order, { + ].filter(Boolean) +} + +// Side effect functions +export const showConfigMissingWarning = () => { + console.log(picocolors.yellow(picocolors.italic(' ๐Ÿ’ก You can try `cz -i` to choose a default project prefix. '))) +} + +export const showCancelMessage = () => { + console.log(picocolors.magenta(' commit abort. ')) +} + +export const showCommitResult = (result: CommitResult) => { + console.log('-----------------------------------------------------------') + console.log(`${picocolors.bgGreen(picocolors.bold(' Title '))} ${picocolors.green(result.title)}`) + if (result.description) { + console.log(`${picocolors.bgGreen(picocolors.bold(' Description '))} ${picocolors.green(result.description)}`) + } + if (result.hash && result.branch) { + console.log(`${picocolors.bgGreen(picocolors.bold(' Commit hash '))} ${picocolors.bold(picocolors.cyan(` ${result.hash} `))} (${picocolors.italic(picocolors.green(result.branch))})`) + } +} + +export const showGitOutput = (stdout: string, stderr?: string) => { + console.log('-----------------------------------------------------------') + console.log(picocolors.dim(stdout)) + if (stderr && stderr !== '') { + console.log('-----------------------------------------------------------') + console.log(picocolors.dim(stderr)) + } + console.log('-----------------------------------------------------------') +} + +export const showGitError = (error: any) => { + console.log(picocolors.red(error.stderr)) + if (error.exitCode === 1) { + console.log(picocolors.bgRed(' No changes added to commit. ')) + } else { + console.error(error) + } +} + +// Main CLI function - orchestrates the flow +export default async () => { + // Load configuration + const configPath = resolve(rootPath, 'keep/cz_config.json') + const config = loadProjectConfig(configPath) + const defaultProjectValue = getDefaultProjectValue(config) + + if (!config) { + showConfigMissingWarning() + } + + // Build choices + const typesList = buildCommitTypesList() + const projectsList = buildProjectsList() + const steps = buildPromptSteps(defaultProjectValue, projectsList, typesList) + + // Handle user input + let isCanceled = false + const response = await prompts(steps as any, { onSubmit: (prompt, answers) => { if (answers === undefined) { isCanceled = true @@ -137,42 +268,35 @@ export default async () => { isCanceled = true return false } - }) + }) as CommitResponse if (isCanceled) { - console.log(picocolors.magenta(' commit abort. ')) + showCancelMessage() return false } - const { commit_type, commit_message, commit_description, is_jira, is_default_project, project_type, jira_id } = response - const type = typesList.find(type => type.value === commit_type)! - const commitTitle = `${type.emoji} ${commit_type}: ${commit_message}` - const typeResponse = is_default_project ? defaultProject?.value : project_type - const projectType = projects.find(project => project.value === typeResponse)! - const result = is_jira - ? `[${projectType.prefix}-${jira_id}] ${commitTitle}` - : commitTitle - try { - const commands = commit_description ? ['commit', '-m', result, '-m', commit_description] : ['commit', '-m', result] - const commitResult = await execa('git', commands) - const branchHashName = commitResult.stdout.match(/\[(.*?)\]/)!.pop()! - const [branchName, branchHash] = branchHashName.split(' ') - console.log('-----------------------------------------------------------') - console.log(picocolors.dim(commitResult.stdout)) - if (commitResult.stderr !== '') { - console.log('-----------------------------------------------------------') - console.log(picocolors.dim(commitResult.stderr)) - } - console.log('-----------------------------------------------------------') - console.log(`${picocolors.bgGreen(picocolors.bold(' Title '))} ${picocolors.green(result)}`) - if (commit_description) { - console.log(`${picocolors.bgGreen(picocolors.bold(' Description '))} ${picocolors.green(commit_description)}`) - } - console.log(`${picocolors.bgGreen(picocolors.bold(' Commit hash '))} ${picocolors.bold(picocolors.cyan(` ${branchHash} `))} (${picocolors.italic(picocolors.green(branchName))})`) + // Build commit message + const commitTitle = buildCommitTitle(response.commit_type, response.commit_message) + const finalCommitMessage = buildFinalCommitMessage(commitTitle, response, defaultProjectValue) + const gitCommands = buildGitCommands(finalCommitMessage, response.commit_description) + + // Execute git commit + const commitResult = await execa('git', gitCommands) + const { branch, hash } = parseCommitResult(commitResult.stdout) + + // Show results + showGitOutput(commitResult.stdout, commitResult.stderr) + showCommitResult({ + title: finalCommitMessage, + description: response.commit_description, + hash, + branch + }) + + return true } catch (error: any) { - console.log(picocolors.red(error.stderr)) - if (error.exitCode === 1) console.log(picocolors.bgRed(' No changes added to commit. ')) - else console.error(error) + showGitError(error) + return false } } diff --git a/src/commands/init.ts b/src/commands/init.ts index 3664524..7f065c8 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -48,7 +48,7 @@ export default async () => { const filePath = resolve(rootPath, 'keep/cz_config.json') fs.writeFileSync( filePath, - `${JSON.stringify({ defaultProject: set_default_project } || {}, null, 2)}`, + `${JSON.stringify({ defaultProject: set_default_project }, null, 2)}` ) console.log(picocolors.green(` default project set: ${set_default_project} `)) } catch (error) { diff --git a/src/helper/commit-types.ts b/src/helper/commit-types.ts index 0baaef9..f304b1f 100644 --- a/src/helper/commit-types.ts +++ b/src/helper/commit-types.ts @@ -1,4 +1,8 @@ -export default [ +import type { CommitType } from '../types' + +export type { CommitType } + +export default [ { name: 'chore', emoji: '๐Ÿงน', @@ -69,7 +73,7 @@ export default [ name: 'storybook', emoji: '๐Ÿ“š', description: 'New storybook', - value: 'story' + value: 'storybook' }, { name: 'revert', diff --git a/src/helper/projects.ts b/src/helper/projects.ts index 38bc7ed..30ff63b 100644 --- a/src/helper/projects.ts +++ b/src/helper/projects.ts @@ -1,10 +1,8 @@ -interface Project { - name: string - prefix: string - value: string -} +import type { ProjectType } from '../types' -export default [ +export type { ProjectType } + +export default [ { name: 'OwlPay', prefix: 'OWLPAY', diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..f7d8ff8 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,74 @@ +// Configuration related types +export interface ProjectConfig { + defaultProject?: string +} + +// CLI response types +export interface CommitResponse { + commit_type: string + commit_message: string + commit_description?: string + is_jira: boolean + is_default_project?: boolean + project_type?: string + jira_id?: number +} + +// Result display types +export interface CommitResult { + title: string + description?: string + hash?: string + branch?: string +} + +// Project structure types +export interface ProjectType { + name: string + prefix: string + value: string +} + +// Commit type structure +export interface CommitType { + name: string + emoji: string + description: string + value: string +} + +// Git command result types +export interface GitCommitResult { + stdout: string + stderr?: string +} + +export interface ParsedCommitResult { + branch: string + hash: string +} + +// Error types +export interface GitError extends Error { + stderr?: string + exitCode?: number +} + +// Prompt step types +export interface PromptChoice { + title: string + description: string + value: string + emoji?: string +} + +export interface PromptStep { + type: string + name: string + message: string | ((prev: any) => string) + choices?: PromptChoice[] + initial?: any + validate?: (value: any) => boolean | string + fallback?: string + onRender?: () => void +} \ No newline at end of file diff --git a/tests/commands/cli-module-loading.test.ts b/tests/commands/cli-module-loading.test.ts new file mode 100644 index 0000000..e6c4749 --- /dev/null +++ b/tests/commands/cli-module-loading.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fs from 'fs' + +// Mock dependencies before importing the module +vi.mock('fs') +vi.mock('prompts') +vi.mock('execa') +vi.mock('picocolors', () => ({ + default: { + yellow: (text: string) => text, + italic: (text: string) => text, + magenta: (text: string) => text, + bgGreen: (text: string) => text, + bold: (text: string) => text, + green: (text: string) => text, + cyan: (text: string) => text, + dim: (text: string) => text, + red: (text: string) => text, + bgRed: (text: string) => text, + bgCyan: (text: string) => text, + white: (text: string) => text + } +})) + +const mockFs = vi.mocked(fs) + +describe('CLI module loading behavior after refactoring', () => { + const mockConsoleLog = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + console.log = mockConsoleLog + }) + + it('should not show warning during module load (warning moved to function execution)', async () => { + // Mock config file read error + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + + // Import the module - this should NOT trigger any console.log + await import('../../src/commands/index') + + expect(mockConsoleLog).not.toHaveBeenCalled() + }) + + it('should load module successfully when config file exists', async () => { + // Mock successful config file read + mockFs.readFileSync = vi.fn().mockReturnValue( + JSON.stringify({ defaultProject: 'owlpay' }) + ) + + // Import the module + await import('../../src/commands/index') + + // No console.log should be called during module loading + expect(mockConsoleLog).not.toHaveBeenCalled() + }) +}) diff --git a/tests/commands/cli-validation.test.ts b/tests/commands/cli-validation.test.ts new file mode 100644 index 0000000..9074660 --- /dev/null +++ b/tests/commands/cli-validation.test.ts @@ -0,0 +1,192 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import fs from 'fs' +import prompts from 'prompts' +import execa from 'execa' + +// Mock dependencies +vi.mock('fs') +vi.mock('prompts') +vi.mock('execa') +vi.mock('picocolors', () => ({ + default: { + yellow: (text: string) => text, + italic: (text: string) => text, + magenta: (text: string) => text, + bgGreen: (text: string) => text, + bold: (text: string) => text, + green: (text: string) => text, + cyan: (text: string) => text, + dim: (text: string) => text, + red: (text: string) => text, + bgRed: (text: string) => text, + bgCyan: (text: string) => text, + white: (text: string) => text + } +})) + +const mockFs = vi.mocked(fs) +const mockPrompts = vi.mocked(prompts) +const mockExeca = vi.mocked(execa) + +describe('CLI validation and edge cases', () => { + const mockConsoleLog = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + console.log = mockConsoleLog + + // Default mock for file not found + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('step type conditions', () => { + it('should handle step_is_default_project conditional type', async () => { + mockFs.readFileSync = vi.fn().mockReturnValue( + JSON.stringify({ defaultProject: 'owlpay' }) + ) + + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test', + commit_description: '', + is_jira: true, + is_default_project: false, // This should trigger the conditional logic + project_type: 'owlnest', + jira_id: 123 + }) + + mockExeca.mockResolvedValue({ + stdout: '[main abc123] [OW-123] ๐Ÿ’ก feat: test', + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', '[OW-123] ๐Ÿ’ก feat: test' + ]) + }) + + it('should handle step_project_type conditional logic when no default project', async () => { + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test', + commit_description: '', + is_jira: true, + project_type: 'market', + jira_id: 456 + }) + + mockExeca.mockResolvedValue({ + stdout: '[main def456] [MAR-456] ๐Ÿ’ก feat: test', + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', '[MAR-456] ๐Ÿ’ก feat: test' + ]) + }) + + it('should handle onSubmit with undefined answers', async () => { + mockPrompts.mockImplementation((questions, options) => { + if (options?.onSubmit) { + // Simulate onSubmit being called with undefined answers + const shouldStop = options.onSubmit({} as any, undefined, {} as any) + expect(shouldStop).toBe(true) + } + return Promise.resolve({}) + }) + + const { default: cli } = await import('../../src/commands/index') + const result = await cli() + + expect(result).toBe(false) + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('commit abort') + ) + }) + }) + + describe('edge cases', () => { + it('should handle no default project with Jira flow', async () => { + // No config file, so no default project + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test', + commit_description: '', + is_jira: true, + project_type: 'paynow', + jira_id: 999 + }) + + mockExeca.mockResolvedValue({ + stdout: '[main xyz999] [PN-999] ๐Ÿ’ก feat: test', + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', '[PN-999] ๐Ÿ’ก feat: test' + ]) + }) + + it('should handle all project types in Jira flow', async () => { + const projects = [ + { value: 'owlpay', prefix: 'OWLPAY' }, + { value: 'owlnest', prefix: 'OW' }, + { value: 'market', prefix: 'MAR' }, + { value: 'paynow', prefix: 'PN' }, + { value: 'wallet-pro', prefix: 'WP' } + ] + + for (const project of projects) { + vi.clearAllMocks() + + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test', + commit_description: '', + is_jira: true, + project_type: project.value, + jira_id: 123 + }) + + mockExeca.mockResolvedValue({ + stdout: `[main abc123] [${project.prefix}-123] ๐Ÿ’ก feat: test`, + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', `[${project.prefix}-123] ๐Ÿ’ก feat: test` + ]) + } + }) + }) +}) diff --git a/tests/commands/cli.test.ts b/tests/commands/cli.test.ts new file mode 100644 index 0000000..8d021fa --- /dev/null +++ b/tests/commands/cli.test.ts @@ -0,0 +1,362 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import fs from 'fs' +import prompts from 'prompts' +import execa from 'execa' + +// Mock dependencies +vi.mock('fs') +vi.mock('prompts') +vi.mock('execa') +vi.mock('picocolors', () => ({ + default: { + yellow: (text: string) => text, + italic: (text: string) => text, + magenta: (text: string) => text, + bgGreen: (text: string) => text, + bold: (text: string) => text, + green: (text: string) => text, + cyan: (text: string) => text, + dim: (text: string) => text, + red: (text: string) => text, + bgRed: (text: string) => text, + bgCyan: (text: string) => text, + white: (text: string) => text + } +})) + +const mockFs = vi.mocked(fs) +const mockPrompts = vi.mocked(prompts) +const mockExeca = vi.mocked(execa) + +// Custom error type for git errors +interface GitError extends Error { + stderr?: string + exitCode?: number +} + +describe('CLI main command', () => { + const mockConsoleLog = vi.fn() + const mockConsoleError = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + console.log = mockConsoleLog + console.error = mockConsoleError + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('config file handling', () => { + it('should load default project when config exists', async () => { + // Mock config file exists + mockFs.readFileSync = vi.fn().mockReturnValue( + JSON.stringify({ defaultProject: 'owlpay' }) + ) + + // Mock successful prompts flow + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test feature', + commit_description: '', + is_jira: false + }) + + // Mock successful git commit + mockExeca.mockResolvedValue({ + stdout: '[main abc123] ๐Ÿ’ก feat: test feature' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockFs.readFileSync).toHaveBeenCalled() + }) + + it('should handle missing config file gracefully', async () => { + // Mock config file read error + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + + // Mock successful prompts flow + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test feature', + commit_description: '', + is_jira: false + }) + + // Mock successful git commit + mockExeca.mockResolvedValue({ + stdout: '[main abc123] ๐Ÿ’ก feat: test feature' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + // The warning message is shown during module loading, not during execution + // So we just verify the function completed successfully + expect(mockExeca).toHaveBeenCalledWith('git', ['commit', '-m', '๐Ÿ’ก feat: test feature']) + }) + }) + + describe('commit flow without Jira', () => { + beforeEach(() => { + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + }) + + it('should handle basic commit without description', async () => { + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'add new feature', + commit_description: '', + is_jira: false + }) + + mockExeca.mockResolvedValue({ + stdout: '[main abc123] ๐Ÿ’ก feat: add new feature', + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', ['commit', '-m', '๐Ÿ’ก feat: add new feature']) + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('๐Ÿ’ก feat: add new feature') + ) + }) + + it('should handle commit with description', async () => { + mockPrompts.mockResolvedValue({ + commit_type: 'fix', + commit_message: 'fix bug', + commit_description: 'Fixed critical bug in authentication', + is_jira: false + }) + + mockExeca.mockResolvedValue({ + stdout: '[main def456] ๐Ÿ› fix: fix bug', + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', '๐Ÿ› fix: fix bug', '-m', 'Fixed critical bug in authentication' + ]) + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('Fixed critical bug in authentication') + ) + }) + + it('should handle all commit types', async () => { + const commitTypes = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'hotfix', 'release', 'storybook', 'revert'] + + for (const type of commitTypes) { + vi.clearAllMocks() + + mockPrompts.mockResolvedValue({ + commit_type: type, + commit_message: `test ${type}`, + commit_description: '', + is_jira: false + }) + + mockExeca.mockResolvedValue({ + stdout: `[main abc123] ${type}: test ${type}`, + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', expect.stringContaining(`${type}: test ${type}`) + ]) + } + }) + }) + + describe('commit flow with Jira', () => { + beforeEach(() => { + mockFs.readFileSync = vi.fn().mockReturnValue( + JSON.stringify({ defaultProject: 'owlpay' }) + ) + }) + + it('should handle Jira commit with default project', async () => { + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'new feature', + commit_description: '', + is_jira: true, + is_default_project: true, + jira_id: 12345 + }) + + mockExeca.mockResolvedValue({ + stdout: '[main abc123] [OWLPAY-12345] ๐Ÿ’ก feat: new feature', + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', '[OWLPAY-12345] ๐Ÿ’ก feat: new feature' + ]) + }) + + it('should handle Jira commit with custom project', async () => { + mockPrompts.mockResolvedValue({ + commit_type: 'fix', + commit_message: 'bug fix', + commit_description: '', + is_jira: true, + is_default_project: false, + project_type: 'owlnest', + jira_id: 98765 + }) + + mockExeca.mockResolvedValue({ + stdout: '[main def456] [OW-98765] ๐Ÿ› fix: bug fix', + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', '[OW-98765] ๐Ÿ› fix: bug fix' + ]) + }) + + it('should handle Jira commit with description', async () => { + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'new feature', + commit_description: 'Added user authentication', + is_jira: true, + is_default_project: true, + jira_id: 11111 + }) + + mockExeca.mockResolvedValue({ + stdout: '[main ghi789] [OWLPAY-11111] ๐Ÿ’ก feat: new feature', + stderr: '' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockExeca).toHaveBeenCalledWith('git', [ + 'commit', '-m', '[OWLPAY-11111] ๐Ÿ’ก feat: new feature', '-m', 'Added user authentication' + ]) + }) + }) + + describe('error handling', () => { + beforeEach(() => { + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + }) + + it('should handle user cancellation', async () => { + mockPrompts.mockImplementation((questions, options) => { + if (options?.onCancel) { + options.onCancel({} as any, {} as any) + } + return Promise.resolve({}) + }) + + const { default: cli } = await import('../../src/commands/index') + const result = await cli() + + expect(result).toBe(false) + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('commit abort') + ) + }) + + it('should handle git commit errors', async () => { + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test feature', + commit_description: '', + is_jira: false + }) + + const gitError: GitError = new Error('Git error') + gitError.stderr = 'fatal: not a git repository' + gitError.exitCode = 128 + mockExeca.mockRejectedValue(gitError) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('fatal: not a git repository') + ) + expect(mockConsoleError).toHaveBeenCalledWith(gitError) + }) + + it('should handle "no changes" git error', async () => { + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test feature', + commit_description: '', + is_jira: false + }) + + const gitError: GitError = new Error('No changes') + gitError.stderr = 'nothing to commit' + gitError.exitCode = 1 + mockExeca.mockRejectedValue(gitError) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('No changes added to commit') + ) + }) + + it('should handle git stderr output', async () => { + mockPrompts.mockResolvedValue({ + commit_type: 'feat', + commit_message: 'test feature', + commit_description: '', + is_jira: false + }) + + mockExeca.mockResolvedValue({ + stdout: '[main abc123] ๐Ÿ’ก feat: test feature', + stderr: 'warning: some git warning' + } as any) + + const { default: cli } = await import('../../src/commands/index') + await cli() + + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('warning: some git warning') + ) + }) + }) + + describe('validation functions', () => { + it('should test commit message validation logic', async () => { + // We test validation indirectly by checking the prompts configuration + const { default: cli } = await import('../../src/commands/index') + + // This is more of a structural test to ensure the function exists + expect(typeof cli).toBe('function') + }) + }) +}) diff --git a/tests/commands/index-refactored.test.ts b/tests/commands/index-refactored.test.ts new file mode 100644 index 0000000..809e598 --- /dev/null +++ b/tests/commands/index-refactored.test.ts @@ -0,0 +1,407 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import fs from 'fs' +import type { CommitResponse, ProjectConfig, CommitResult } from '../../src/types' +import { + loadProjectConfig, + getDefaultProjectValue, + buildCommitTypesList, + buildProjectsList, + findCommitType, + findProject, + buildCommitTitle, + buildFinalCommitMessage, + buildGitCommands, + parseCommitResult, + buildPromptSteps, + showConfigMissingWarning, + showCancelMessage, + showCommitResult, + showGitOutput, + showGitError +} from '../../src/commands/index' + +// Mock dependencies +vi.mock('fs') +vi.mock('picocolors', () => ({ + default: { + yellow: (text: string) => text, + italic: (text: string) => text, + magenta: (text: string) => text, + bgGreen: (text: string) => text, + bold: (text: string) => text, + green: (text: string) => text, + cyan: (text: string) => text, + dim: (text: string) => text, + red: (text: string) => text, + bgRed: (text: string) => text, + bgCyan: (text: string) => text, + white: (text: string) => text + } +})) + +const mockFs = vi.mocked(fs) + +describe('Refactored CLI Functions (TDD Style)', () => { + const mockConsoleLog = vi.fn() + const mockConsoleError = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + console.log = mockConsoleLog + console.error = mockConsoleError + }) + + describe('Configuration Loading Functions', () => { + describe('loadProjectConfig', () => { + it('should load valid config file', () => { + const mockConfig = { defaultProject: 'owlpay' } + mockFs.readFileSync = vi.fn().mockReturnValue(JSON.stringify(mockConfig)) + + const result = loadProjectConfig('/path/to/config.json') + + expect(result).toEqual(mockConfig) + expect(mockFs.readFileSync).toHaveBeenCalledWith('/path/to/config.json', 'utf8') + }) + + it('should return null for invalid config file', () => { + mockFs.readFileSync = vi.fn().mockImplementation(() => { + throw new Error('File not found') + }) + + const result = loadProjectConfig('/path/to/config.json') + + expect(result).toBeNull() + }) + + it('should return null for invalid JSON', () => { + mockFs.readFileSync = vi.fn().mockReturnValue('invalid json') + + const result = loadProjectConfig('/path/to/config.json') + + expect(result).toBeNull() + }) + }) + + describe('getDefaultProjectValue', () => { + it('should return default project when config exists', () => { + const config: ProjectConfig = { defaultProject: 'owlpay' } + const result = getDefaultProjectValue(config) + expect(result).toBe('owlpay') + }) + + it('should return empty string when config is null', () => { + const result = getDefaultProjectValue(null) + expect(result).toBe('') + }) + + it('should return empty string when defaultProject is undefined', () => { + const config: ProjectConfig = {} + const result = getDefaultProjectValue(config) + expect(result).toBe('') + }) + }) + }) + + describe('List Building Functions', () => { + describe('buildCommitTypesList', () => { + it('should build commit types list correctly', () => { + const result = buildCommitTypesList() + + expect(result).toHaveLength(13) + expect(result[0]).toMatchObject({ + title: 'chore', + description: '๐Ÿงน Build process or auxiliary tool changes', + value: 'chore', + emoji: '๐Ÿงน' + }) + }) + }) + + describe('buildProjectsList', () => { + it('should build projects list correctly', () => { + const result = buildProjectsList() + + expect(result).toHaveLength(5) + expect(result[0]).toMatchObject({ + title: 'OwlPay', + description: '[OWLPAY-13845] title', + value: 'owlpay' + }) + }) + }) + }) + + describe('Finder Functions', () => { + describe('findCommitType', () => { + it('should find existing commit type', () => { + const result = findCommitType('feat') + expect(result).toMatchObject({ + name: 'feat', + emoji: '๐Ÿ’ก', + value: 'feat' + }) + }) + + it('should return undefined for non-existing commit type', () => { + const result = findCommitType('nonexistent') + expect(result).toBeUndefined() + }) + }) + + describe('findProject', () => { + it('should find existing project', () => { + const result = findProject('owlpay') + expect(result).toMatchObject({ + name: 'OwlPay', + prefix: 'OWLPAY', + value: 'owlpay' + }) + }) + + it('should return undefined for non-existing project', () => { + const result = findProject('nonexistent') + expect(result).toBeUndefined() + }) + }) + }) + + describe('Commit Message Building Functions', () => { + describe('buildCommitTitle', () => { + it('should build commit title correctly', () => { + const result = buildCommitTitle('feat', 'add new feature') + expect(result).toBe('๐Ÿ’ก feat: add new feature') + }) + + it('should throw error for invalid commit type', () => { + expect(() => buildCommitTitle('invalid', 'message')).toThrow('Invalid commit type: invalid') + }) + }) + + describe('buildFinalCommitMessage', () => { + it('should return plain commit title when not using Jira', () => { + const response: CommitResponse = { + commit_type: 'feat', + commit_message: 'test', + is_jira: false + } + + const result = buildFinalCommitMessage('๐Ÿ’ก feat: test', response, 'owlpay') + expect(result).toBe('๐Ÿ’ก feat: test') + }) + + it('should build Jira commit with default project', () => { + const response: CommitResponse = { + commit_type: 'feat', + commit_message: 'test', + is_jira: true, + is_default_project: true, + jira_id: 12345 + } + + const result = buildFinalCommitMessage('๐Ÿ’ก feat: test', response, 'owlpay') + expect(result).toBe('[OWLPAY-12345] ๐Ÿ’ก feat: test') + }) + + it('should build Jira commit with selected project', () => { + const response: CommitResponse = { + commit_type: 'feat', + commit_message: 'test', + is_jira: true, + is_default_project: false, + project_type: 'owlnest', + jira_id: 67890 + } + + const result = buildFinalCommitMessage('๐Ÿ’ก feat: test', response, 'owlpay') + expect(result).toBe('[OW-67890] ๐Ÿ’ก feat: test') + }) + + it('should throw error for invalid project type', () => { + const response: CommitResponse = { + commit_type: 'feat', + commit_message: 'test', + is_jira: true, + is_default_project: false, + project_type: 'invalid' + } + + expect(() => buildFinalCommitMessage('๐Ÿ’ก feat: test', response, 'owlpay')) + .toThrow('Invalid project type: invalid') + }) + + it('should throw error when Jira ID is missing', () => { + const response: CommitResponse = { + commit_type: 'feat', + commit_message: 'test', + is_jira: true, + is_default_project: true + } + + expect(() => buildFinalCommitMessage('๐Ÿ’ก feat: test', response, 'owlpay')) + .toThrow('Jira ID is required when using Jira integration') + }) + }) + + describe('buildGitCommands', () => { + it('should build basic git command without description', () => { + const result = buildGitCommands('feat: add feature') + expect(result).toEqual(['commit', '-m', 'feat: add feature']) + }) + + it('should build git command with description', () => { + const result = buildGitCommands('feat: add feature', 'detailed description') + expect(result).toEqual(['commit', '-m', 'feat: add feature', '-m', 'detailed description']) + }) + + it('should handle empty description as falsy value', () => { + const result = buildGitCommands('feat: add feature', '') + // Empty string is falsy, so it should NOT include the second -m flag + expect(result).toEqual(['commit', '-m', 'feat: add feature']) + }) + }) + }) + + describe('Result Parsing Functions', () => { + describe('parseCommitResult', () => { + it('should parse git commit output correctly', () => { + const stdout = '[main abc123] feat: add new feature' + const result = parseCommitResult(stdout) + + expect(result).toEqual({ + branch: 'main', + hash: 'abc123' + }) + }) + + it('should handle different branch names', () => { + const stdout = '[feature/test-branch def456] fix: bug fix' + const result = parseCommitResult(stdout) + + expect(result).toEqual({ + branch: 'feature/test-branch', + hash: 'def456' + }) + }) + + it('should throw error for invalid output format', () => { + const stdout = 'invalid git output' + expect(() => parseCommitResult(stdout)).toThrow('Could not parse commit result') + }) + }) + }) + + describe('Prompt Step Building Functions', () => { + describe('buildPromptSteps', () => { + it('should build all steps when default project exists', () => { + const projectsList = buildProjectsList() + const typesList = buildCommitTypesList() + + const steps = buildPromptSteps('owlpay', projectsList, typesList) + + expect(steps).toHaveLength(7) // All steps including default project step + expect(steps[0]).toMatchObject({ + type: 'autocomplete', + name: 'commit_type' + }) + }) + + it('should skip default project step when no default project', () => { + const projectsList = buildProjectsList() + const typesList = buildCommitTypesList() + + const steps = buildPromptSteps('', projectsList, typesList) + + expect(steps).toHaveLength(6) // Missing default project step + }) + }) + }) + + describe('Display Functions', () => { + describe('showConfigMissingWarning', () => { + it('should display warning message', () => { + showConfigMissingWarning() + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('You can try `cz -i` to choose a default project prefix') + ) + }) + }) + + describe('showCancelMessage', () => { + it('should display cancel message', () => { + showCancelMessage() + expect(mockConsoleLog).toHaveBeenCalledWith(' commit abort. ') + }) + }) + + describe('showCommitResult', () => { + it('should display basic commit result', () => { + const result: CommitResult = { + title: 'feat: new feature' + } + + showCommitResult(result) + + expect(mockConsoleLog).toHaveBeenCalledWith('-----------------------------------------------------------') + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('feat: new feature')) + }) + + it('should display commit result with description and hash', () => { + const result: CommitResult = { + title: 'feat: new feature', + description: 'detailed description', + hash: 'abc123', + branch: 'main' + } + + showCommitResult(result) + + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('feat: new feature')) + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('detailed description')) + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('abc123')) + expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('main')) + }) + }) + + describe('showGitOutput', () => { + it('should display git stdout only', () => { + showGitOutput('git output') + + expect(mockConsoleLog).toHaveBeenCalledWith('-----------------------------------------------------------') + expect(mockConsoleLog).toHaveBeenCalledWith('git output') + }) + + it('should display git stdout and stderr', () => { + showGitOutput('git output', 'git error') + + expect(mockConsoleLog).toHaveBeenCalledWith('git output') + expect(mockConsoleLog).toHaveBeenCalledWith('git error') + }) + }) + + describe('showGitError', () => { + it('should display git error with no changes message', () => { + const error = { + stderr: 'git error', + exitCode: 1 + } + + showGitError(error) + + expect(mockConsoleLog).toHaveBeenCalledWith('git error') + expect(mockConsoleLog).toHaveBeenCalledWith(' No changes added to commit. ') + }) + + it('should display git error without special message', () => { + const error = { + stderr: 'some other error', + exitCode: 2 + } + + showGitError(error) + + expect(mockConsoleLog).toHaveBeenCalledWith('some other error') + expect(mockConsoleError).toHaveBeenCalledWith(error) + }) + }) + }) +}) diff --git a/tests/commands/init.test.ts b/tests/commands/init.test.ts new file mode 100644 index 0000000..9bf47ba --- /dev/null +++ b/tests/commands/init.test.ts @@ -0,0 +1,188 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import fs from 'fs' +import prompts from 'prompts' + +// Mock dependencies +vi.mock('fs') +vi.mock('prompts') +vi.mock('picocolors', () => ({ + default: { + green: (text: string) => text, + bgRed: (text: string) => text, + white: (text: string) => text, + magenta: (text: string) => text + } +})) + +const mockFs = vi.mocked(fs) +const mockPrompts = vi.mocked(prompts) + +describe('init command', () => { + const mockConsoleLog = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + console.log = mockConsoleLog + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should save selected project to config file', async () => { + // Mock prompts response + mockPrompts.mockResolvedValue({ + set_default_project: 'owlpay' + }) + + // Mock fs.writeFileSync + mockFs.writeFileSync = vi.fn() + + const { default: init } = await import('../../src/commands/init') + + await init() + + expect(mockPrompts).toHaveBeenCalled() + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('keep/cz_config.json'), + JSON.stringify({ defaultProject: 'owlpay' }, null, 2) + ) + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('default project set: owlpay') + ) + }) + + it('should handle user cancellation', async () => { + // Mock prompts cancellation + mockPrompts.mockImplementation((questions, options) => { + if (options?.onCancel) { + options.onCancel({} as any, {} as any) + } + return Promise.resolve({}) + }) + + const { default: init } = await import('../../src/commands/init') + + const result = await init() + + expect(result).toBe(false) + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('init abort') + ) + }) + + it('should handle user cancellation via onSubmit with undefined', async () => { + // Mock prompts with onSubmit returning undefined + mockPrompts.mockImplementation((questions, options) => { + if (options?.onSubmit) { + // onSubmit should return true to stop, and answers being undefined triggers cancellation + options.onSubmit({} as any, undefined, {} as any) + } + return Promise.resolve({}) + }) + + const { default: init } = await import('../../src/commands/init') + + const result = await init() + + expect(result).toBe(false) + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('init abort') + ) + }) + + it('should handle file write errors', async () => { + // Mock prompts response + mockPrompts.mockResolvedValue({ + set_default_project: 'owlpay' + }) + + // Mock fs.writeFileSync to throw error + mockFs.writeFileSync = vi.fn().mockImplementation(() => { + throw new Error('Permission denied') + }) + + const { default: init } = await import('../../src/commands/init') + + await init() + + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining('init Fail') + ) + }) + + it('should present correct project choices', async () => { + mockPrompts.mockResolvedValue({ + set_default_project: 'owlnest' + }) + mockFs.writeFileSync = vi.fn() + + const { default: init } = await import('../../src/commands/init') + + await init() + + expect(mockPrompts).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + type: 'autocomplete', + name: 'set_default_project', + message: 'Set default project prefix.', + choices: expect.arrayContaining([ + expect.objectContaining({ + title: 'OwlPay', + value: 'owlpay' + }), + expect.objectContaining({ + title: 'OwlNest', + value: 'owlnest' + }) + ]) + }) + ]), + expect.any(Object) + ) + }) + + it('should test all available projects in choices', async () => { + mockPrompts.mockResolvedValue({ + set_default_project: 'market' + }) + mockFs.writeFileSync = vi.fn() + + const { default: init } = await import('../../src/commands/init') + + await init() + + const callArgs = mockPrompts.mock.calls[0] + const stepType = callArgs[0][0] + expect(stepType.choices).toEqual( + expect.arrayContaining([ + expect.objectContaining({ title: 'OwlPay', value: 'owlpay' }), + expect.objectContaining({ title: 'OwlNest', value: 'owlnest' }), + expect.objectContaining({ title: 'Market', value: 'market' }), + expect.objectContaining({ title: 'PayNow', value: 'paynow' }), + expect.objectContaining({ title: 'Wallet Pro', value: 'wallet-pro' }) + ]) + ) + }) + + it('should handle successful completion with different projects', async () => { + const projects = ['owlpay', 'owlnest', 'market', 'paynow', 'wallet-pro'] + + for (const project of projects) { + vi.clearAllMocks() + + mockPrompts.mockResolvedValue({ + set_default_project: project + }) + mockFs.writeFileSync = vi.fn() + + const { default: init } = await import('../../src/commands/init') + await init() + + expect(mockConsoleLog).toHaveBeenCalledWith( + expect.stringContaining(`default project set: ${project}`) + ) + } + }) +}) diff --git a/tests/commands/where.test.ts b/tests/commands/where.test.ts new file mode 100644 index 0000000..9c5c586 --- /dev/null +++ b/tests/commands/where.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// Mock console.log to capture output +const mockConsoleLog = vi.fn() +console.log = mockConsoleLog + +describe('where command', () => { + beforeEach(() => { + mockConsoleLog.mockClear() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should show config directory path', async () => { + // We need to dynamically import to ensure mocks are in place + const { default: where } = await import('../../src/commands/where') + + where() + + expect(mockConsoleLog).toHaveBeenCalled() + const output = mockConsoleLog.mock.calls[0][0] + expect(output).toContain('Your config will be saved in') + expect(output).toContain('src/commands') + }) + + it('should display a path ending with a period', async () => { + const { default: where } = await import('../../src/commands/where') + + where() + + expect(mockConsoleLog).toHaveBeenCalled() + const output = mockConsoleLog.mock.calls[0][0] + expect(output).toMatch(/.*\.$/) + }) +}) diff --git a/tests/helper/commit-types.test.ts b/tests/helper/commit-types.test.ts new file mode 100644 index 0000000..1a7c8a2 --- /dev/null +++ b/tests/helper/commit-types.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import commitTypes from '../../src/helper/commit-types' + +describe('commit-types', () => { + it('should export an array of commit types', () => { + expect(Array.isArray(commitTypes)).toBe(true) + expect(commitTypes.length).toBeGreaterThan(0) + }) + + it('should have all required properties for each commit type', () => { + commitTypes.forEach(type => { + expect(type).toHaveProperty('name') + expect(type).toHaveProperty('emoji') + expect(type).toHaveProperty('description') + expect(type).toHaveProperty('value') + + expect(typeof type.name).toBe('string') + expect(typeof type.emoji).toBe('string') + expect(typeof type.description).toBe('string') + expect(typeof type.value).toBe('string') + }) + }) + + it('should contain expected commit types', () => { + const expectedTypes = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'hotfix', 'release', 'storybook', 'revert'] + const actualTypes = commitTypes.map(type => type.value) + + expectedTypes.forEach(expectedType => { + expect(actualTypes).toContain(expectedType) + }) + }) + + it('should have unique values', () => { + const values = commitTypes.map(type => type.value) + const uniqueValues = [...new Set(values)] + expect(values.length).toBe(uniqueValues.length) + }) + + it('should have non-empty descriptions', () => { + commitTypes.forEach(type => { + expect(type.description.trim()).not.toBe('') + }) + }) + + it('should have emoji characters', () => { + commitTypes.forEach(type => { + expect(type.emoji).toMatch(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u) + }) + }) +}) diff --git a/tests/helper/projects.test.ts b/tests/helper/projects.test.ts new file mode 100644 index 0000000..f755652 --- /dev/null +++ b/tests/helper/projects.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest' +import projects from '../../src/helper/projects' + +describe('projects', () => { + it('should export an array of projects', () => { + expect(Array.isArray(projects)).toBe(true) + expect(projects.length).toBeGreaterThan(0) + }) + + it('should have all required properties for each project', () => { + projects.forEach(project => { + expect(project).toHaveProperty('name') + expect(project).toHaveProperty('prefix') + expect(project).toHaveProperty('value') + + expect(typeof project.name).toBe('string') + expect(typeof project.prefix).toBe('string') + expect(typeof project.value).toBe('string') + }) + }) + + it('should contain expected projects', () => { + const expectedProjects = ['owlpay', 'owlnest', 'market', 'paynow', 'wallet-pro'] + const actualProjects = projects.map(project => project.value) + + expectedProjects.forEach(expectedProject => { + expect(actualProjects).toContain(expectedProject) + }) + }) + + it('should have unique values', () => { + const values = projects.map(project => project.value) + const uniqueValues = [...new Set(values)] + expect(values.length).toBe(uniqueValues.length) + }) + + it('should have unique prefixes', () => { + const prefixes = projects.map(project => project.prefix) + const uniquePrefixes = [...new Set(prefixes)] + expect(prefixes.length).toBe(uniquePrefixes.length) + }) + + it('should have non-empty names and prefixes', () => { + projects.forEach(project => { + expect(project.name.trim()).not.toBe('') + expect(project.prefix.trim()).not.toBe('') + expect(project.value.trim()).not.toBe('') + }) + }) + + it('should have uppercase prefixes', () => { + projects.forEach(project => { + expect(project.prefix).toBe(project.prefix.toUpperCase()) + }) + }) + + it('should have lowercase values', () => { + projects.forEach(project => { + expect(project.value).toBe(project.value.toLowerCase()) + }) + }) +}) diff --git a/tests/index.test.ts b/tests/index.test.ts new file mode 100644 index 0000000..cb82fe0 --- /dev/null +++ b/tests/index.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// Mock the command modules +vi.mock('../src/commands', () => ({ + default: vi.fn() +})) + +vi.mock('../src/commands/init', () => ({ + default: vi.fn() +})) + +vi.mock('../src/commands/where', () => ({ + default: vi.fn() +})) + +describe('main index', () => { + let mockCli: any + let mockInit: any + let mockWhere: any + + beforeEach(async () => { + vi.clearAllMocks() + + // Import mocked modules + mockCli = (await import('../src/commands')).default + mockInit = (await import('../src/commands/init')).default + mockWhere = (await import('../src/commands/where')).default + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should call init command when --init flag is provided', async () => { + const { run } = await import('../src/index') + + await run(['cz', '--init']) + + expect(mockInit).toHaveBeenCalled() + expect(mockCli).not.toHaveBeenCalled() + expect(mockWhere).not.toHaveBeenCalled() + }) + + it('should call init command when -i flag is provided', async () => { + const { run } = await import('../src/index') + + await run(['cz', '-i']) + + expect(mockInit).toHaveBeenCalled() + expect(mockCli).not.toHaveBeenCalled() + expect(mockWhere).not.toHaveBeenCalled() + }) + + it('should call where command when --where flag is provided', async () => { + const { run } = await import('../src/index') + + await run(['cz', '--where']) + + expect(mockWhere).toHaveBeenCalled() + expect(mockInit).not.toHaveBeenCalled() + expect(mockCli).not.toHaveBeenCalled() + }) + + it('should call where command when -w flag is provided', async () => { + const { run } = await import('../src/index') + + await run(['cz', '-w']) + + expect(mockWhere).toHaveBeenCalled() + expect(mockInit).not.toHaveBeenCalled() + expect(mockCli).not.toHaveBeenCalled() + }) + + it('should call default cli command when no flags are provided', async () => { + const { run } = await import('../src/index') + + await run(['cz']) + + expect(mockCli).toHaveBeenCalled() + expect(mockInit).not.toHaveBeenCalled() + expect(mockWhere).not.toHaveBeenCalled() + }) + + it('should handle multiple arguments correctly', async () => { + const { run } = await import('../src/index') + + await run(['cz', 'some', 'other', 'args']) + + expect(mockCli).toHaveBeenCalled() + expect(mockInit).not.toHaveBeenCalled() + expect(mockWhere).not.toHaveBeenCalled() + }) +}) diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..bd3b059 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,11 @@ +// Test setup file +import { vi } from 'vitest' + +// Setup global mocks +global.console = { + ...console, + log: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn() +} diff --git a/tests/utils/test-helpers.ts b/tests/utils/test-helpers.ts new file mode 100644 index 0000000..ae144ce --- /dev/null +++ b/tests/utils/test-helpers.ts @@ -0,0 +1,46 @@ +import { vi } from 'vitest' + +/** + * Creates a mock console object for testing console output + */ +export function createMockConsole() { + return { + log: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn() + } +} + +/** + * Helper to mock prompts responses + */ +export function mockPromptsResponse(response: any) { + const prompts = vi.hoisted(() => vi.fn()) + prompts.mockResolvedValue(response) + return prompts +} + +/** + * Helper to mock file system operations + */ +export function createMockFs() { + return { + writeFileSync: vi.fn(), + readFileSync: vi.fn(), + existsSync: vi.fn() + } +} + +/** + * Helper to test git command execution + */ +export function mockGitExecution(result: { stdout: string; stderr?: string; exitCode?: number }) { + const execa = vi.hoisted(() => vi.fn()) + execa.mockResolvedValue({ + stdout: result.stdout, + stderr: result.stderr || '', + exitCode: result.exitCode || 0 + }) + return execa +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..1861f49 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + setupFiles: ['./tests/setup.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'json-summary', 'html', 'lcov'], + exclude: [ + 'node_modules/**', + 'tests/**', + 'dist/**', + 'coverage/**', + '*.config.{js,ts}', + 'bin/**', + 'scripts/**', + 'src/types/**' + ], + all: true + } + }, + resolve: { + alias: { + '@': './src' + } + } +})