@threenative/core 0.3.3 → 0.3.4

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.
@@ -30,14 +30,13 @@ export async function applyThreePatch(options = {}) {
30
30
  const patchPath = path.join(packageRoot, "patches", PATCH_NAME);
31
31
  const patch = parsePatch(await readFile(patchPath, "utf8"), patchPath);
32
32
  const plans = await planPatch(threeRoot, patch);
33
+ // `planPatch` refuses any file it does not fully recognise, so every plan that is not already
34
+ // `patched` is safe to write — including the files an older release of this patch already
35
+ // touched, which is the state a project upgrading from the previous `latest` is in.
33
36
  if (plans.every((plan) => plan.status === "patched")) return "unchanged";
34
- if (plans.some((plan) => plan.status !== "stock")) {
35
- throw new Error(
36
- `TN_THREE_PATCH_PARTIAL: ${manifestPath} is neither stock nor fully patched; refusing to modify a partial installation.`,
37
- );
38
- }
39
37
 
40
- for (const plan of plans) await writeFile(plan.file, plan.contents, "utf8");
38
+ for (const plan of plans)
39
+ if (plan.status !== "patched") await writeFile(plan.file, plan.contents, "utf8");
41
40
  return "patched";
42
41
  }
43
42
 
@@ -217,56 +216,81 @@ async function planPatch(threeRoot, files) {
217
216
  }
218
217
  const original = await readFile(target, "utf8");
219
218
  const lines = original.replaceAll("\r\n", "\n").split("\n");
220
- const status = classifyFile(lines, file.hunks);
219
+ const states = classifyFile(lines, file.hunks);
220
+ const unknown = states.findIndex((state) => state === "unknown");
221
+ if (unknown !== -1) {
222
+ throw new Error(
223
+ `TN_THREE_PATCH_PARTIAL: hunk ${String(unknown + 1)} of ${file.oldPath} matches neither the stock nor the patched text in ${target}; refusing to modify an installation this package does not recognise.`,
224
+ );
225
+ }
226
+ const status = states.every((state) => state === "patched")
227
+ ? "patched"
228
+ : states.some((state) => state === "patched")
229
+ ? "extended"
230
+ : "stock";
221
231
  if (status === "patched") {
222
232
  plans.push({ contents: original, file: target, status });
223
233
  continue;
224
234
  }
225
- if (status !== "stock") {
226
- throw new Error(
227
- `TN_THREE_PATCH_PARTIAL: hunk state is mixed in ${target}; refusing to modify a partial installation.`,
228
- );
229
- }
230
- plans.push({ contents: applyFile(lines, file.hunks, target).join("\n"), file: target, status });
235
+ plans.push({
236
+ contents: applyFile(lines, file.hunks, states, target).join("\n"),
237
+ file: target,
238
+ status,
239
+ });
231
240
  }
232
241
  return plans;
233
242
  }
234
243
 
244
+ /**
245
+ * Every hunk's state in this file, in patch order: `"stock"`, `"patched"` or `"unknown"`.
246
+ *
247
+ * Per hunk rather than per file, because an upgrade lands on a file an *older release of this same
248
+ * package's patch* already modified. The consumer declares the patch in its own manifest, so the
249
+ * package manager applies it at install time — meaning a project scaffolded from the previous
250
+ * `latest` carries the previous patch's text, and only this release's new hunks are still stock.
251
+ * Both states are repairable by applying the stock remainder, and the old hunks are not re-derived
252
+ * from a different patch: they are this patch's own hunks, already at their patched text.
253
+ *
254
+ * A hunk in neither state is a file this package does not recognise — hand-edited, or owned by a
255
+ * different patch — and stays the refusal it always was.
256
+ */
235
257
  function classifyFile(lines, hunks) {
236
- let stock = true;
237
- let patched = true;
258
+ const states = [];
238
259
  let offset = 0;
239
260
  for (const hunk of hunks) {
240
- const oldLines = hunk.lines
241
- .filter((line) => line[0] === " " || line[0] === "-")
242
- .map((line) => line.slice(1));
243
- const newLines = hunk.lines
244
- .filter((line) => line[0] === " " || line[0] === "+")
245
- .map((line) => line.slice(1));
246
- if (locate(lines, oldLines, hunk.oldStart - 1 + offset) === undefined) stock = false;
247
- if (locate(lines, newLines, hunk.newStart - 1) === undefined) patched = false;
261
+ const oldLines = hunkLines(hunk, "old");
262
+ const newLines = hunkLines(hunk, "new");
263
+ if (locate(lines, newLines, hunk.newStart - 1) !== undefined) states.push("patched");
264
+ else if (locate(lines, oldLines, hunk.oldStart - 1 + offset) !== undefined)
265
+ states.push("stock");
266
+ else states.push("unknown");
248
267
  offset += newLines.length - oldLines.length;
249
268
  }
250
- if (stock) return "stock";
251
- if (patched) return "patched";
252
- return "mixed";
269
+ return states;
253
270
  }
254
271
 
255
- function applyFile(lines, hunks, file) {
272
+ function hunkLines(hunk, side) {
273
+ return hunk.lines
274
+ .filter((line) =>
275
+ side === "old" ? line[0] === " " || line[0] === "-" : line[0] === " " || line[0] === "+",
276
+ )
277
+ .map((line) => line.slice(1));
278
+ }
279
+
280
+ function applyFile(lines, hunks, states, file) {
256
281
  const result = [...lines];
257
282
  let offset = 0;
258
- for (const hunk of hunks) {
259
- const oldLines = hunk.lines
260
- .filter((line) => line[0] === " " || line[0] === "-")
261
- .map((line) => line.slice(1));
262
- const newLines = hunk.lines
263
- .filter((line) => line[0] === " " || line[0] === "+")
264
- .map((line) => line.slice(1));
283
+ for (const [number, hunk] of hunks.entries()) {
284
+ const oldLines = hunkLines(hunk, "old");
285
+ const newLines = hunkLines(hunk, "new");
265
286
  const index = locate(result, oldLines, hunk.oldStart - 1 + offset);
266
- if (index === undefined) {
267
- throw new Error(`TN_THREE_PATCH_INVALID: stock hunk no longer matches ${file}.`);
287
+ if (states[number] === "stock") {
288
+ if (index === undefined)
289
+ throw new Error(`TN_THREE_PATCH_INVALID: stock hunk no longer matches ${file}.`);
290
+ result.splice(index, oldLines.length, ...newLines);
268
291
  }
269
- result.splice(index, oldLines.length, ...newLines);
292
+ // Already-patched hunks are left alone. Their line-count change is already in the file, so
293
+ // the offset still moves to keep later stock hunks near their expected location.
270
294
  offset += newLines.length - oldLines.length;
271
295
  }
272
296
  return result;