Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# git-json-resolver

## 0.0.1

### Patch Changes

- Wire generic types through resolveConflicts function for improved type safety with custom strategies
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "git-json-resolver",
"author": "Mayank Kumar Chaudhari (https://mayank-chaudhari.vercel.app)",
"private": false,
"version": "0.0.0",
"version": "0.0.1",
"description": "A rules-based JSON conflict resolver that parses Git conflict markers, reconstructs ours/theirs, and merges with deterministic strategies — beyond line-based merges.",
"license": "MPL-2.0",
"main": "./dist/index.js",
Expand Down
133 changes: 133 additions & 0 deletions lib/src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// cli.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import fs from "node:fs";
import path from "node:path";
import * as child_process from "node:child_process";

vi.mock("node:fs");
vi.mock("node:child_process");
vi.mock("./index", () => ({
resolveConflicts: vi.fn(),
}));
vi.mock("./normalizer", () => ({
DEFAULT_CONFIG: { defaultStrategy: "merge" },
}));

// Import after mocks
import type { Config } from "./types";

// Re-import CLI helpers (not the top-level IIFE)
import * as cli from "./cli";

describe("cli helpers", () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe("findGitRoot", () => {
it("returns git root from execSync", () => {
vi.spyOn(child_process, "execSync").mockReturnValue("/git/root\n" as any);
const root = (cli as any).findGitRoot();
expect(root).toBe("/git/root");
});

it("falls back to process.cwd() on error", () => {
vi.spyOn(child_process, "execSync").mockImplementation(() => {
throw new Error("no git");
});
const root = (cli as any).findGitRoot();
expect(root).toBe(process.cwd());
});
});

describe("parseArgs", () => {
it("parses include/exclude/matcher/debug/strict-arrays/sidecar", () => {
const argv = [
"node",
"cli",
"--include",
"a.json,b.json",
"--exclude",
"c.json",
"--matcher",
"micromatch",
"--debug",
"--strict-arrays",
"--sidecar",
];
const result = (cli as any).parseArgs(argv);
expect(result.overrides).toEqual({
include: ["a.json", "b.json"],
exclude: ["c.json"],
matcher: "micromatch",
debug: true,
strictArrays: true,
writeConflictSidecar: true,
});
expect(result.init).toBe(false);
});

it("sets init flag", () => {
const result = (cli as any).parseArgs(["node", "cli", "--init"]);
expect(result.init).toBe(true);
});

it("warns on unknown option", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
(cli as any).parseArgs(["node", "cli", "--unknown"]);
expect(warn).toHaveBeenCalledWith("[git-json-resolver] Unknown option: --unknown");
});
});

describe("initConfig", () => {
const tmpDir = "/tmp/test-cli";
const configPath = path.join(tmpDir, "git-json-resolver.config.js");

it("writes starter config if none exists", () => {
(fs.existsSync as any).mockReturnValue(false);
const writeFileSync = vi.spyOn(fs, "writeFileSync").mockImplementation(() => {});
const log = vi.spyOn(console, "log").mockImplementation(() => {});

(cli as any).initConfig(tmpDir);

expect(writeFileSync).toHaveBeenCalled();
expect(log).toHaveBeenCalledWith(
`[git-json-resolver] Created starter config at ${configPath}`,
);
});

it("exits if config already exists", () => {
(fs.existsSync as any).mockReturnValue(true);
const exit = vi.spyOn(process, "exit").mockImplementation(() => {
throw new Error("exit");
});
const error = vi.spyOn(console, "error").mockImplementation(() => {});

expect(() => (cli as any).initConfig(tmpDir)).toThrow("exit");
expect(error).toHaveBeenCalledWith(
`[git-json-resolver] Config file already exists: ${configPath}`,
);
expect(exit).toHaveBeenCalledWith(1);
});
});

