Rebuild.
This commit is contained in:
+16
-1
@@ -21,9 +21,24 @@ keywords:
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
GITHUB_TOKEN:
|
GITHUB_TOKEN:
|
||||||
description: 'GitHub token'
|
description: 'Token used for API calls (GitHub/Gitea/Forgejo). Typically set to ${{ secrets.GITHUB_TOKEN }}'
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
DRY_RUN:
|
||||||
|
description: 'If true, the action will log proposed changes but will not rebase or push commits'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
GIT_PLATFORM:
|
||||||
|
description: 'Platform the action is running on (e.g., "github", "gitea", "forgejo"). Determines API usage'
|
||||||
|
required: false
|
||||||
|
default: 'github'
|
||||||
|
|
||||||
|
GIT_PLATFORM_API_BASE:
|
||||||
|
description: 'Base API URL for non-GitHub platforms (e.g., Gitea/Forgejo) for diff endpoints'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
main: 'out/github-action.cjs'
|
main: 'out/github-action.cjs'
|
||||||
|
|||||||
+591
-794
File diff suppressed because it is too large
Load Diff
+102
-135
@@ -8,11 +8,17 @@ import { generateCommitMessageByDiff } from './generateCommitMessageFromGitDiff'
|
|||||||
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
import { randomIntFromInterval } from './utils/randomIntFromInterval';
|
||||||
import { sleep } from './utils/sleep';
|
import { sleep } from './utils/sleep';
|
||||||
|
|
||||||
// This should be a token with access to your repository scoped in as a secret.
|
// ------------ New Imports ------------
|
||||||
// The YML workflow will need to set GITHUB_TOKEN with the GitHub Secret Token
|
import fetch from 'node-fetch';
|
||||||
// GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
// https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#about-the-github_token-secret
|
// Inputs
|
||||||
const GITHUB_TOKEN = core.getInput('GITHUB_TOKEN');
|
const GITHUB_TOKEN = core.getInput('GITHUB_TOKEN');
|
||||||
|
const DRY_RUN = core.getInput('DRY_RUN') === 'true';
|
||||||
|
const ocPrefix = '[OpenCommit]'; // Commit prefix to skip
|
||||||
|
|
||||||
|
// Platforms: github | gitea | forgejo (via GIT_PLATFORM env)
|
||||||
|
const PLATFORM = (process.env.GIT_PLATFORM || 'github').toLowerCase();
|
||||||
|
|
||||||
const octokit = github.getOctokit(GITHUB_TOKEN);
|
const octokit = github.getOctokit(GITHUB_TOKEN);
|
||||||
const context = github.context;
|
const context = github.context;
|
||||||
const owner = context.repo.owner;
|
const owner = context.repo.owner;
|
||||||
@@ -21,149 +27,132 @@ const repo = context.repo.repo;
|
|||||||
type SHA = string;
|
type SHA = string;
|
||||||
type Diff = string;
|
type Diff = string;
|
||||||
|
|
||||||
async function getCommitDiff(commitSha: string) {
|
async function fetchDiffFromAPI(commitSha: string) {
|
||||||
const diffResponse = await octokit.request<string>(
|
if (PLATFORM === 'github') {
|
||||||
'GET /repos/{owner}/{repo}/commits/{ref}',
|
const resp = await octokit.request<string>(
|
||||||
{
|
'GET /repos/{owner}/{repo}/commits/{ref}',
|
||||||
owner,
|
{
|
||||||
repo,
|
owner,
|
||||||
ref: commitSha,
|
repo,
|
||||||
headers: {
|
ref: commitSha,
|
||||||
Accept: 'application/vnd.github.v3.diff'
|
headers: { Accept: 'application/vnd.github.v3.diff' }
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
return resp.data;
|
||||||
return { sha: commitSha, diff: diffResponse.data };
|
} else {
|
||||||
|
// Gitea/Forgejo unified diff endpoint
|
||||||
|
const diffUrl = `${process.env.GIT_PLATFORM_API_BASE}/repos/${owner}/${repo}/git/commits/${commitSha}.diff`;
|
||||||
|
const response = await fetch(diffUrl, {
|
||||||
|
headers: { Authorization: `token ${GITHUB_TOKEN}` }
|
||||||
|
});
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCommitDiff(commitSha: string) {
|
||||||
|
const diff = await fetchDiffFromAPI(commitSha);
|
||||||
|
return { sha: commitSha, diff };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out merge commits and those already rewritten
|
||||||
|
function filterCommits(
|
||||||
|
commits: { id: string; message: string }[]
|
||||||
|
) {
|
||||||
|
return commits.filter(({ message }) => {
|
||||||
|
if (message.startsWith('Merge')) return false;
|
||||||
|
if (message.startsWith(ocPrefix)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiffAndSHA {
|
interface DiffAndSHA {
|
||||||
sha: SHA;
|
sha: SHA;
|
||||||
diff: Diff;
|
diff: Diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MsgAndSHA {
|
interface MsgAndSHA {
|
||||||
sha: SHA;
|
sha: SHA;
|
||||||
msg: string;
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// send only 3-4 size chunks of diffs in steps,
|
// split into chunks for OpenAI
|
||||||
// because openAI restricts "too many requests" at once with 429 error
|
|
||||||
async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) {
|
async function improveMessagesInChunks(diffsAndSHAs: DiffAndSHA[]) {
|
||||||
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
|
const chunkSize = diffsAndSHAs!.length % 2 === 0 ? 4 : 3;
|
||||||
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
outro(`Improving commit messages in chunks of ${chunkSize}.`);
|
||||||
const improvePromises = diffsAndSHAs!.map((commit) =>
|
const improvePromises = diffsAndSHAs!.map((c) =>
|
||||||
generateCommitMessageByDiff(commit.diff, false)
|
generateCommitMessageByDiff(c.diff, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
let improvedMessagesAndSHAs: MsgAndSHA[] = [];
|
let improved: MsgAndSHA[] = [];
|
||||||
for (let step = 0; step < improvePromises.length; step += chunkSize) {
|
for (let step = 0; step < improvePromises.length; step += chunkSize) {
|
||||||
const chunkOfPromises = improvePromises.slice(step, step + chunkSize);
|
const chunkPromises = improvePromises.slice(step, step + chunkSize);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chunkOfImprovedMessages = await Promise.all(chunkOfPromises);
|
const results = await Promise.all(chunkPromises);
|
||||||
|
results.forEach((msg, idx) => {
|
||||||
const chunkOfImprovedMessagesBySha = chunkOfImprovedMessages.map(
|
const sha = diffsAndSHAs![improved.length + idx].sha;
|
||||||
(improvedMsg, i) => {
|
improved.push({ sha, msg });
|
||||||
const index = improvedMessagesAndSHAs.length;
|
});
|
||||||
const sha = diffsAndSHAs![index + i].sha;
|
await sleep(
|
||||||
|
1000 * randomIntFromInterval(1, 5) +
|
||||||
return { sha, msg: improvedMsg };
|
100 * randomIntFromInterval(1, 5)
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
improvedMessagesAndSHAs.push(...chunkOfImprovedMessagesBySha);
|
const wait = 60000 + 1000 * randomIntFromInterval(1, 5);
|
||||||
|
outro(`Retrying after ${wait}`);
|
||||||
// sometimes openAI errors with 429 code (too many requests),
|
await sleep(wait);
|
||||||
// so lets sleep a bit
|
|
||||||
const sleepFor =
|
|
||||||
1000 * randomIntFromInterval(1, 5) + 100 * randomIntFromInterval(1, 5);
|
|
||||||
|
|
||||||
outro(
|
|
||||||
`Improved ${chunkOfPromises.length} messages. Sleeping for ${sleepFor}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await sleep(sleepFor);
|
|
||||||
} catch (error) {
|
|
||||||
outro(error as string);
|
|
||||||
|
|
||||||
// if sleeping in try block still fails with 429,
|
|
||||||
// openAI wants at least 1 minute before next request
|
|
||||||
const sleepFor = 60000 + 1000 * randomIntFromInterval(1, 5);
|
|
||||||
outro(`Retrying after sleeping for ${sleepFor}`);
|
|
||||||
await sleep(sleepFor);
|
|
||||||
|
|
||||||
// go to previous step
|
|
||||||
step -= chunkSize;
|
step -= chunkSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return improved;
|
||||||
return improvedMessagesAndSHAs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDiffsBySHAs = async (SHAs: string[]) => {
|
|
||||||
const diffPromises = SHAs.map((sha) => getCommitDiff(sha));
|
|
||||||
|
|
||||||
const diffs = await Promise.all(diffPromises).catch((error) => {
|
|
||||||
outro(`Error in Promise.all(getCommitDiffs(SHAs)): ${error}.`);
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
return diffs;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function improveCommitMessages(
|
async function improveCommitMessages(
|
||||||
commitsToImprove: { id: string; message: string }[]
|
commits: { id: string; message: string }[]
|
||||||
): Promise<void> {
|
) {
|
||||||
if (commitsToImprove.length) {
|
const toImprove = filterCommits(commits);
|
||||||
outro(`Found ${commitsToImprove.length} commits to improve.`);
|
if (!toImprove.length) {
|
||||||
} else {
|
outro('No eligible commits to improve.');
|
||||||
outro('No new commits found.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
outro('Fetching commit diffs by SHAs.');
|
const shas = toImprove.map((c) => c.id);
|
||||||
const commitSHAsToImprove = commitsToImprove.map((commit) => commit.id);
|
const diffs = await Promise.all(shas.map((sha) => getCommitDiff(sha)));
|
||||||
const diffsWithSHAs = await getDiffsBySHAs(commitSHAsToImprove);
|
|
||||||
outro('Done.');
|
|
||||||
|
|
||||||
const improvedMessagesWithSHAs = await improveMessagesInChunks(diffsWithSHAs);
|
const improved = await improveMessagesInChunks(diffs);
|
||||||
|
|
||||||
console.log(
|
const changesExist = improved.some(
|
||||||
`Improved ${improvedMessagesWithSHAs.length} commits: `,
|
({ msg }, i) => msg !== toImprove[i].message
|
||||||
improvedMessagesWithSHAs
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if there are actually any changes in the commit messages
|
if (!changesExist) {
|
||||||
const messagesChanged = improvedMessagesWithSHAs.some(
|
outro('No commit message changes.');
|
||||||
({ sha, msg }, index) => msg !== commitsToImprove[index].message
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!messagesChanged) {
|
|
||||||
console.log('No changes in commit messages detected, skipping rebase');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCommitMessageFile = (message: string, index: number) =>
|
if (DRY_RUN) {
|
||||||
writeFileSync(`./commit-${index}.txt`, message);
|
outro('Dry‑run: logging proposed changes (no rebase/push).');
|
||||||
improvedMessagesWithSHAs.forEach(({ msg }, i) =>
|
improved.forEach(({ sha, msg }) =>
|
||||||
createCommitMessageFile(msg, i)
|
console.log(`Would rewrite ${sha} => ${msg}`)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
improved.forEach(({ msg }, i) =>
|
||||||
|
writeFileSync(`./commit-${i}.txt`, msg)
|
||||||
);
|
);
|
||||||
|
|
||||||
writeFileSync(`./count.txt`, '0');
|
writeFileSync('./count.txt', '0');
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
'./rebase-exec.sh',
|
'./rebase-exec.sh',
|
||||||
`#!/bin/bash
|
`#!/bin/bash
|
||||||
count=$(cat count.txt)
|
count=$(cat count.txt)
|
||||||
git commit --amend -F commit-$count.txt
|
git commit --amend -F commit-$count.txt
|
||||||
echo $(( count + 1 )) > count.txt`
|
echo $((count+1)) > count.txt`
|
||||||
);
|
);
|
||||||
|
|
||||||
await exec.exec(`chmod +x ./rebase-exec.sh`);
|
await exec.exec('chmod +x ./rebase-exec.sh');
|
||||||
|
|
||||||
await exec.exec(
|
await exec.exec(
|
||||||
'git',
|
'git',
|
||||||
['rebase', `${commitsToImprove[0].id}^`, '--exec', './rebase-exec.sh'],
|
['rebase', `${shas[0]}^`, '--exec', './rebase-exec.sh'],
|
||||||
{
|
{
|
||||||
env: {
|
env: {
|
||||||
GIT_SEQUENCE_EDITOR: 'sed -i -e "s/^pick/reword/g"',
|
GIT_SEQUENCE_EDITOR: 'sed -i -e "s/^pick/reword/g"',
|
||||||
@@ -173,53 +162,31 @@ async function improveCommitMessages(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteCommitMessageFile = (index: number) =>
|
toImprove.forEach((_c, i) => unlinkSync(`./commit-${i}.txt`));
|
||||||
unlinkSync(`./commit-${index}.txt`);
|
|
||||||
commitsToImprove.forEach((_commit, i) => deleteCommitMessageFile(i));
|
|
||||||
|
|
||||||
unlinkSync('./count.txt');
|
unlinkSync('./count.txt');
|
||||||
unlinkSync('./rebase-exec.sh');
|
unlinkSync('./rebase-exec.sh');
|
||||||
|
|
||||||
outro('Force pushing non-interactively rebased commits into remote.');
|
await exec.exec('git', ['push', '--force']);
|
||||||
|
outro('Done');
|
||||||
await exec.exec('git', ['status']);
|
|
||||||
|
|
||||||
// Force push the rebased commits
|
|
||||||
await exec.exec('git', ['push', `--force`]);
|
|
||||||
|
|
||||||
outro('Done 🧙');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
intro('OpenCommit — improving lame commit messages');
|
intro('OpenCommit — improving commit messages');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (github.context.eventName === 'push') {
|
if (context.eventName === 'push') {
|
||||||
outro(`Processing commits in a Push event`);
|
const payload = context.payload as PushEvent;
|
||||||
|
await exec.exec('git', ['config', 'user.email', payload.pusher.email!]);
|
||||||
|
await exec.exec('git', ['config', 'user.name', payload.pusher.name!]);
|
||||||
|
|
||||||
const payload = github.context.payload as PushEvent;
|
|
||||||
|
|
||||||
const commits = payload.commits;
|
await improveCommitMessages(payload.commits);
|
||||||
|
|
||||||
// Set local Git user identity for future git history manipulations
|
|
||||||
if (payload.pusher.email)
|
|
||||||
await exec.exec('git', ['config', 'user.email', payload.pusher.email]);
|
|
||||||
|
|
||||||
await exec.exec('git', ['config', 'user.name', payload.pusher.name]);
|
|
||||||
|
|
||||||
await exec.exec('git', ['status']);
|
|
||||||
await exec.exec('git', ['log', '--oneline']);
|
|
||||||
|
|
||||||
await improveCommitMessages(commits);
|
|
||||||
} else {
|
} else {
|
||||||
outro('Wrong action.');
|
|
||||||
core.error(
|
core.error(
|
||||||
`OpenCommit was called on ${github.context.payload.action}. OpenCommit is supposed to be used on "push" action.`
|
`Wrong event: expected push, got ${context.eventName}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const err = error?.message || error;
|
core.setFailed(error.message || String(error));
|
||||||
core.setFailed(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// src/platform.ts
|
||||||
|
import { Gitea } from '@go-gitea/sdk.js';
|
||||||
|
import github from '@actions/github';
|
||||||
|
import { PushEvent } from '@octokit/webhooks-types';
|
||||||
|
|
||||||
|
export const REWRITE_MARKER = '[OPENCOMMIT]';
|
||||||
|
|
||||||
|
export type NormalizedCommit = {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
author?: { name?: string; email?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isGitea(): boolean {
|
||||||
|
return !!process.env.GITEA_BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPushCommits(): Promise<NormalizedCommit[]> {
|
||||||
|
if (isGitea()) {
|
||||||
|
const payload = github.context.payload as any;
|
||||||
|
const commits = payload.commits as Array<any>;
|
||||||
|
|
||||||
|
return commits
|
||||||
|
.filter((commit) => {
|
||||||
|
if (commit.message.startsWith('Merge')) return false;
|
||||||
|
if (commit.message.includes(REWRITE_MARKER)) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((commit) => ({
|
||||||
|
id: commit.id,
|
||||||
|
message: commit.message,
|
||||||
|
author: commit.author
|
||||||
|
? {
|
||||||
|
name: commit.author.name,
|
||||||
|
email: commit.author.email ?? undefined, // coerce null → undefined
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const payload = github.context.payload as PushEvent;
|
||||||
|
const commits = payload.commits;
|
||||||
|
|
||||||
|
return commits
|
||||||
|
.filter((commit) => {
|
||||||
|
if (commit.message.startsWith('Merge')) return false;
|
||||||
|
if (commit.message.includes(REWRITE_MARKER)) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((commit) => ({
|
||||||
|
id: commit.id,
|
||||||
|
message: commit.message,
|
||||||
|
author: {
|
||||||
|
name: commit.author.name,
|
||||||
|
email: commit.author.email ?? undefined, // coerce null → undefined
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCommitDiff(
|
||||||
|
sha: string,
|
||||||
|
giteaConfig?: { baseUrl: string; token: string }
|
||||||
|
): Promise<{ sha: string; diff: string }> {
|
||||||
|
if (isGitea() && giteaConfig) {
|
||||||
|
const gitea = new Gitea({
|
||||||
|
baseUrl: giteaConfig.baseUrl,
|
||||||
|
auth: giteaConfig.token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await gitea.rest.git.getCommit({
|
||||||
|
owner: process.env.GITEA_REPO_OWNER!,
|
||||||
|
repo: process.env.GITEA_REPO_NAME!,
|
||||||
|
sha,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filesChanged =
|
||||||
|
data.files?.map((f: any) => `diff --git a/${f.filename} b/${f.filename}`).join('\n') ?? '';
|
||||||
|
|
||||||
|
return { sha, diff: filesChanged };
|
||||||
|
} else {
|
||||||
|
const octokit = github.getOctokit(process.env.GITHUB_TOKEN!);
|
||||||
|
const context = github.context;
|
||||||
|
|
||||||
|
const { data: diff } = await octokit.request<string>(
|
||||||
|
'GET /repos/{owner}/{repo}/commits/{ref}',
|
||||||
|
{
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
ref: sha,
|
||||||
|
headers: { Accept: 'application/vnd.github.v3.diff' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { sha, diff };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
require("cli-testing-library/extend-expect");
|
||||||
|
const child_process_1 = require("child_process");
|
||||||
|
const utils_1 = require("./utils");
|
||||||
|
const util_1 = require("util");
|
||||||
|
const cli_testing_library_1 = require("cli-testing-library");
|
||||||
|
const path_2 = require("path");
|
||||||
|
const fs_1 = require("fs");
|
||||||
|
const fsExec = (0, util_1.promisify)(child_process_1.exec);
|
||||||
|
const fsRemove = (0, util_1.promisify)(fs_1.rm);
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* [no remotes]
|
||||||
|
*/
|
||||||
|
const prepareNoRemoteGitRepository = async () => {
|
||||||
|
const tempDir = await (0, utils_1.prepareTempDir)();
|
||||||
|
await fsExec('git init test', { cwd: tempDir });
|
||||||
|
const gitDir = path_1.default.resolve(tempDir, 'test');
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* origin /tmp/remote.git (fetch)
|
||||||
|
* origin /tmp/remote.git (push)
|
||||||
|
*/
|
||||||
|
const prepareOneRemoteGitRepository = async () => {
|
||||||
|
const tempDir = await (0, utils_1.prepareTempDir)();
|
||||||
|
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||||
|
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||||
|
const gitDir = path_1.default.resolve(tempDir, 'test');
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* git remote -v
|
||||||
|
*
|
||||||
|
* origin /tmp/remote.git (fetch)
|
||||||
|
* origin /tmp/remote.git (push)
|
||||||
|
* other ../remote2.git (fetch)
|
||||||
|
* other ../remote2.git (push)
|
||||||
|
*/
|
||||||
|
const prepareTwoRemotesGitRepository = async () => {
|
||||||
|
const tempDir = await (0, utils_1.prepareTempDir)();
|
||||||
|
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||||
|
await fsExec('git init --bare other.git', { cwd: tempDir });
|
||||||
|
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||||
|
const gitDir = path_1.default.resolve(tempDir, 'test');
|
||||||
|
await fsExec('git remote add other ../other.git', { cwd: gitDir });
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
};
|
||||||
|
describe('cli flow to push git branch', () => {
|
||||||
|
it('do nothing when OCO_GITPUSH is set to false', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareNoRemoteGitRepository();
|
||||||
|
await (0, cli_testing_library_1.render)('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await (0, cli_testing_library_1.render)('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
const { queryByText, findByText, userEvent } = await (0, cli_testing_library_1.render)(`OCO_AI_PROVIDER='test' OCO_GITPUSH='false' node`, [(0, path_2.resolve)('./out/cli.cjs')], { cwd: gitDir });
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await queryByText('Choose a remote to push to')).not.toBeInTheConsole();
|
||||||
|
expect(await queryByText('Do you want to run `git push`?')).not.toBeInTheConsole();
|
||||||
|
expect(await queryByText('Successfully pushed all commits to origin')).not.toBeInTheConsole();
|
||||||
|
expect(await queryByText('Command failed with exit code 1')).not.toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
it('push and cause error when there is no remote', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareNoRemoteGitRepository();
|
||||||
|
await (0, cli_testing_library_1.render)('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await (0, cli_testing_library_1.render)('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
const { queryByText, findByText, userEvent } = await (0, cli_testing_library_1.render)(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [(0, path_2.resolve)('./out/cli.cjs')], { cwd: gitDir });
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await queryByText('Choose a remote to push to')).not.toBeInTheConsole();
|
||||||
|
expect(await queryByText('Do you want to run `git push`?')).not.toBeInTheConsole();
|
||||||
|
expect(await queryByText('Successfully pushed all commits to origin')).not.toBeInTheConsole();
|
||||||
|
expect(await findByText('Command failed with exit code 1')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
it('push when one remote is set', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareOneRemoteGitRepository();
|
||||||
|
await (0, cli_testing_library_1.render)('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await (0, cli_testing_library_1.render)('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
const { findByText, userEvent } = await (0, cli_testing_library_1.render)(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [(0, path_2.resolve)('./out/cli.cjs')], { cwd: gitDir });
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Do you want to run `git push`?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
it('push when two remotes are set', async () => {
|
||||||
|
const { gitDir, cleanup } = await prepareTwoRemotesGitRepository();
|
||||||
|
await (0, cli_testing_library_1.render)('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await (0, cli_testing_library_1.render)('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
const { findByText, userEvent } = await (0, cli_testing_library_1.render)(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [(0, path_2.resolve)('./out/cli.cjs')], { cwd: gitDir });
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Choose a remote to push to')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const path_1 = require("path");
|
||||||
|
const cli_testing_library_1 = require("cli-testing-library");
|
||||||
|
require("cli-testing-library/extend-expect");
|
||||||
|
const utils_1 = require("./utils");
|
||||||
|
it('cli flow when there are no changes', async () => {
|
||||||
|
const { gitDir, cleanup } = await (0, utils_1.prepareEnvironment)();
|
||||||
|
const { findByText } = await (0, cli_testing_library_1.render)(`OCO_AI_PROVIDER='test' node`, [(0, path_1.resolve)('./out/cli.cjs')], { cwd: gitDir });
|
||||||
|
expect(await findByText('No changes detected')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const path_1 = require("path");
|
||||||
|
const cli_testing_library_1 = require("cli-testing-library");
|
||||||
|
require("cli-testing-library/extend-expect");
|
||||||
|
const utils_1 = require("./utils");
|
||||||
|
it('cli flow to generate commit message for 1 new file (staged)', async () => {
|
||||||
|
const { gitDir, cleanup } = await (0, utils_1.prepareEnvironment)();
|
||||||
|
await (0, cli_testing_library_1.render)('echo', [`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
|
||||||
|
await (0, cli_testing_library_1.render)('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
const { queryByText, findByText, userEvent } = await (0, cli_testing_library_1.render)(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [(0, path_1.resolve)('./out/cli.cjs')], { cwd: gitDir });
|
||||||
|
expect(await queryByText('No files are staged')).not.toBeInTheConsole();
|
||||||
|
expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole();
|
||||||
|
expect(await findByText('Generating the commit message')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Do you want to run `git push`?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
it('cli flow to generate commit message for 1 changed file (not staged)', async () => {
|
||||||
|
const { gitDir, cleanup } = await (0, utils_1.prepareEnvironment)();
|
||||||
|
await (0, cli_testing_library_1.render)('echo', [`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
|
||||||
|
await (0, cli_testing_library_1.render)('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
await (0, cli_testing_library_1.render)('git', [`commit -m 'add new file'`], { cwd: gitDir });
|
||||||
|
await (0, cli_testing_library_1.render)('echo', [`'console.log("Good night World");' >> index.ts`], { cwd: gitDir });
|
||||||
|
const { findByText, userEvent } = await (0, cli_testing_library_1.render)(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [(0, path_1.resolve)('./out/cli.cjs')], { cwd: gitDir });
|
||||||
|
expect(await findByText('No files are staged')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Do you want to stage all files and generate commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Generating the commit message')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Successfully committed')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Do you want to run `git push`?')).toBeInTheConsole();
|
||||||
|
userEvent.keyboard('[Enter]');
|
||||||
|
expect(await findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const path_1 = require("path");
|
||||||
|
const cli_testing_library_1 = require("cli-testing-library");
|
||||||
|
require("cli-testing-library/extend-expect");
|
||||||
|
const utils_1 = require("../utils");
|
||||||
|
const path_2 = __importDefault(require("path"));
|
||||||
|
function getAbsolutePath(relativePath) {
|
||||||
|
// Use process.cwd() which should be the project root during test execution
|
||||||
|
return path_2.default.resolve(process.cwd(), 'test/e2e/prompt-module', relativePath);
|
||||||
|
}
|
||||||
|
async function setupCommitlint(dir, ver) {
|
||||||
|
let packagePath, packageJsonPath, configPath;
|
||||||
|
switch (ver) {
|
||||||
|
case 9:
|
||||||
|
packagePath = getAbsolutePath('./data/commitlint_9/node_modules');
|
||||||
|
packageJsonPath = getAbsolutePath('./data/commitlint_9/package.json');
|
||||||
|
configPath = getAbsolutePath('./data/commitlint_9/commitlint.config.js');
|
||||||
|
break;
|
||||||
|
case 18:
|
||||||
|
packagePath = getAbsolutePath('./data/commitlint_18/node_modules');
|
||||||
|
packageJsonPath = getAbsolutePath('./data/commitlint_18/package.json');
|
||||||
|
configPath = getAbsolutePath('./data/commitlint_18/commitlint.config.js');
|
||||||
|
break;
|
||||||
|
case 19:
|
||||||
|
packagePath = getAbsolutePath('./data/commitlint_19/node_modules');
|
||||||
|
packageJsonPath = getAbsolutePath('./data/commitlint_19/package.json');
|
||||||
|
configPath = getAbsolutePath('./data/commitlint_19/commitlint.config.js');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await (0, cli_testing_library_1.render)('cp', ['-r', packagePath, '.'], { cwd: dir });
|
||||||
|
await (0, cli_testing_library_1.render)('cp', [packageJsonPath, '.'], { cwd: dir });
|
||||||
|
await (0, cli_testing_library_1.render)('cp', [configPath, '.'], { cwd: dir });
|
||||||
|
await (0, utils_1.wait)(3000); // Avoid flakiness by waiting
|
||||||
|
}
|
||||||
|
describe('cli flow to run "oco commitlint force"', () => {
|
||||||
|
it('on commitlint@9 using CJS', async () => {
|
||||||
|
const { gitDir, cleanup } = await (0, utils_1.prepareEnvironment)();
|
||||||
|
await setupCommitlint(gitDir, 9);
|
||||||
|
const npmList = await (0, cli_testing_library_1.render)('npm', ['list', '@commitlint/load'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
expect(await npmList.findByText('@commitlint/load@9')).toBeInTheConsole();
|
||||||
|
const { findByText } = await (0, cli_testing_library_1.render)(`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
|
node ${(0, path_1.resolve)('./out/cli.cjs')} commitlint force \
|
||||||
|
`, [], { cwd: gitDir });
|
||||||
|
expect(await findByText('opencommit — configure @commitlint')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Read @commitlint configuration')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Generating consistency with given @commitlint rules')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Done - please review contents of')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
it('on commitlint@18 using CJS', async () => {
|
||||||
|
const { gitDir, cleanup } = await (0, utils_1.prepareEnvironment)();
|
||||||
|
await setupCommitlint(gitDir, 18);
|
||||||
|
const npmList = await (0, cli_testing_library_1.render)('npm', ['list', '@commitlint/load'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
expect(await npmList.findByText('@commitlint/load@18')).toBeInTheConsole();
|
||||||
|
const { findByText } = await (0, cli_testing_library_1.render)(`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
|
node ${(0, path_1.resolve)('./out/cli.cjs')} commitlint force \
|
||||||
|
`, [], { cwd: gitDir });
|
||||||
|
expect(await findByText('opencommit — configure @commitlint')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Read @commitlint configuration')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Generating consistency with given @commitlint rules')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Done - please review contents of')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
it('on commitlint@19 using ESM', async () => {
|
||||||
|
const { gitDir, cleanup } = await (0, utils_1.prepareEnvironment)();
|
||||||
|
await setupCommitlint(gitDir, 19);
|
||||||
|
const npmList = await (0, cli_testing_library_1.render)('npm', ['list', '@commitlint/load'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
expect(await npmList.findByText('@commitlint/load@19')).toBeInTheConsole();
|
||||||
|
const { findByText } = await (0, cli_testing_library_1.render)(`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
|
node ${(0, path_1.resolve)('./out/cli.cjs')} commitlint force \
|
||||||
|
`, [], { cwd: gitDir });
|
||||||
|
expect(await findByText('opencommit — configure @commitlint')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Read @commitlint configuration')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Generating consistency with given @commitlint rules')).toBeInTheConsole();
|
||||||
|
expect(await findByText('Done - please review contents of')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('cli flow to generate commit message using @commitlint prompt-module', () => {
|
||||||
|
it('on commitlint@19 using ESM', async () => {
|
||||||
|
const { gitDir, cleanup } = await (0, utils_1.prepareEnvironment)();
|
||||||
|
// Setup commitlint@19
|
||||||
|
await setupCommitlint(gitDir, 19);
|
||||||
|
const npmList = await (0, cli_testing_library_1.render)('npm', ['list', '@commitlint/load'], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
expect(await npmList.findByText('@commitlint/load@19')).toBeInTheConsole();
|
||||||
|
// Run `oco commitlint force`
|
||||||
|
const commitlintForce = await (0, cli_testing_library_1.render)(`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
|
node ${(0, path_1.resolve)('./out/cli.cjs')} commitlint force \
|
||||||
|
`, [], { cwd: gitDir });
|
||||||
|
expect(await commitlintForce.findByText('Done - please review contents of')).toBeInTheConsole();
|
||||||
|
// Run `oco commitlint get`
|
||||||
|
const commitlintGet = await (0, cli_testing_library_1.render)(`
|
||||||
|
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
|
node ${(0, path_1.resolve)('./out/cli.cjs')} commitlint get \
|
||||||
|
`, [], { cwd: gitDir });
|
||||||
|
expect(await commitlintGet.findByText('consistency')).toBeInTheConsole();
|
||||||
|
// Run 'oco' using .opencommit-commitlint
|
||||||
|
await (0, cli_testing_library_1.render)('echo', [`'console.log("Hello World");' > index.ts`], {
|
||||||
|
cwd: gitDir
|
||||||
|
});
|
||||||
|
await (0, cli_testing_library_1.render)('git', ['add index.ts'], { cwd: gitDir });
|
||||||
|
const oco = await (0, cli_testing_library_1.render)(`
|
||||||
|
OCO_TEST_MOCK_TYPE='commit-message' \
|
||||||
|
OCO_PROMPT_MODULE='@commitlint' \
|
||||||
|
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
|
||||||
|
node ${(0, path_1.resolve)('./out/cli.cjs')} \
|
||||||
|
`, [], { cwd: gitDir });
|
||||||
|
expect(await oco.findByText('Generating the commit message')).toBeInTheConsole();
|
||||||
|
expect(await oco.findByText('Confirm the commit message?')).toBeInTheConsole();
|
||||||
|
oco.userEvent.keyboard('[Enter]');
|
||||||
|
expect(await oco.findByText('Do you want to run `git push`?')).toBeInTheConsole();
|
||||||
|
oco.userEvent.keyboard('[Enter]');
|
||||||
|
expect(await oco.findByText('Successfully pushed all commits to origin')).toBeInTheConsole();
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.wait = exports.prepareTempDir = exports.prepareEnvironment = void 0;
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const fs_1 = require("fs");
|
||||||
|
const util_1 = require("util");
|
||||||
|
const os_1 = require("os");
|
||||||
|
const child_process_1 = require("child_process");
|
||||||
|
const fsMakeTempDir = (0, util_1.promisify)(fs_1.mkdtemp);
|
||||||
|
const fsExec = (0, util_1.promisify)(child_process_1.exec);
|
||||||
|
const fsRemove = (0, util_1.promisify)(fs_1.rm);
|
||||||
|
/**
|
||||||
|
* Prepare the environment for the test
|
||||||
|
* Create a temporary git repository in the temp directory
|
||||||
|
*/
|
||||||
|
const prepareEnvironment = async () => {
|
||||||
|
const tempDir = await (0, exports.prepareTempDir)();
|
||||||
|
// Create a remote git repository int the temp directory. This is necessary to execute the `git push` command
|
||||||
|
await fsExec('git init --bare remote.git', { cwd: tempDir });
|
||||||
|
await fsExec('git clone remote.git test', { cwd: tempDir });
|
||||||
|
const gitDir = path_1.default.resolve(tempDir, 'test');
|
||||||
|
const cleanup = async () => {
|
||||||
|
return fsRemove(tempDir, { recursive: true });
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
gitDir,
|
||||||
|
cleanup,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
exports.prepareEnvironment = prepareEnvironment;
|
||||||
|
const prepareTempDir = async () => {
|
||||||
|
return await fsMakeTempDir(path_1.default.join((0, os_1.tmpdir)(), 'opencommit-test-'));
|
||||||
|
};
|
||||||
|
exports.prepareTempDir = prepareTempDir;
|
||||||
|
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
exports.wait = wait;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const globals_1 = require("@jest/globals");
|
||||||
|
require("cli-testing-library/extend-expect");
|
||||||
|
const cli_testing_library_1 = require("cli-testing-library");
|
||||||
|
// Make Jest available globally
|
||||||
|
global.jest = globals_1.jest;
|
||||||
|
/**
|
||||||
|
* Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky
|
||||||
|
*/
|
||||||
|
(0, cli_testing_library_1.configure)({ asyncUtilTimeout: 2000 });
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const fs_1 = require("fs");
|
||||||
|
const config_1 = require("../../src/commands/config");
|
||||||
|
const utils_1 = require("./utils");
|
||||||
|
describe('config', () => {
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
let globalConfigFile;
|
||||||
|
let envConfigFile;
|
||||||
|
function resetEnv(env) {
|
||||||
|
Object.keys(process.env).forEach((key) => {
|
||||||
|
if (!(key in env)) {
|
||||||
|
delete process.env[key];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
process.env[key] = env[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
beforeEach(async () => {
|
||||||
|
resetEnv(originalEnv);
|
||||||
|
if (globalConfigFile)
|
||||||
|
await globalConfigFile.cleanup();
|
||||||
|
if (envConfigFile)
|
||||||
|
await envConfigFile.cleanup();
|
||||||
|
});
|
||||||
|
afterEach(async () => {
|
||||||
|
if (globalConfigFile)
|
||||||
|
await globalConfigFile.cleanup();
|
||||||
|
if (envConfigFile)
|
||||||
|
await envConfigFile.cleanup();
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
resetEnv(originalEnv);
|
||||||
|
});
|
||||||
|
const generateConfig = async (fileName, content) => {
|
||||||
|
const fileContent = Object.entries(content)
|
||||||
|
.map(([key, value]) => `${key}="${value}"`)
|
||||||
|
.join('\n');
|
||||||
|
return await (0, utils_1.prepareFile)(fileName, fileContent);
|
||||||
|
};
|
||||||
|
describe('getConfig', () => {
|
||||||
|
it('should prioritize local .env over global .opencommit config', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'global-key',
|
||||||
|
OCO_MODEL: 'gpt-3.5-turbo',
|
||||||
|
OCO_LANGUAGE: 'en'
|
||||||
|
});
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_API_KEY: 'local-key',
|
||||||
|
OCO_LANGUAGE: 'fr'
|
||||||
|
});
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual('local-key');
|
||||||
|
expect(config.OCO_MODEL).toEqual('gpt-3.5-turbo');
|
||||||
|
expect(config.OCO_LANGUAGE).toEqual('fr');
|
||||||
|
});
|
||||||
|
it('should fallback to global config when local config is not set', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'global-key',
|
||||||
|
OCO_MODEL: 'gpt-4',
|
||||||
|
OCO_LANGUAGE: 'de',
|
||||||
|
OCO_DESCRIPTION: 'true'
|
||||||
|
});
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_API_URL: 'local-api-url'
|
||||||
|
});
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual('global-key');
|
||||||
|
expect(config.OCO_API_URL).toEqual('local-api-url');
|
||||||
|
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||||
|
expect(config.OCO_LANGUAGE).toEqual('de');
|
||||||
|
expect(config.OCO_DESCRIPTION).toEqual(true);
|
||||||
|
});
|
||||||
|
it('should handle boolean and numeric values correctly', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_TOKENS_MAX_INPUT: '4096',
|
||||||
|
OCO_TOKENS_MAX_OUTPUT: '500',
|
||||||
|
OCO_GITPUSH: 'true'
|
||||||
|
});
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_TOKENS_MAX_INPUT: '8192',
|
||||||
|
OCO_ONE_LINE_COMMIT: 'false',
|
||||||
|
OCO_OMIT_SCOPE: 'true'
|
||||||
|
});
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
|
||||||
|
expect(config.OCO_TOKENS_MAX_OUTPUT).toEqual(500);
|
||||||
|
expect(config.OCO_GITPUSH).toEqual(true);
|
||||||
|
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
|
||||||
|
expect(config.OCO_OMIT_SCOPE).toEqual(true);
|
||||||
|
});
|
||||||
|
it('should handle custom HTTP headers correctly', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_CUSTOM_HEADERS: '{"X-Global-Header": "global-value"}'
|
||||||
|
});
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_API_CUSTOM_HEADERS: '{"Authorization": "Bearer token123", "X-Custom-Header": "test-value"}'
|
||||||
|
});
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_CUSTOM_HEADERS).toEqual({ "Authorization": "Bearer token123", "X-Custom-Header": "test-value" });
|
||||||
|
// No need to parse JSON again since it's already an object
|
||||||
|
const parsedHeaders = config.OCO_API_CUSTOM_HEADERS;
|
||||||
|
expect(parsedHeaders).toHaveProperty('Authorization', 'Bearer token123');
|
||||||
|
expect(parsedHeaders).toHaveProperty('X-Custom-Header', 'test-value');
|
||||||
|
expect(parsedHeaders).not.toHaveProperty('X-Global-Header');
|
||||||
|
});
|
||||||
|
it('should handle empty local config correctly', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'global-key',
|
||||||
|
OCO_MODEL: 'gpt-4',
|
||||||
|
OCO_LANGUAGE: 'es'
|
||||||
|
});
|
||||||
|
envConfigFile = await generateConfig('.env', {});
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual('global-key');
|
||||||
|
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||||
|
expect(config.OCO_LANGUAGE).toEqual('es');
|
||||||
|
});
|
||||||
|
it('should override global config with null values in local .env', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'global-key',
|
||||||
|
OCO_MODEL: 'gpt-4',
|
||||||
|
OCO_LANGUAGE: 'es'
|
||||||
|
});
|
||||||
|
envConfigFile = await generateConfig('.env', {
|
||||||
|
OCO_API_KEY: 'null'
|
||||||
|
});
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual(null);
|
||||||
|
});
|
||||||
|
it('should handle empty global config', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
envConfigFile = await generateConfig('.env', {});
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath,
|
||||||
|
envPath: envConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config).not.toEqual(null);
|
||||||
|
expect(config.OCO_API_KEY).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('setConfig', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// we create and delete the file to have the parent directory, but not the file, to test the creation of the file
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
(0, fs_1.rmSync)(globalConfigFile.filePath);
|
||||||
|
});
|
||||||
|
it('should create .opencommit file with DEFAULT CONFIG if it does not exist on first setConfig run', async () => {
|
||||||
|
const isGlobalConfigFileExist = (0, fs_1.existsSync)(globalConfigFile.filePath);
|
||||||
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
|
await (0, config_1.setConfig)([[config_1.CONFIG_KEYS.OCO_API_KEY, 'persisted-key_1']], globalConfigFile.filePath);
|
||||||
|
const fileContent = (0, fs_1.readFileSync)(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent).toContain('OCO_API_KEY=persisted-key_1');
|
||||||
|
Object.entries(config_1.DEFAULT_CONFIG).forEach(([key, value]) => {
|
||||||
|
expect(fileContent).toContain(`${key}=${value}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should set new config values', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
await (0, config_1.setConfig)([
|
||||||
|
[config_1.CONFIG_KEYS.OCO_API_KEY, 'new-key'],
|
||||||
|
[config_1.CONFIG_KEYS.OCO_MODEL, 'gpt-4']
|
||||||
|
], globalConfigFile.filePath);
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config.OCO_API_KEY).toEqual('new-key');
|
||||||
|
expect(config.OCO_MODEL).toEqual('gpt-4');
|
||||||
|
});
|
||||||
|
it('should update existing config values', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {
|
||||||
|
OCO_API_KEY: 'initial-key'
|
||||||
|
});
|
||||||
|
await (0, config_1.setConfig)([[config_1.CONFIG_KEYS.OCO_API_KEY, 'updated-key']], globalConfigFile.filePath);
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config.OCO_API_KEY).toEqual('updated-key');
|
||||||
|
});
|
||||||
|
it('should handle boolean and numeric values correctly', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
await (0, config_1.setConfig)([
|
||||||
|
[config_1.CONFIG_KEYS.OCO_TOKENS_MAX_INPUT, '8192'],
|
||||||
|
[config_1.CONFIG_KEYS.OCO_DESCRIPTION, 'true'],
|
||||||
|
[config_1.CONFIG_KEYS.OCO_ONE_LINE_COMMIT, 'false']
|
||||||
|
], globalConfigFile.filePath);
|
||||||
|
const config = (0, config_1.getConfig)({
|
||||||
|
globalPath: globalConfigFile.filePath
|
||||||
|
});
|
||||||
|
expect(config.OCO_TOKENS_MAX_INPUT).toEqual(8192);
|
||||||
|
expect(config.OCO_DESCRIPTION).toEqual(true);
|
||||||
|
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
|
||||||
|
});
|
||||||
|
it('should throw an error for unsupported config keys', async () => {
|
||||||
|
globalConfigFile = await generateConfig('.opencommit', {});
|
||||||
|
try {
|
||||||
|
await (0, config_1.setConfig)([['UNSUPPORTED_KEY', 'value']], globalConfigFile.filePath);
|
||||||
|
throw new Error('NEVER_REACHED');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
expect(error.message).toContain('Unsupported config key: UNSUPPORTED_KEY');
|
||||||
|
expect(error.message).not.toContain('NEVER_REACHED');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should persist changes to the config file', async () => {
|
||||||
|
const isGlobalConfigFileExist = (0, fs_1.existsSync)(globalConfigFile.filePath);
|
||||||
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
|
await (0, config_1.setConfig)([[config_1.CONFIG_KEYS.OCO_API_KEY, 'persisted-key']], globalConfigFile.filePath);
|
||||||
|
const fileContent = (0, fs_1.readFileSync)(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent).toContain('OCO_API_KEY=persisted-key');
|
||||||
|
});
|
||||||
|
it('should set multiple configs in a row and keep the changes', async () => {
|
||||||
|
const isGlobalConfigFileExist = (0, fs_1.existsSync)(globalConfigFile.filePath);
|
||||||
|
expect(isGlobalConfigFileExist).toBe(false);
|
||||||
|
await (0, config_1.setConfig)([[config_1.CONFIG_KEYS.OCO_API_KEY, 'persisted-key']], globalConfigFile.filePath);
|
||||||
|
const fileContent1 = (0, fs_1.readFileSync)(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent1).toContain('OCO_API_KEY=persisted-key');
|
||||||
|
await (0, config_1.setConfig)([[config_1.CONFIG_KEYS.OCO_MODEL, 'gpt-4']], globalConfigFile.filePath);
|
||||||
|
const fileContent2 = (0, fs_1.readFileSync)(globalConfigFile.filePath, 'utf8');
|
||||||
|
expect(fileContent2).toContain('OCO_MODEL=gpt-4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const gemini_1 = require("../../src/engine/gemini");
|
||||||
|
const generative_ai_1 = require("@google/generative-ai");
|
||||||
|
const config_1 = require("../../src/commands/config");
|
||||||
|
describe('Gemini', () => {
|
||||||
|
let gemini;
|
||||||
|
let mockConfig;
|
||||||
|
let mockGoogleGenerativeAi;
|
||||||
|
let mockGenerativeModel;
|
||||||
|
let mockExit;
|
||||||
|
const noop = (...args) => { };
|
||||||
|
const mockGemini = () => {
|
||||||
|
mockConfig = (0, config_1.getConfig)();
|
||||||
|
gemini = new gemini_1.GeminiEngine({
|
||||||
|
apiKey: mockConfig.OCO_API_KEY,
|
||||||
|
model: mockConfig.OCO_MODEL
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const oldEnv = process.env;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
process.env = { ...oldEnv };
|
||||||
|
jest.mock('@google/generative-ai');
|
||||||
|
jest.mock('../src/commands/config');
|
||||||
|
jest.mock('@clack/prompts', () => ({
|
||||||
|
intro: jest.fn(),
|
||||||
|
outro: jest.fn()
|
||||||
|
}));
|
||||||
|
mockExit = jest.spyOn(process, 'exit').mockImplementation();
|
||||||
|
mockConfig = (0, config_1.getConfig)();
|
||||||
|
mockConfig.OCO_AI_PROVIDER = config_1.OCO_AI_PROVIDER_ENUM.GEMINI;
|
||||||
|
mockConfig.OCO_API_KEY = 'mock-api-key';
|
||||||
|
mockConfig.OCO_MODEL = 'gemini-1.5-flash';
|
||||||
|
mockGoogleGenerativeAi = new generative_ai_1.GoogleGenerativeAI(mockConfig.OCO_API_KEY);
|
||||||
|
mockGenerativeModel = mockGoogleGenerativeAi.getGenerativeModel({
|
||||||
|
model: mockConfig.OCO_MODEL
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
gemini = undefined;
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
mockExit.mockRestore();
|
||||||
|
process.env = oldEnv;
|
||||||
|
});
|
||||||
|
it.skip('should exit process if OCO_GEMINI_API_KEY is not set and command is not config', () => {
|
||||||
|
process.env.OCO_GEMINI_API_KEY = undefined;
|
||||||
|
process.env.OCO_AI_PROVIDER = 'gemini';
|
||||||
|
mockGemini();
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
it('should generate commit message', async () => {
|
||||||
|
const mockGenerateContent = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ response: { text: () => 'generated content' } });
|
||||||
|
mockGenerativeModel.generateContent = mockGenerateContent;
|
||||||
|
mockGemini();
|
||||||
|
const messages = [
|
||||||
|
{ role: 'system', content: 'system message' },
|
||||||
|
{ role: 'assistant', content: 'assistant message' }
|
||||||
|
];
|
||||||
|
jest
|
||||||
|
.spyOn(gemini, 'generateCommitMessage')
|
||||||
|
.mockImplementation(async () => 'generated content');
|
||||||
|
const result = await gemini.generateCommitMessage(messages);
|
||||||
|
expect(result).toEqual('generated content');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const removeContentTags_1 = require("../../src/utils/removeContentTags");
|
||||||
|
describe('removeContentTags', () => {
|
||||||
|
it('should remove content wrapped in specified tags', () => {
|
||||||
|
const content = 'This is <think>something to hide</think> visible content';
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'think');
|
||||||
|
expect(result).toBe('This is visible content');
|
||||||
|
});
|
||||||
|
it('should handle multiple tag occurrences', () => {
|
||||||
|
const content = '<think>hidden</think> visible <think>also hidden</think> text';
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'think');
|
||||||
|
expect(result).toBe('visible text');
|
||||||
|
});
|
||||||
|
it('should handle multiline content within tags', () => {
|
||||||
|
const content = 'Start <think>hidden\nover multiple\nlines</think> End';
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'think');
|
||||||
|
expect(result).toBe('Start End');
|
||||||
|
});
|
||||||
|
it('should return content as is when tag is not found', () => {
|
||||||
|
const content = 'Content without any tags';
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'think');
|
||||||
|
expect(result).toBe('Content without any tags');
|
||||||
|
});
|
||||||
|
it('should work with different tag names', () => {
|
||||||
|
const content = 'This is <custom>something to hide</custom> visible content';
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'custom');
|
||||||
|
expect(result).toBe('This is visible content');
|
||||||
|
});
|
||||||
|
it('should handle null content', () => {
|
||||||
|
const content = null;
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'think');
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
it('should handle undefined content', () => {
|
||||||
|
const content = undefined;
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'think');
|
||||||
|
expect(result).toBe(undefined);
|
||||||
|
});
|
||||||
|
it('should trim the result', () => {
|
||||||
|
const content = ' <think>hidden</think> visible ';
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'think');
|
||||||
|
expect(result).toBe('visible');
|
||||||
|
});
|
||||||
|
it('should handle nested tags correctly', () => {
|
||||||
|
const content = 'Outside <think>Inside <think>Nested</think></think> End';
|
||||||
|
const result = (0, removeContentTags_1.removeContentTags)(content, 'think');
|
||||||
|
expect(result).toBe('Outside End');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.prepareFile = void 0;
|
||||||
|
const fs_1 = require("fs");
|
||||||
|
const os_1 = require("os");
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
const util_1 = require("util");
|
||||||
|
const fsMakeTempDir = (0, util_1.promisify)(fs_1.mkdtemp);
|
||||||
|
const fsRemove = (0, util_1.promisify)(fs_1.rm);
|
||||||
|
const fsWriteFile = (0, util_1.promisify)(fs_1.writeFile);
|
||||||
|
/**
|
||||||
|
* Prepare tmp file for the test
|
||||||
|
*/
|
||||||
|
async function prepareFile(fileName, content) {
|
||||||
|
const tempDir = await fsMakeTempDir(path_1.default.join((0, os_1.tmpdir)(), 'opencommit-test-'));
|
||||||
|
const filePath = path_1.default.resolve(tempDir, fileName);
|
||||||
|
await fsWriteFile(filePath, content);
|
||||||
|
const cleanup = async () => {
|
||||||
|
if ((0, fs_1.existsSync)(tempDir)) {
|
||||||
|
await fsRemove(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
cleanup
|
||||||
|
};
|
||||||
|
}
|
||||||
|
exports.prepareFile = prepareFile;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./out",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"target": "ES2020",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowImportingTsExtensions": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"test",
|
||||||
|
"out"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user