Skip to content

Commit 25c491b

Browse files
Pretty-print JSON files in apply-patchset to preserve formatting
Since read-files minifies JSON before sending to the remote workflow, the patchset content comes back without formatting. This restores readable indentation when writing JSON files back to disk. For modify actions, the existing file's indentation style (tabs vs spaces, indent width) is detected and preserved. For create actions, a default of 2-space indentation is used. Falls back to raw content if JSON parsing fails.
1 parent 4f09203 commit 25c491b

File tree

1 file changed

+91
-2
lines changed

1 file changed

+91
-2
lines changed

src/lib/init/local-ops.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,61 @@ import type {
3737
WizardOptions,
3838
} from "./types.js";
3939

40+
/** Whitespace characters used for JSON indentation. */
41+
const Indenter = {
42+
SPACE: " ",
43+
TAB: "\t",
44+
} as const;
45+
46+
/** Describes the indentation style of a JSON file. */
47+
type JsonIndent = {
48+
/** The whitespace character used for indentation. */
49+
replacer: (typeof Indenter)[keyof typeof Indenter];
50+
/** How many times the replacer is repeated per indent level. */
51+
length: number;
52+
};
53+
54+
const DEFAULT_JSON_INDENT: JsonIndent = {
55+
replacer: Indenter.SPACE,
56+
length: 2,
57+
};
58+
59+
/** Matches the first indented line in a string to detect whitespace style. */
60+
const INDENT_PATTERN = /^(\s+)/m;
61+
62+
/**
63+
* Detect the indentation style of a JSON string by inspecting the first
64+
* indented line. Returns a default of 2 spaces if no indentation is found.
65+
*/
66+
function detectJsonIndent(content: string): JsonIndent {
67+
const match = content.match(INDENT_PATTERN);
68+
if (!match?.[1]) {
69+
return DEFAULT_JSON_INDENT;
70+
}
71+
const indent = match[1];
72+
if (indent.includes("\t")) {
73+
return { replacer: Indenter.TAB, length: indent.length };
74+
}
75+
return { replacer: Indenter.SPACE, length: indent.length };
76+
}
77+
78+
/** Build the third argument for `JSON.stringify` from a `JsonIndent`. */
79+
function jsonIndentArg(indent: JsonIndent): string {
80+
return indent.replacer.repeat(indent.length);
81+
}
82+
83+
/**
84+
* Pretty-print a JSON string using the given indentation style.
85+
* Returns the original string if it cannot be parsed as valid JSON.
86+
*/
87+
function prettyPrintJson(content: string, indent: JsonIndent): string {
88+
try {
89+
return `${JSON.stringify(JSON.parse(content), null, jsonIndentArg(indent))}\n`;
90+
} catch {
91+
return content;
92+
}
93+
}
94+
4095
/**
4196
* Shell metacharacters that enable chaining, piping, substitution, or redirection.
4297
* All legitimate install commands are simple single commands that don't need these.
@@ -511,6 +566,28 @@ function applyPatchsetDryRun(payload: ApplyPatchsetPayload): LocalOpResult {
511566
return { ok: true, data: { applied } };
512567
}
513568

569+
/**
570+
* Resolve the final file content for a patch, pretty-printing JSON files
571+
* to preserve readable formatting. For `modify` actions, the existing file's
572+
* indentation style is detected and preserved. For `create` actions, a default
573+
* of 2-space indentation is used.
574+
*/
575+
function resolvePatchContent(
576+
absPath: string,
577+
filePath: string,
578+
rawContent: string,
579+
action: string
580+
): string {
581+
if (!filePath.endsWith(".json")) {
582+
return rawContent;
583+
}
584+
if (action === "modify") {
585+
const existing = fs.readFileSync(absPath, "utf-8");
586+
return prettyPrintJson(rawContent, detectJsonIndent(existing));
587+
}
588+
return prettyPrintJson(rawContent, DEFAULT_JSON_INDENT);
589+
}
590+
514591
function applyPatchset(
515592
payload: ApplyPatchsetPayload,
516593
dryRun?: boolean
@@ -542,7 +619,13 @@ function applyPatchset(
542619
case "create": {
543620
const dir = path.dirname(absPath);
544621
fs.mkdirSync(dir, { recursive: true });
545-
fs.writeFileSync(absPath, patch.patch, "utf-8");
622+
const content = resolvePatchContent(
623+
absPath,
624+
patch.path,
625+
patch.patch,
626+
patch.action
627+
);
628+
fs.writeFileSync(absPath, content, "utf-8");
546629
applied.push({ path: patch.path, action: "create" });
547630
break;
548631
}
@@ -554,7 +637,13 @@ function applyPatchset(
554637
data: { applied },
555638
};
556639
}
557-
fs.writeFileSync(absPath, patch.patch, "utf-8");
640+
const content = resolvePatchContent(
641+
absPath,
642+
patch.path,
643+
patch.patch,
644+
patch.action
645+
);
646+
fs.writeFileSync(absPath, content, "utf-8");
558647
applied.push({ path: patch.path, action: "modify" });
559648
break;
560649
}

0 commit comments

Comments
 (0)