@fern-api/replay 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2082,6 +2082,51 @@ async function buildMergePlan(args) {
2082
2082
  // src/applicator/runMergeAndRecord.ts
2083
2083
  var import_promises2 = require("fs/promises");
2084
2084
  var import_node_path4 = require("path");
2085
+
2086
+ // src/autoversion.ts
2087
+ var AUTOVERSION_PLACEHOLDERS = [
2088
+ "v0.0.0-fern-placeholder",
2089
+ // Go -- a superstring of the default; check first
2090
+ "0.0.0-fern-placeholder",
2091
+ // TS/Java/C#/Ruby/PHP/Rust/default
2092
+ "0.0.0.dev0"
2093
+ // Python (PEP 440 -- Poetry rejects hyphens)
2094
+ ];
2095
+ var VERSION_TOKEN = /(?:v)?\d+\.\d+\.\d+[0-9A-Za-z.+-]*/g;
2096
+ var VERSION_MASK = "@@FERN_VERSION@@";
2097
+ function maskVersions(line) {
2098
+ return line.replace(VERSION_TOKEN, VERSION_MASK);
2099
+ }
2100
+ function containsPlaceholder(content) {
2101
+ if (!content) return false;
2102
+ return AUTOVERSION_PLACEHOLDERS.some((p) => content.includes(p));
2103
+ }
2104
+ function remapManagedLines(genBase, target, theirs) {
2105
+ if (!containsPlaceholder(genBase)) return theirs;
2106
+ const managedMasks = /* @__PURE__ */ new Set();
2107
+ for (const line of genBase.split("\n")) {
2108
+ if (AUTOVERSION_PLACEHOLDERS.some((p) => line.includes(p))) managedMasks.add(maskVersions(line));
2109
+ }
2110
+ if (managedMasks.size === 0) return theirs;
2111
+ const targetByMask = /* @__PURE__ */ new Map();
2112
+ for (const line of target.split("\n")) {
2113
+ const mask = maskVersions(line);
2114
+ if (managedMasks.has(mask) && !targetByMask.has(mask)) targetByMask.set(mask, line);
2115
+ }
2116
+ return theirs.split("\n").map((line) => {
2117
+ const mask = maskVersions(line);
2118
+ const replacement = managedMasks.has(mask) ? targetByMask.get(mask) : void 0;
2119
+ return replacement !== void 0 && replacement !== line ? replacement : line;
2120
+ }).join("\n");
2121
+ }
2122
+ function neutralizeAutoversionTheirs(genBase, theirs) {
2123
+ return remapManagedLines(genBase, genBase, theirs);
2124
+ }
2125
+ function alignAutoversionToOurs(genBase, ours, theirs) {
2126
+ return remapManagedLines(genBase, ours, theirs);
2127
+ }
2128
+
2129
+ // src/applicator/runMergeAndRecord.ts
2085
2130
  async function runMergeAndRecord(args) {
2086
2131
  const { plan, inputs, patch, accumulator } = args;
2087
2132
  const { resolvedPath, metadata, oursPath, ours } = inputs;
@@ -2112,11 +2157,13 @@ async function runMergeAndRecord(args) {
2112
2157
  accumulator.recordMerge(resolvedPath, merged2.content, patch.base_generation);
2113
2158
  return { file: resolvedPath, status: "merged", ...unreachableFlag };
2114
2159
  }
2115
- const merged = threeWayMerge(plan.mergeBase, ours, plan.theirs);
2160
+ const mergeBase = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.mergeBase) : plan.mergeBase;
2161
+ const theirs = inputs.base != null ? alignAutoversionToOurs(inputs.base, ours, plan.theirs) : plan.theirs;
2162
+ const merged = threeWayMerge(mergeBase, ours, theirs);
2116
2163
  await (0, import_promises2.mkdir)((0, import_node_path4.dirname)(oursPath), { recursive: true });
2117
2164
  await (0, import_promises2.writeFile)(oursPath, merged.content);
2118
2165
  if (merged.hasConflicts) {
2119
- accumulator.recordConflict(resolvedPath, plan.theirs, patch.base_generation);
2166
+ accumulator.recordConflict(resolvedPath, theirs, patch.base_generation);
2120
2167
  return {
2121
2168
  file: resolvedPath,
2122
2169
  status: "conflict",
@@ -2125,7 +2172,7 @@ async function runMergeAndRecord(args) {
2125
2172
  conflictMetadata: metadata
2126
2173
  };
2127
2174
  }
2128
- accumulator.recordMerge(resolvedPath, plan.theirs, patch.base_generation);
2175
+ accumulator.recordMerge(resolvedPath, theirs, patch.base_generation);
2129
2176
  return {
2130
2177
  file: resolvedPath,
2131
2178
  status: "merged",
@@ -3911,7 +3958,9 @@ var ReplayService = class {
3911
3958
  if (baseChanged) repointed++;
3912
3959
  continue;
3913
3960
  }
3914
- const diff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
3961
+ const rawDiff = await this.git.exec(["diff", currentGenSha, "--", ...patch.files]).catch(() => null);
3962
+ const neutralizedRebase = rawDiff != null && containsPlaceholder(rawDiff) ? await this.computeAutoversionNeutralizedRebase(patch.files, currentGenSha) : null;
3963
+ const diff = neutralizedRebase ? neutralizedRebase.diff : rawDiff;
3915
3964
  if (diff === null) continue;
3916
3965
  if (!diff.trim()) {
3917
3966
  if (hasProtectedFiles) {
@@ -3967,7 +4016,7 @@ var ReplayService = class {
3967
4016
  patch.base_generation = currentGenSha;
3968
4017
  patch.patch_content = diff;
3969
4018
  patch.content_hash = newContentHash;
3970
- const snapshot = await this.readSnapshotFromDisk(patch.files);
4019
+ const snapshot = neutralizedRebase ? neutralizedRebase.snapshot : await this.readSnapshotFromDisk(patch.files);
3971
4020
  const snapshotIsNonEmpty = Object.keys(snapshot).length > 0;
3972
4021
  if (snapshotIsNonEmpty) {
3973
4022
  patch.theirs_snapshot = snapshot;
@@ -4007,6 +4056,47 @@ var ReplayService = class {
4007
4056
  }
4008
4057
  return snapshot;
4009
4058
  }
4059
+ /**
4060
+ * Autoversion-aware replacement for the naive `git diff currentGen -- files`
4061
+ * + on-disk snapshot used when rebasing a cleanly-applied patch.
4062
+ *
4063
+ * The generation tree (BASE) still carries the placeholder on version lines
4064
+ * (autoversion runs after `[fern-generated]`), but the working tree holds
4065
+ * autoversion's resolved semver. A raw diff/snapshot would fold the
4066
+ * placeholder->semver swap into the customer's patch_content/theirs_snapshot,
4067
+ * pinning a stale version. This re-derives both against a version-neutralized
4068
+ * THEIRS so the pipeline-owned version line never enters the patch.
4069
+ *
4070
+ * Returns null when no file is under autoversion (fixed `--version`, i.e. no
4071
+ * placeholder in the generation tree) — callers keep their existing behavior.
4072
+ */
4073
+ async computeAutoversionNeutralizedRebase(files, currentGenSha) {
4074
+ const genByFile = /* @__PURE__ */ new Map();
4075
+ let anyPlaceholder = false;
4076
+ for (const file of files) {
4077
+ const gen = await this.git.showFile(currentGenSha, file).catch(() => null);
4078
+ genByFile.set(file, gen);
4079
+ if (containsPlaceholder(gen)) anyPlaceholder = true;
4080
+ }
4081
+ if (!anyPlaceholder) return null;
4082
+ const snapshot = {};
4083
+ const fragments = [];
4084
+ for (const file of files) {
4085
+ if (isBinaryFile(file)) continue;
4086
+ const disk = await (0, import_promises6.readFile)((0, import_node_path9.join)(this.outputDir, file), "utf-8").catch(() => null);
4087
+ if (disk == null) continue;
4088
+ const gen = genByFile.get(file) ?? null;
4089
+ const neutralized = gen != null ? neutralizeAutoversionTheirs(gen, disk) : disk;
4090
+ snapshot[file] = neutralized;
4091
+ if (gen == null) {
4092
+ fragments.push(synthesizeNewFileDiff(file, neutralized));
4093
+ } else if (gen !== neutralized) {
4094
+ const fileDiff = await unifiedDiff(gen, neutralized, file);
4095
+ if (fileDiff.trim()) fragments.push(fileDiff);
4096
+ }
4097
+ }
4098
+ return { diff: fragments.join(""), snapshot };
4099
+ }
4010
4100
  /**
4011
4101
  * Detects autoversion contamination — returns true when a
4012
4102
  * `[fern-autoversion]` commit between `fromSha` and `toSha` touched any