json-merge-resolver
Version:
A rules-based JSON conflict resolver that parses Git conflict markers, reconstructs ours/theirs, and merges with deterministic strategies — beyond line-based merges.
208 lines (207 loc) • 8.08 kB
TypeScript
import { createLogger } from "./logger";
import { Matcher } from "./matcher";
import { StrategyStatus_OK, StrategyStatus_CONTINUE, StrategyStatus_FAIL, StrategyStatus_SKIP } from "./utils";
/**
* Built-in merge strategies.
* ⚠️ Reserved names — plugin authors should avoid reusing these.
*
* | Strategy | Description |
* |-------------|-----------------------------------------------------------------------------|
* | `merge` | Deep merge objects/arrays where possible; conflicts bubble up otherwise. |
* | `ours` | Always take the value from "ours". |
* | `theirs` | Always take the value from "theirs". |
* | `base` | Revert to the common ancestor ("base"). |
* | `skip` | Leave unresolved; conflict entry is created with `undefined`. |
* | `drop` | Remove the field entirely (always). |
* | `non-empty` | Prefer non-empty value: `ours` > `theirs` > `base`; fallback to undefined. |
* | `update` | Update with "theirs" if field exists in "ours"; drop if missing in "ours". |
* | `concat` | Concatenate arrays from both sides (applies only if both are arrays). |
* | `unique` | Merge arrays and remove duplicates (applies only if both are arrays). |
*/
export type InbuiltMergeStrategies = "merge" | "ours" | "theirs" | "base" | "skip" | "drop" | "non-empty" | "update" | "concat" | "unique";
export type StrategyStatus = typeof StrategyStatus_OK | typeof StrategyStatus_CONTINUE | typeof StrategyStatus_FAIL | typeof StrategyStatus_SKIP;
/**
* Union type representing the outcome of a strategy function.
*/
export type StrategyResult = {
status: typeof StrategyStatus_OK;
value: unknown;
} | {
status: typeof StrategyStatus_CONTINUE;
reason?: string;
} | {
status: typeof StrategyStatus_SKIP;
reason: string;
} | {
status: typeof StrategyStatus_FAIL;
reason: string;
};
/**
* Strategy function signature.
*
* @template TContext - Optional context type passed during resolution.
*/
export type StrategyFn<TContext = unknown> = (args: {
/** Value from "ours". */
ours: unknown;
/** Value from "theirs". */
theirs: unknown;
/** Value from common ancestor (if available). */
base?: unknown;
/** JSON path of the current field. */
path: string;
/** Full file path of the file being merged. */
filePath?: string;
/** Custom context object, if provided by caller. */
context?: TContext;
/** logger */
logger: Awaited<ReturnType<typeof createLogger>>;
}) => StrategyResult | Promise<StrategyResult>;
/**
* Utility type: excludes strategy names ending with "!".
* The "!" suffix is reserved for internal overrides.
*/
type ForbidBangEnd<T extends string> = T extends `${string}!` ? never : T;
/**
* Interface for plugin-contributed strategies.
* Plugins should augment this interface to add their strategy names.
*
* Example plugin declaration:
* ```ts
* declare module "git-json-resolver" {
* interface PluginStrategies {
* "semantic-version": string;
* "timestamp-latest": string;
* }
* }
* ```
*/
export interface PluginStrategies {
}
/**
* All available strategy names (built-in + plugins).
*/
export type AllStrategies = InbuiltMergeStrategies | keyof PluginStrategies;
/**
* Interface for plugin-specific configuration.
* Plugins augment this interface with their own config types.
*
* Example plugin declaration:
* ```ts
* declare module "git-json-resolver" {
* interface PluginConfigs {
* "git-json-resolver-semver": {
* strict?: boolean;
* fallback?: "ours" | "theirs" | "continue" | "error";
* preferValid?: boolean;
* preferRange?: boolean;
* workspacePattern?: string;
* };
* }
* }
* ```
*/
export interface PluginConfigs {
[ ]: Record<string, unknown>;
}
/**
* Rule tree: maps field glob patterns → strategy names or nested rule trees.
*
* - Keys: field glob patterns (matcher configurable)
* - Values: one or more strategies, or further nested rules
*/
export type RuleTree<T extends string = AllStrategies> = {
[ ]: T[] | RuleTree<T>;
};
/** Logging levels available to the logger. */
export type LogLevel = "info" | "warn" | "error" | "debug";
/** Logging modes: in-memory or streaming to files. */
export type LogMode = "memory" | "stream";
/**
* Logger configuration.
*/
export interface LoggerConfig {
/** Logging mode (default: `"memory"`). */
mode?: LogMode;
/** Directory for log files (default: `"logs"`). */
logDir?: string;
/** Whether to log into a single file instead of per-input-file logs. */
singleFile?: boolean;
/** Level filters for stdout and file logging. */
levels?: {
/** Levels to print on stdout (default: `["warn", "error"]`). */
stdout?: LogLevel[];
/** Levels to write into files (default: `["info", "warn", "error"]`). */
file?: LogLevel[];
};
}
/** A parser function that takes a raw string and returns parsed content. */
export type Parser = {
name: string;
parser: (input: string) => unknown;
};
/** Built-in parser identifiers or a custom parser function. */
export type SupportedParsers = "json" | "json5" | "yaml" | "toml" | "xml" | Parser;
/**
* Plugin interface that strategy plugins must implement.
*/
export interface StrategyPlugin<TContext = unknown> {
/** Strategy functions provided by this plugin. */
strategies: Record<string, StrategyFn<TContext>>;
/** Optional plugin initialization with config. */
init?(config: PluginConfigs[keyof PluginConfigs]): void | Promise<void>;
}
/**
* High-level configuration object for conflict resolution.
*
* @template T - Strategy string type (defaults to built-in strategies).
* @template TContext - Context object type passed to strategies.
*/
export interface Config<T extends string = AllStrategies, TContext = unknown> {
/** Fallback strategy when no rule matches. */
defaultStrategy?: T | T[];
/** Rule tree mapping globs → strategies. */
rules?: RuleTree<T>;
/** Reverse mapping: strategy → list of field globs. */
byStrategy?: Partial<Record<T, string[]>>;
/** Custom strategies (names must not clash with built-ins). */
customStrategies?: Record<Exclude<ForbidBangEnd<T>, InbuiltMergeStrategies>, StrategyFn<TContext>>;
/** File inclusion globs. */
include?: string[];
/** File exclusion globs. */
exclude?: string[];
/** Glob matcher: `"micromatch"`, `"picomatch"`, or a custom implementation. Defaults to internal minimal matcher */
matcher?: "micromatch" | "picomatch" | Matcher;
/**
* Whether to include files that do not contain conflicts.
* Useful if strategies (e.g., "drop") should apply even to clean files.
* Default: `false`.
*/
includeNonConflicted?: boolean;
/** Enable debug mode for verbose logs and traceability (slower). */
debug?: boolean;
/** Whether to write sidecar files with unresolved conflicts. */
writeConflictSidecar?: boolean;
/** Logger configuration. */
loggerConfig?: LoggerConfig;
/** Directory for backing up original files before modification. */
backupDir?: string;
/**
* Parsers to attempt, in order:
* - A single parser (`"json"`, `"yaml"`, custom function, etc.).
* - An array of parsers (e.g. `["yaml", "json5"]`).
*
* Defaults to `"json"`.
*/
parsers?: "auto" | SupportedParsers | SupportedParsers[];
/**
* Whether to automatically stage resolved files.
* Default: `true`.
*/
autoStage?: boolean;
/** NPM package names of strategy plugins to load. */
plugins?: string[];
/** Plugin-specific configuration passed to plugin.init(). */
pluginConfig?: Partial<PluginConfigs>;
}
export type { Matcher };