describe("loadConfigFile", () => {
it("returns {} if no config found", async () => {
(fs.existsSync as any).mockReturnValue(false);
const result = await (cli as any).loadConfigFile();
expect(result).toEqual({});
});

it.skip("loads config from js file", async () => {
const fakeConfig: Partial<Config> = { debug: true };
(fs.existsSync as any).mockReturnValue(true);
vi.doMock("/git/root/git-json-resolver.config.js", () => ({
default: fakeConfig,
}));
vi.spyOn(child_process, "execSync").mockReturnValue("/git/root\n" as any);

const mod = await (cli as any).loadConfigFile();
expect(mod).toEqual(fakeConfig);
});
});
});
8 changes: 4 additions & 4 deletions lib/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const CONFIG_FILENAME = "git-json-resolver.config.js";
/**
* Find Git root directory
*/
const findGitRoot = (): string => {
export const findGitRoot = (): string => {
try {
return execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
} catch {
Expand All @@ -23,7 +23,7 @@ const findGitRoot = (): string => {
/**
* Load configuration file (js/ts) from current dir or Git root.
*/
const loadConfigFile = async (): Promise<Partial<Config>> => {
export const loadConfigFile = async (): Promise<Partial<Config>> => {
const searchDirs = [process.cwd(), findGitRoot()];
const configNames = [CONFIG_FILENAME, "git-json-resolver.config.ts"];

Expand All @@ -42,7 +42,7 @@ const loadConfigFile = async (): Promise<Partial<Config>> => {
/**
* Write a starter config file
*/
const initConfig = (targetDir: string) => {
export const initConfig = (targetDir: string) => {
const targetPath = path.join(targetDir, CONFIG_FILENAME);
if (fs.existsSync(targetPath)) {
console.error(`[git-json-resolver] Config file already exists: ${targetPath}`);
Expand All @@ -63,7 +63,7 @@ module.exports = ${JSON.stringify(DEFAULT_CONFIG, null, 2)};
/**
* CLI argument parser (minimal, no external deps).
*/
const parseArgs = (argv: string[]): { overrides: Partial<Config>; init?: boolean } => {
export const parseArgs = (argv: string[]): { overrides: Partial<Config>; init?: boolean } => {
const overrides: Partial<Config> = {};
let init = false;

Expand Down
8 changes: 5 additions & 3 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { parseConflictContent } from "./file-parser";
import { serialize } from "./file-serializer";
import { Conflict, mergeObject } from "./merger";
import { normalizeConfig, NormalizedConfig } from "./normalizer";
import { Config } from "./types";
import { Config, InbuiltMergeStrategies } from "./types";
import { backupFile, listMatchingFiles } from "./utils";
import fs from "node:fs/promises";
import { reconstructConflict } from "./conflict-helper";
Expand All @@ -12,8 +12,10 @@ export * from "./types";

const _strategyCache = new Map<string, string[]>();

export const resolveConflicts = async (config: Config) => {
const normalizedConfig: NormalizedConfig = await normalizeConfig(config);
export const resolveConflicts = async <T extends string = InbuiltMergeStrategies>(
config: Config<T>,
) => {
const normalizedConfig: NormalizedConfig = await normalizeConfig<T>(config);
const filesEntries = await listMatchingFiles(normalizedConfig);
await Promise.all(
filesEntries.map(async ({ filePath, content }) => {
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @repo/shared

## 0.0.1

### Patch Changes

- Updated dependencies
- [email protected]
2 changes: 1 addition & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@repo/shared",
"version": "0.0.0",
"version": "0.0.1",
"private": true,
"sideEffects": false,
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion scripts/publish-canonical.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { execSync } = require("child_process");

// Publish canonical packages
[].forEach(pkg => {
["json-merge-resolver", "json-conflict-resolver"].forEach(pkg => {
execSync(`sed -i -e "s/name.*/name\\": \\"${pkg.replace(/\//g, "\\\\/")}\\",/" lib/package.json`);
execSync("cd lib && npm publish --provenance --access public");
});
Loading