json5-manifest-sync 0.1.3

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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [0.1.2] - 2026-02-25
6
+
7
+ ### Fixed
8
+
9
+ - Fixed CLI entrypoint detection so execution works through npm/yarn/pnpm `.bin` symlinks and wrappers.
10
+ - Fixed `npm run sync:json5` no-op behavior seen in consuming repositories installed from git dependencies.
11
+ - Kept direct invocation behavior intact for `node dist/json5-manifest-sync.js`.
12
+
13
+ ### Tests
14
+
15
+ - Added coverage for direct invocation path detection.
16
+ - Added coverage for symlinked `.bin`-style invocation path detection.
17
+ - Added regression test proving `runCli` updates `package.json5` from canonical `package.json`.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # json5-manifest-sync
2
+
3
+ Keep a documented `package.json5` in sync with the real `package.json` used by Node and package managers.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install github:BBaysinger/json5-manifest-sync#v0.1.3
9
+ ```
10
+
11
+ Add a script in your project's `package.json`:
12
+
13
+ ```json
14
+ {
15
+ "scripts": {
16
+ "sync:json5": "json5-manifest-sync"
17
+ }
18
+ }
19
+ ```
20
+
21
+ Run it:
22
+
23
+ ```bash
24
+ npm run sync:json5
25
+ ```
26
+
27
+ ## Why this exists
28
+
29
+ `package.json` must be strict JSON, which means comments are invalid. That makes it hard to document complex scripts, dependency choices, or workspace settings directly in the manifest.
30
+
31
+ `package.json` is arguably the most important file in most JavaScript/TypeScript repositories. It drives install behavior, scripts, dependency policy, packaging, and release workflows—so it deserves clear inline documentation.
32
+
33
+ At the same time, Node/npm/pnpm/yarn do not treat `package.json5` as a package manifest source. A JSON5 file can be great for human-readable documentation, but tooling still requires `package.json`.
34
+
35
+ > [!NOTE]
36
+ > It would be great to see first-class support for JSON5-style manifests in npm/tooling over time, but today this project provides a practical bridge.
37
+
38
+ This tool solves that gap by letting you maintain both:
39
+
40
+ - `package.json` as the canonical, machine-consumed manifest
41
+ - `package.json5` as the human-documented companion file
42
+
43
+ Then it synchronizes `package.json5` from `package.json` while preserving mapped `//` comments where possible.
44
+
45
+ ## What it does
46
+
47
+ - Finds `package.json` files recursively (excluding `node_modules`)
48
+ - Skips paths ignored by your root `.gitignore`
49
+ - For each matching `package.json5`, rewrites values from canonical `package.json`
50
+ - Preserves/migrates mapped `//` comments for keys and supported array items
51
+ - Writes stable JSON5 formatting with trailing commas for cleaner diffs
52
+
53
+ Repository: https://github.com/BBaysinger/json5-manifest-sync
54
+
55
+ ## Author
56
+
57
+ Bradley Baysinger ([@BBaysinger](https://github.com/BBaysinger))
58
+
59
+ ## Install from GitHub
60
+
61
+ Install from a tag (recommended):
62
+
63
+ ```bash
64
+ npm install github:BBaysinger/json5-manifest-sync#v0.1.3
65
+ ```
66
+
67
+ Or from a branch during iteration:
68
+
69
+ ```bash
70
+ npm install github:BBaysinger/json5-manifest-sync#main
71
+ ```
72
+
73
+ Because this repo defines a `prepare` script, npm will build TypeScript during git-based install.
74
+
75
+ ## Use in a project
76
+
77
+ Add a script in your consuming project's `package.json`:
78
+
79
+ ```json
80
+ {
81
+ "scripts": {
82
+ "sync:json5": "json5-manifest-sync"
83
+ }
84
+ }
85
+ ```
86
+
87
+ Run it:
88
+
89
+ ```bash
90
+ npm run sync:json5
91
+ ```
92
+
93
+ ## Generated `package.json5` example
94
+
95
+ Illustrative output (trimmed):
96
+
97
+ <!-- prettier-ignore -->
98
+ ```json5
99
+ {
100
+ // Package name used by npm and consumers.
101
+ "name": "consumer-app",
102
+ // Release version (keep in sync with git tags).
103
+ "version": "1.2.3",
104
+ // Development and release scripts.
105
+ "scripts": {
106
+ // Compile TypeScript to dist/.
107
+ "build": "tsc -p tsconfig.json",
108
+ },
109
+ }
110
+ ```
111
+
112
+ For the current full output style, see this repo's live example: [`package.json5`](./package.json5).
113
+
114
+ ### Options
115
+
116
+ - `--no-empty-comment` or `--no-empty-comments`
117
+ - `--empty-comment=false`
118
+ - env var: `SYNC_JSON5_ADD_EMPTY_COMMENT=false`
119
+
120
+ ### Formatting note
121
+
122
+ Recommended: exclude `package.json5` from Prettier (for example via `.prettierignore`).
123
+
124
+ Prettier's JSON5 formatter can remove quotes from valid keys, which makes `package.json5` less similar to canonical `package.json`. Ignoring `package.json5` helps preserve intentional key/comment style and reduces avoidable drift.
125
+
126
+ > [!NOTE]
127
+ > It would be nice if Prettier provided an option to preserve quoted keys in JSON5.
128
+
129
+ ## Example dependency block in consumer
130
+
131
+ ```json
132
+ {
133
+ "dependencies": {
134
+ "json5-manifest-sync": "github:BBaysinger/json5-manifest-sync#v0.1.3"
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## Changelog
140
+
141
+ See `CHANGELOG.md` for full release history.
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * JSON5 Package Synchronization Tool
4
+ *
5
+ * Synchronizes package.json5 files with their canonical package.json counterparts.
6
+ *
7
+ * Source of truth: package.json (parsed as strict JSON).
8
+ * Output: package.json5 rewritten with stable formatting and with *some* `//` comments
9
+ * preserved/migrated onto the corresponding keys/items.
10
+ *
11
+ * Important: this script does NOT attempt to preserve arbitrary formatting (blank
12
+ * lines, exact indentation, key ordering, etc.). It regenerates the JSON5 structure
13
+ * using a consistent 2-space indent and trailing commas.
14
+ *
15
+ * This tool handles:
16
+ * - Preceding `//` comments immediately above object keys
17
+ * - Trailing `//` comments on the same line as a key/value
18
+ * - Nested objects/arrays via simple brace/bracket tracking
19
+ * - Arrays: preserves comments for string-literal items when each item is on its own line
20
+ * - Always emits trailing commas for cleaner diffs
21
+ *
22
+ * Limitations (by design / current implementation):
23
+ * - Only line comments (`//`) are recognized. Block comments (`/* ... *\/`) are ignored.
24
+ * - Trailing-comment detection is heuristic (quote counting) and is most reliable for
25
+ * double-quoted strings; unusual quoting/escaping can confuse it.
26
+ * - Array item comment mapping is primarily intended for arrays of strings; arrays of
27
+ * objects/numbers will serialize correctly but per-item comment preservation is limited.
28
+ */
29
+ export type SyncOptions = {
30
+ /**
31
+ * When true, insert a blank comment line (`//`) before any key that has no
32
+ * preceding comment captured from the existing JSON5. This helps visually
33
+ * separate entries like:
34
+ *
35
+ * "private": true,
36
+ * //
37
+ * "type": "module",
38
+ *
39
+ * Defaults to true. Can be disabled via CLI flag `--no-empty-comment` or the
40
+ * environment variable `SYNC_JSON5_ADD_EMPTY_COMMENT=false`.
41
+ */
42
+ addEmptyCommentIfMissing?: boolean;
43
+ };
44
+ /**
45
+ * Synchronizes a JSON5 file with its canonical JSON source while preserving formatting.
46
+ *
47
+ * This is the main synchronization function that:
48
+ * - Parses the existing JSON5 to extract comments and structure
49
+ * - Applies the canonical JSON data as the source of truth
50
+ * - Reconstructs the JSON5 with preserved comments and consistent formatting
51
+ * - Uses trailing commas throughout for version control benefits
52
+ *
53
+ * The process ensures that while the data stays synchronized with the canonical
54
+ * source, all the valuable documentation and formatting is maintained.
55
+ *
56
+ * @param raw - Raw contents of the existing JSON5 file
57
+ * @param source - Parsed JSON object from the canonical package.json
58
+ * @returns The updated JSON5 content as a formatted string
59
+ */
60
+ export declare function syncJson5(raw: string, source: Record<string, unknown>, options?: SyncOptions): string;
61
+ /**
62
+ * CLI entrypoint.
63
+ *
64
+ * Scans from the current working directory and synchronizes any
65
+ * `package.json5` files that have a sibling `package.json`.
66
+ *
67
+ * @param args - Optional CLI arguments (defaults to process argv slice)
68
+ */
69
+ export declare function runCli(args?: string[]): void;
70
+ /**
71
+ * Determines whether this module is being executed as a CLI entrypoint.
72
+ *
73
+ * This supports direct Node execution and npm/pnpm/yarn .bin symlink paths by
74
+ * comparing normalized real paths when possible.
75
+ *
76
+ * @param argv1 - Entry script path (typically process.argv[1])
77
+ * @param moduleUrl - URL of this module (typically import.meta.url)
78
+ * @returns true when current process should run CLI behavior
79
+ */
80
+ export declare function isCliInvocation(argv1?: string | undefined, moduleUrl?: string): boolean;
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { sync as globSync } from "glob";
6
+ import ignore from "ignore";
7
+ /**
8
+ * Quotes an object key for consistent JSON5 formatting.
9
+ * Always returns a quoted key to maintain consistency across all properties.
10
+ *
11
+ * @param key - The key to quote
12
+ * @returns The quoted key string (e.g., "scripts", "dependencies")
13
+ */
14
+ function quoteKey(key) {
15
+ return /^[$A-Z_][0-9A-Z_$]*$/i.test(key) ? `"${key}"` : JSON.stringify(key);
16
+ }
17
+ /**
18
+ * Indents a line by the specified number of spaces and trims surrounding whitespace.
19
+ * Used to maintain consistent indentation throughout the generated JSON5.
20
+ *
21
+ * @param line - The line to indent
22
+ * @param level - The number of spaces to indent (default: 0)
23
+ * @returns The properly indented line
24
+ */
25
+ function indent(line, level = 0) {
26
+ return " ".repeat(level) + line.trim();
27
+ }
28
+ /**
29
+ * Parses a JSON5 file and builds a comment map with path-based indexing.
30
+ *
31
+ * This is the core parsing function that:
32
+ * - Tracks nested object/array structure using path stacks
33
+ * - Maps comments to their associated JSON paths (e.g., "pnpm.onlyBuiltDependencies.0")
34
+ * - Handles both preceding comments (above lines) and trailing comments (same line)
35
+ * - Heuristically detects comments vs URLs containing "//" sequences
36
+ * - Supports array items with individual comments and numeric indices
37
+ *
38
+ * The comment map structure allows for precise reconstruction of the original
39
+ * formatting while applying new data from the canonical source.
40
+ *
41
+ * @param lines - Lines from the existing JSON5 file
42
+ * @returns A map of dot-notation paths to comment information
43
+ */
44
+ function buildCommentMap(lines) {
45
+ const commentsMap = {};
46
+ // State tracking for nested structure parsing.
47
+ const pathStack = [];
48
+ const arrayIndexStack = [];
49
+ let bufferedComments = [];
50
+ let insideArray = false;
51
+ /**
52
+ * Extracts trailing comments from a line while correctly handling URLs.
53
+ *
54
+ * This function attempts to distinguish actual comments vs "//" sequences that
55
+ * appear inside string values (like URLs) using a lightweight heuristic.
56
+ *
57
+ * Notes:
58
+ * - It is designed for typical package.json(5) formatting (double-quoted strings).
59
+ * - It does not implement full JSON5 tokenization; unusual quoting/escaping may
60
+ * lead to false positives/negatives.
61
+ *
62
+ * Examples:
63
+ * - `"url": "https://github.com/user/repo", // This is a comment` ✅
64
+ * - `"url": "https://github.com/user/repo"` (no comment) ✅
65
+ *
66
+ * @param line - The line to analyze for trailing comments
67
+ * @returns The trailing comment string (including "//") or undefined
68
+ */
69
+ function extractTrailingComment(line) {
70
+ let searchStart = 0;
71
+ while (true) {
72
+ const commentIdx = line.indexOf("//", searchStart);
73
+ if (commentIdx === -1)
74
+ return undefined;
75
+ // Count quotes before this "//" to determine if we're inside a string
76
+ const beforeComment = line.slice(0, commentIdx);
77
+ const quoteCount = (beforeComment.match(/"/g) || []).length;
78
+ // Even number of quotes = outside string = actual comment
79
+ if (quoteCount % 2 === 0) {
80
+ return line.slice(commentIdx).trim();
81
+ }
82
+ // Odd number = inside string (like URL), keep searching
83
+ searchStart = commentIdx + 2;
84
+ }
85
+ }
86
+ // Process each line and associate comments with object/array paths.
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const line = lines[i];
89
+ const trimmed = line.trim();
90
+ // Skip empty lines
91
+ if (!trimmed)
92
+ continue;
93
+ // Collect preceding comments (lines that start with //)
94
+ if (trimmed.startsWith("//")) {
95
+ bufferedComments.push(line);
96
+ continue;
97
+ }
98
+ // Handle array start patterns: "key": [
99
+ const arrayStartMatch = line.match(/^\s*(["']?)([^"']+)\1\s*:\s*\[\s*$/);
100
+ if (arrayStartMatch) {
101
+ const key = arrayStartMatch[2];
102
+ const fullPath = [...pathStack, key].join(".");
103
+ const commentInfo = {
104
+ preceding: [...bufferedComments],
105
+ };
106
+ const trailing = extractTrailingComment(line);
107
+ if (trailing) {
108
+ commentInfo.trailing = trailing;
109
+ }
110
+ if (commentInfo.preceding.length > 0 || commentInfo.trailing) {
111
+ commentsMap[fullPath] = commentInfo;
112
+ }
113
+ pathStack.push(key);
114
+ arrayIndexStack.push(0);
115
+ insideArray = true;
116
+ bufferedComments = [];
117
+ continue;
118
+ }
119
+ // Handle array items - lines that start with quotes but contain no colon
120
+ if (insideArray && trimmed.startsWith('"') && !trimmed.includes(":")) {
121
+ const currentArrayIndex = arrayIndexStack[arrayIndexStack.length - 1];
122
+ const arrayItemPath = [...pathStack, currentArrayIndex.toString()].join(".");
123
+ const commentInfo = {
124
+ preceding: [...bufferedComments],
125
+ };
126
+ const trailing = extractTrailingComment(line);
127
+ if (trailing) {
128
+ commentInfo.trailing = trailing;
129
+ }
130
+ if (commentInfo.preceding.length > 0 || commentInfo.trailing) {
131
+ commentsMap[arrayItemPath] = commentInfo;
132
+ }
133
+ // Increment array index for next item and clear comment buffer
134
+ arrayIndexStack[arrayIndexStack.length - 1]++;
135
+ bufferedComments = [];
136
+ continue;
137
+ }
138
+ // Handle regular object keys.
139
+ const keyMatch = line.match(/^\s*(["']?)([^"']+)\1\s*:/);
140
+ if (keyMatch) {
141
+ const key = keyMatch[2];
142
+ const fullPath = [...pathStack, key].join(".");
143
+ const commentInfo = {
144
+ preceding: [...bufferedComments],
145
+ };
146
+ const trailing = extractTrailingComment(line);
147
+ if (trailing) {
148
+ commentInfo.trailing = trailing;
149
+ }
150
+ if (commentInfo.preceding.length > 0 || commentInfo.trailing) {
151
+ commentsMap[fullPath] = commentInfo;
152
+ }
153
+ bufferedComments = [];
154
+ // If this is an object start, update the path stack
155
+ if (trimmed.endsWith("{")) {
156
+ pathStack.push(key);
157
+ insideArray = false;
158
+ }
159
+ }
160
+ else if (bufferedComments.length > 0 && trimmed !== "") {
161
+ // Non-comment, non-key lines break the comment block
162
+ bufferedComments = [];
163
+ }
164
+ // Handle closing brackets to maintain proper nesting.
165
+ if (trimmed === "}" ||
166
+ trimmed === "}," ||
167
+ trimmed === "]" ||
168
+ trimmed === "],") {
169
+ if (trimmed.startsWith("]")) {
170
+ insideArray = false;
171
+ if (arrayIndexStack.length > 0) {
172
+ arrayIndexStack.pop();
173
+ }
174
+ }
175
+ if (pathStack.length)
176
+ pathStack.pop();
177
+ }
178
+ }
179
+ return commentsMap;
180
+ }
181
+ /**
182
+ * Generates a simple blank comment used as a visual spacer when enabled.
183
+ *
184
+ * @returns A blank comment string
185
+ */
186
+ function generateScriptComment() {
187
+ return "//";
188
+ }
189
+ /**
190
+ * Synchronizes a JSON5 file with its canonical JSON source while preserving formatting.
191
+ *
192
+ * This is the main synchronization function that:
193
+ * - Parses the existing JSON5 to extract comments and structure
194
+ * - Applies the canonical JSON data as the source of truth
195
+ * - Reconstructs the JSON5 with preserved comments and consistent formatting
196
+ * - Uses trailing commas throughout for version control benefits
197
+ *
198
+ * The process ensures that while the data stays synchronized with the canonical
199
+ * source, all the valuable documentation and formatting is maintained.
200
+ *
201
+ * @param raw - Raw contents of the existing JSON5 file
202
+ * @param source - Parsed JSON object from the canonical package.json
203
+ * @returns The updated JSON5 content as a formatted string
204
+ */
205
+ export function syncJson5(raw, source, options = { addEmptyCommentIfMissing: true }) {
206
+ const lines = raw.split("\n");
207
+ const comments = buildCommentMap(lines);
208
+ const addEmptyCommentIfMissing = options.addEmptyCommentIfMissing !== undefined
209
+ ? options.addEmptyCommentIfMissing
210
+ : true;
211
+ /**
212
+ * Recursively writes an object in normalized JSON5 style with mapped comments.
213
+ *
214
+ * @param obj - Object subtree from canonical JSON
215
+ * @param path - Dot-notation path for comment lookups
216
+ * @param level - Current indentation level in spaces
217
+ * @returns Formatted output lines for this subtree
218
+ */
219
+ function writeObject(obj, path = [], level = 2) {
220
+ const result = [];
221
+ // Type guard to ensure obj is an object
222
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
223
+ return result;
224
+ }
225
+ const keys = Object.keys(obj);
226
+ const indentSize = 2;
227
+ for (let i = 0; i < keys.length; i++) {
228
+ const key = keys[i];
229
+ const fullPath = [...path, key].join(".");
230
+ const val = obj[key];
231
+ const comma = ","; // Always add trailing comma for version control benefits
232
+ const commentInfo = comments[fullPath];
233
+ // Add any preceding comments above this key
234
+ if (commentInfo?.preceding && commentInfo.preceding.length > 0) {
235
+ for (const c of commentInfo.preceding) {
236
+ result.push(indent(c, level));
237
+ }
238
+ }
239
+ else if (addEmptyCommentIfMissing &&
240
+ !comments.hasOwnProperty(fullPath)) {
241
+ // No preceding comment captured — optionally add a blank one for spacing
242
+ // Applies to any section, not just scripts
243
+ const autoComment = generateScriptComment();
244
+ result.push(indent(autoComment, level));
245
+ }
246
+ if (typeof val === "object" && val !== null && !Array.isArray(val)) {
247
+ // Handle nested objects with optional trailing comment on opening brace
248
+ const openLine = `${quoteKey(key)}: {`;
249
+ const withTrailing = commentInfo?.trailing
250
+ ? `${openLine} ${commentInfo.trailing}`
251
+ : openLine;
252
+ result.push(indent(withTrailing, level));
253
+ // Recursively process nested object
254
+ result.push(...writeObject(val, [...path, key], level + indentSize));
255
+ result.push(indent(`}${comma}`, level));
256
+ }
257
+ else if (Array.isArray(val)) {
258
+ // Handle arrays with optional trailing comment on opening bracket
259
+ const openLine = `${quoteKey(key)}: [`;
260
+ const withTrailing = commentInfo?.trailing
261
+ ? `${openLine} ${commentInfo.trailing}`
262
+ : openLine;
263
+ result.push(indent(withTrailing, level));
264
+ // Process each array item with individual comments
265
+ for (let j = 0; j < val.length; j++) {
266
+ const itemPath = [...path, key, j.toString()].join(".");
267
+ const itemCommentInfo = comments[itemPath];
268
+ // Add preceding comments for this array item
269
+ if (itemCommentInfo?.preceding) {
270
+ for (const c of itemCommentInfo.preceding) {
271
+ result.push(indent(c, level + indentSize));
272
+ }
273
+ }
274
+ else if (addEmptyCommentIfMissing &&
275
+ !comments.hasOwnProperty(itemPath)) {
276
+ const autoComment = generateScriptComment();
277
+ result.push(indent(autoComment, level + indentSize));
278
+ }
279
+ // Add array item with optional trailing comment
280
+ const itemLine = `${JSON.stringify(val[j])},`;
281
+ const withTrailing = itemCommentInfo?.trailing
282
+ ? `${itemLine} ${itemCommentInfo.trailing}`
283
+ : itemLine;
284
+ result.push(indent(withTrailing, level + indentSize));
285
+ }
286
+ result.push(indent(`]${comma}`, level));
287
+ }
288
+ else {
289
+ // Handle primitive values (strings, numbers, booleans) with optional trailing comment
290
+ const valueLine = `${quoteKey(key)}: ${JSON.stringify(val)}${comma}`;
291
+ const withTrailing = commentInfo?.trailing
292
+ ? `${valueLine} ${commentInfo.trailing}`
293
+ : valueLine;
294
+ result.push(indent(withTrailing, level));
295
+ }
296
+ }
297
+ return result;
298
+ }
299
+ // Generate the complete JSON5 structure
300
+ const generated = writeObject(source, [], 2);
301
+ return `{\n${generated.join("\n")}\n}`;
302
+ }
303
+ // 🔁 Main Execution Runner
304
+ //
305
+ // This section handles the file discovery and processing logic:
306
+ // - Finds all package.json files in the project (excluding node_modules)
307
+ // - Checks for corresponding package.json5 files
308
+ // - Applies root .gitignore rules to skip excluded files
309
+ // - Performs synchronization for each valid pair
310
+ /**
311
+ * CLI entrypoint.
312
+ *
313
+ * Scans from the current working directory and synchronizes any
314
+ * `package.json5` files that have a sibling `package.json`.
315
+ *
316
+ * @param args - Optional CLI arguments (defaults to process argv slice)
317
+ */
318
+ export function runCli(args = process.argv.slice(2)) {
319
+ const rootDir = process.cwd();
320
+ const ig = ignore();
321
+ const gitignorePath = path.join(rootDir, ".gitignore");
322
+ // Load gitignore rules if available
323
+ if (fs.existsSync(gitignorePath)) {
324
+ ig.add(fs.readFileSync(gitignorePath, "utf8"));
325
+ }
326
+ // Find all package.json files recursively, excluding node_modules
327
+ const packageJsonPaths = globSync("**/package.json", {
328
+ cwd: rootDir,
329
+ ignore: ["**/node_modules/**"],
330
+ absolute: true,
331
+ });
332
+ // Parse simple CLI flags / env for options
333
+ let addEmptyCommentIfMissing = true;
334
+ // Env toggle: SYNC_JSON5_ADD_EMPTY_COMMENT=false
335
+ if (typeof process.env.SYNC_JSON5_ADD_EMPTY_COMMENT === "string" &&
336
+ process.env.SYNC_JSON5_ADD_EMPTY_COMMENT.toLowerCase() === "false") {
337
+ addEmptyCommentIfMissing = false;
338
+ }
339
+ // CLI toggles: --no-empty-comment, --empty-comment=false
340
+ for (const arg of args) {
341
+ if (arg === "--no-empty-comment" || arg === "--no-empty-comments") {
342
+ addEmptyCommentIfMissing = false;
343
+ }
344
+ else if (arg.startsWith("--empty-comment=")) {
345
+ const v = arg.split("=", 2)[1]?.toLowerCase();
346
+ if (v === "false" || v === "0" || v === "no") {
347
+ addEmptyCommentIfMissing = false;
348
+ }
349
+ }
350
+ }
351
+ // Process each package.json to sync with its package.json5 counterpart
352
+ for (const packageJsonPath of packageJsonPaths) {
353
+ const relPath = path.relative(rootDir, packageJsonPath);
354
+ // Skip files that match gitignore patterns
355
+ if (ig.ignores(relPath))
356
+ continue;
357
+ const json5Path = packageJsonPath + "5"; // package.json5
358
+ // Skip if no corresponding package.json5 exists
359
+ if (!fs.existsSync(json5Path)) {
360
+ console.info(`⏩ Skipping ${relPath} — no package.json5 found`);
361
+ continue;
362
+ }
363
+ // Perform the synchronization
364
+ const rawJson5 = fs.readFileSync(json5Path, "utf8");
365
+ const sourceJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
366
+ const updated = syncJson5(rawJson5, sourceJson, {
367
+ addEmptyCommentIfMissing,
368
+ });
369
+ // Write the synchronized result
370
+ fs.writeFileSync(json5Path, updated);
371
+ console.info(`✅ Synced ${relPath} → ${path.basename(json5Path)}`);
372
+ }
373
+ }
374
+ /**
375
+ * Determines whether this module is being executed as a CLI entrypoint.
376
+ *
377
+ * This supports direct Node execution and npm/pnpm/yarn .bin symlink paths by
378
+ * comparing normalized real paths when possible.
379
+ *
380
+ * @param argv1 - Entry script path (typically process.argv[1])
381
+ * @param moduleUrl - URL of this module (typically import.meta.url)
382
+ * @returns true when current process should run CLI behavior
383
+ */
384
+ export function isCliInvocation(argv1 = process.argv[1], moduleUrl = import.meta.url) {
385
+ if (!argv1)
386
+ return false;
387
+ const modulePath = fileURLToPath(moduleUrl);
388
+ const entryPath = path.resolve(argv1);
389
+ try {
390
+ return fs.realpathSync(entryPath) === fs.realpathSync(modulePath);
391
+ }
392
+ catch {
393
+ // Fall back to normalized absolute-path comparison if realpath fails.
394
+ return path.resolve(entryPath) === path.resolve(modulePath);
395
+ }
396
+ }
397
+ if (isCliInvocation()) {
398
+ runCli();
399
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "json5-manifest-sync",
3
+ "version": "0.1.3",
4
+ "description": "Sync package.json5 files from canonical package.json while preserving key comments.",
5
+ "author": {
6
+ "name": "Bradley Baysinger",
7
+ "url": "https://github.com/BBaysinger"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/json5-manifest-sync.js",
11
+ "types": "./dist/json5-manifest-sync.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/json5-manifest-sync.d.ts",
15
+ "default": "./dist/json5-manifest-sync.js"
16
+ }
17
+ },
18
+ "bin": {
19
+ "json5-manifest-sync": "./dist/json5-manifest-sync.js"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "format": "prettier . --write",
29
+ "format:check": "prettier . --check",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "prepare": "npm run build"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "glob": "^11.0.1",
39
+ "ignore": "^7.0.4"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.13.10",
43
+ "prettier": "^3.8.1",
44
+ "typescript": "^5.8.2",
45
+ "vitest": "^2.1.9"
46
+ }
47
+ }