json5-manifest-sync 0.2.1 → 0.3.0

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 CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.3.0] - 2026-11-02
6
+
7
+ ### Added
8
+
9
+ - Added bidirectional synchronization with configurable `package.json` or `package.json5` source of truth.
10
+ - Added `JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH` support in `.env` and the process environment.
11
+ - Added JSON5-only project discovery and generation of a strict-JSON `package.json`.
12
+
13
+ ### Fixed
14
+
15
+ - Report invalid source-of-truth environment values with a descriptive error.
16
+ - Skip writes when synchronization produces unchanged content.
17
+
18
+ ## [0.2.2] - 2026-05-13
19
+
20
+ ### Changed
21
+
22
+ - Refined README wording so the npm package page explains sync behavior, blank-comment modes, and current limitations more clearly.
23
+ - Added npm package discoverability improvements, including package metadata fields and a top-of-page npm badge/link.
24
+
5
25
  ## [0.2.1] - 2026-05-06
6
26
 
7
27
  ### Changed
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # json5-manifest-sync
2
2
 
3
- Keep a documented `package.json5` in sync with the real `package.json` used by Node and package managers, including `version` updates.
3
+ Keep `package.json` and a documented `package.json5` in sync. Choose either file as the source of truth: maintain comments in `package.json5` and generate strict JSON for tools that require it, or update the JSON5 companion from `package.json`.
4
+
5
+ pnpm supports `package.json5` natively, but other tools in a project’s scripts, CI/CD, publishing, or deployment workflows may still expect `package.json`. When both files exist, pnpm prefers `package.json`; it reads `package.json5` natively when `package.json` is absent. This tool bridges mixed toolchains by keeping both manifests synchronized in either direction.
4
6
 
5
7
  ## Quick Start
6
8
 
@@ -30,27 +32,31 @@ npm run sync:json5
30
32
 
31
33
  `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
34
 
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`.
35
+ Some tooling does not treat `package.json5` as a package manifest source. While pnpm supports it in supported workflows, npm, Node-based tooling, legacy CI/CD environments, deployment systems, release tooling, and other ecosystem integrations may still require the conventional `package.json` filename. A companion `package.json5` approach provides documented manifests without giving up broad compatibility.
36
+
37
+ This is also why the long-running Yarn Berry discussion about comment-friendly manifests is relevant here: [yarnpkg/berry#241](https://github.com/yarnpkg/berry/issues/241) describes the same underlying need, along with the ecosystem compatibility constraints that make a companion `package.json5` approach useful today.
34
38
 
35
39
  > [!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.
40
+ > pnpm's native `package.json5` support is useful when pnpm controls the workflow, but it does not make `package.json5` universally recognized across npm, CI/CD, deployment, release, and other JavaScript tooling. This project keeps `package.json` available for compatibility while providing a documented companion file.
37
41
 
38
- This tool solves that gap by letting you maintain both:
42
+ The source of truth is configurable. By default:
39
43
 
40
- - `package.json` as the canonical, machine-consumed manifest
41
- - `package.json5` as the human-documented companion file
44
+ - `package.json` is authoritative for manifest values.
45
+ - `package.json5` is authoritative for comments; values are synchronized from `package.json` while mapped `//` comments are preserved where possible.
42
46
 
43
- Then it synchronizes `package.json5` from `package.json` while preserving mapped `//` comments where possible.
44
- When values like `version`, `scripts`, or dependency versions change in `package.json`, those updates are propagated into `package.json5`.
47
+ Alternatively, make `package.json5` authoritative for values and generate a strict `package.json` for tools that need it. In this direction, comments remain in `package.json5`; they are not copied to `package.json`.
45
48
 
46
49
  ## What it does
47
50
 
48
51
  - Finds `package.json` files recursively (excluding `node_modules`)
49
52
  - Skips paths ignored by your root `.gitignore`
50
- - For each matching `package.json5`, rewrites values from canonical `package.json` (including `version`, scripts, dependencies, and other manifest fields)
53
+ - Synchronizes each matching `package.json` / `package.json5` pair in either direction
54
+ - By default, rewrites `package.json5` values from `package.json` (including `version`, scripts, dependencies, and other manifest fields)
55
+ - With `--source=package.json5`, parses JSON5 and creates or updates a strict-JSON `package.json`
51
56
  - Preserves/migrates mapped `//` comments for keys and supported array items
52
57
  - Supports three blank-comment modes: `preserve`, `fill`, and `remove`
53
58
  - Writes stable JSON5 formatting with trailing commas for cleaner diffs
59
+ - Keeps a documented `package.json5` synchronized while retaining `package.json` compatibility for npm, legacy CI/CD, and other tooling that requires it
54
60
 
55
61
  ### Current limitations
56
62
 
@@ -129,6 +135,29 @@ For the current full output style, see this repo's live example: [`package.json5
129
135
 
130
136
  By default, the tool runs with `--blank-comments=preserve`.
131
137
 
138
+ Choose which manifest is authoritative with:
139
+
140
+ - `--source=package.json` (default): update `package.json5` from `package.json`, preserving mapped JSON5 comments.
141
+ - `--source=package.json5`: create or update `package.json` from `package.json5`; comments remain only in the JSON5 file.
142
+
143
+ To retain the choice across runs, set `JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH` in the project-root `.env` file:
144
+
145
+ ```dotenv
146
+ JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH=package.json5
147
+ ```
148
+
149
+ An existing shell environment variable takes precedence over `.env`, and `--source=...` takes precedence over both. If the setting is omitted, the default remains `package.json`.
150
+
151
+ For a project that has only `package.json5`, run the tool through `npx` (or install it globally) to generate the initial `package.json`—no pnpm installation is needed:
152
+
153
+ ```bash
154
+ npx --yes --package=json5-manifest-sync json5-manifest-sync --source=package.json5
155
+ ```
156
+
157
+ You cannot use a local `npm run` script or install this tool as a local dev dependency until `package.json` exists. Once generated, npm and other strict-JSON tools can use it. Note that commands such as `npm install` update `package.json`; reconcile those changes back into `package.json5` with the default `--source=package.json` mode before regenerating JSON from the JSON5 source, or those updates could be overwritten.
158
+
159
+ Because pnpm prefers `package.json` when both files exist, it will read the generated JSON copy in that setup; it reads `package.json5` directly only when `package.json` is absent.
160
+
132
161
  Choose a blank-comment mode with one of the following:
133
162
 
134
163
  - `--blank-comments=preserve`
@@ -161,7 +190,7 @@ Example prompt:
161
190
  ```json
162
191
  {
163
192
  "dependencies": {
164
- "json5-manifest-sync": "^0.1.3"
193
+ "json5-manifest-sync": "^0.3.0"
165
194
  }
166
195
  }
167
196
  ```
@@ -2,11 +2,11 @@
2
2
  /**
3
3
  * JSON5 Package Synchronization Tool
4
4
  *
5
- * Synchronizes package.json5 files with their canonical package.json counterparts.
5
+ * Synchronizes paired package.json and package.json5 manifests in either direction.
6
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.
7
+ * By default, package.json is the source of manifest values and package.json5 is
8
+ * rewritten with stable formatting and with *some* `//` comments preserved/migrated.
9
+ * The CLI can instead use package.json5 as the source and generate package.json.
10
10
  *
11
11
  * Important: this script does NOT attempt to preserve arbitrary formatting (blank
12
12
  * lines, exact indentation, key ordering, etc.). It regenerates the JSON5 structure
@@ -27,6 +27,8 @@
27
27
  * objects/numbers will serialize correctly but per-item comment preservation is limited.
28
28
  */
29
29
  export type BlankCommentMode = "preserve" | "fill" | "remove";
30
+ export declare const manifestSourceOptions: readonly ["package.json", "package.json5"];
31
+ export type ManifestSource = (typeof manifestSourceOptions)[number];
30
32
  export type SyncOptions = {
31
33
  /**
32
34
  * Controls how blank comment lines (`//`) are handled.
@@ -59,8 +61,8 @@ export declare function syncJson5(raw: string, source: Record<string, unknown>,
59
61
  /**
60
62
  * CLI entrypoint.
61
63
  *
62
- * Scans from the current working directory and synchronizes any
63
- * `package.json5` files that have a sibling `package.json`.
64
+ * Scans from the current working directory and synchronizes paired
65
+ * `package.json` and `package.json5` files in the selected direction.
64
66
  *
65
67
  * @param args - Optional CLI arguments (defaults to process argv slice)
66
68
  */
@@ -2,8 +2,25 @@
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import JSON5 from "json5";
6
+ import { parse as parseDotenv } from "dotenv";
5
7
  import { sync as globSync } from "glob";
6
8
  import ignore from "ignore";
9
+ export const manifestSourceOptions = ["package.json", "package.json5"];
10
+ function isManifestSource(value) {
11
+ return manifestSourceOptions.includes(value);
12
+ }
13
+ function describeManifestSources() {
14
+ return manifestSourceOptions.map((source) => `"${source}"`).join(" or ");
15
+ }
16
+ function writeFileIfChanged(filePath, content) {
17
+ if (fs.existsSync(filePath) &&
18
+ fs.readFileSync(filePath, "utf8") === content) {
19
+ return false;
20
+ }
21
+ fs.writeFileSync(filePath, content);
22
+ return true;
23
+ }
7
24
  function isBlankCommentLine(line) {
8
25
  return /^\s*\/\/\s*$/.test(line);
9
26
  }
@@ -328,16 +345,16 @@ export function syncJson5(raw, source, options = { blankCommentMode: "preserve"
328
345
  }
329
346
  // 🔁 Main Execution Runner
330
347
  //
331
- // This section handles the file discovery and processing logic:
332
- // - Finds all package.json files in the project (excluding node_modules)
333
- // - Checks for corresponding package.json5 files
348
+ // This section handles source-specific file discovery and processing:
349
+ // - Finds the selected manifest type in the project (excluding node_modules)
350
+ // - Checks for the counterpart when package.json is the source
334
351
  // - Applies root .gitignore rules to skip excluded files
335
352
  // - Performs synchronization for each valid pair
336
353
  /**
337
354
  * CLI entrypoint.
338
355
  *
339
- * Scans from the current working directory and synchronizes any
340
- * `package.json5` files that have a sibling `package.json`.
356
+ * Scans from the current working directory and synchronizes paired
357
+ * `package.json` and `package.json5` files in the selected direction.
341
358
  *
342
359
  * @param args - Optional CLI arguments (defaults to process argv slice)
343
360
  */
@@ -349,15 +366,31 @@ export function runCli(args = process.argv.slice(2)) {
349
366
  if (fs.existsSync(gitignorePath)) {
350
367
  ig.add(fs.readFileSync(gitignorePath, "utf8"));
351
368
  }
352
- // Find all package.json files recursively, excluding node_modules
353
- const packageJsonPaths = globSync("**/package.json", {
354
- cwd: rootDir,
355
- ignore: ["**/node_modules/**"],
356
- absolute: true,
357
- });
358
369
  // Parse CLI flags for options
359
370
  let blankCommentMode = "preserve";
360
371
  let warnOnBlankComments = false;
372
+ const dotenvPath = path.join(rootDir, ".env");
373
+ const dotenvValues = fs.existsSync(dotenvPath)
374
+ ? parseDotenv(fs.readFileSync(dotenvPath))
375
+ : {};
376
+ const sourceSettings = [
377
+ {
378
+ value: dotenvValues.JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH,
379
+ location: ".env",
380
+ },
381
+ {
382
+ value: process.env.JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH,
383
+ location: "the process environment",
384
+ },
385
+ ];
386
+ const invalidSourceSetting = sourceSettings.find(({ value }) => value !== undefined && !isManifestSource(value));
387
+ if (invalidSourceSetting) {
388
+ throw new Error(`Invalid JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH in ${invalidSourceSetting.location}: "${invalidSourceSetting.value}". Expected ${describeManifestSources()}.`);
389
+ }
390
+ const configuredSource = process.env.JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH ??
391
+ dotenvValues.JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH ??
392
+ "package.json";
393
+ let sourceOverride;
361
394
  for (const arg of args) {
362
395
  if (arg.startsWith("--blank-comments=")) {
363
396
  const v = arg.split("=", 2)[1]?.toLowerCase();
@@ -365,33 +398,69 @@ export function runCli(args = process.argv.slice(2)) {
365
398
  blankCommentMode = v;
366
399
  }
367
400
  }
401
+ else if (arg.startsWith("--source=")) {
402
+ const v = arg.slice("--source=".length);
403
+ if (isManifestSource(v)) {
404
+ sourceOverride = v;
405
+ }
406
+ else {
407
+ console.error(`Invalid source "${v}". Use --source=${manifestSourceOptions.join(" or --source=")}.`);
408
+ return;
409
+ }
410
+ }
368
411
  else if (arg === "--warn-blank-comments") {
369
412
  warnOnBlankComments = true;
370
413
  }
371
414
  }
372
- // Process each package.json to sync with its package.json5 counterpart
373
- for (const packageJsonPath of packageJsonPaths) {
374
- const relPath = path.relative(rootDir, packageJsonPath);
415
+ const configuredSourceValue = sourceOverride ?? configuredSource;
416
+ if (!isManifestSource(configuredSourceValue)) {
417
+ console.error(`Invalid JSON5_MANIFEST_SYNC_SOURCE_OF_TRUTH "${configuredSourceValue}". Use ${manifestSourceOptions.join(" or ")}.`);
418
+ return;
419
+ }
420
+ const source = configuredSourceValue;
421
+ const sourcePattern = source === "package.json" ? "**/package.json" : "**/package.json5";
422
+ const sourcePaths = globSync(sourcePattern, {
423
+ cwd: rootDir,
424
+ ignore: ["**/node_modules/**"],
425
+ absolute: true,
426
+ });
427
+ // Process each manifest using the selected source of truth.
428
+ for (const sourcePath of sourcePaths) {
429
+ const relPath = path.relative(rootDir, sourcePath);
375
430
  // Skip files that match gitignore patterns
376
431
  if (ig.ignores(relPath))
377
432
  continue;
378
- const json5Path = packageJsonPath + "5"; // package.json5
379
- // Skip if no corresponding package.json5 exists
380
- if (!fs.existsSync(json5Path)) {
433
+ const packageJsonPath = source === "package.json"
434
+ ? sourcePath
435
+ : path.join(path.dirname(sourcePath), "package.json");
436
+ const json5Path = source === "package.json" ? `${sourcePath}5` : sourcePath;
437
+ // The default direction requires an existing JSON5 companion. In the
438
+ // reverse direction, package.json is created when it does not exist.
439
+ if (source === "package.json" && !fs.existsSync(json5Path)) {
381
440
  console.info(`⏩ Skipping ${relPath} — no package.json5 found`);
382
441
  continue;
383
442
  }
384
- // Perform the synchronization
385
- const rawJson5 = fs.readFileSync(json5Path, "utf8");
386
- const sourceJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
387
- const updated = syncJson5(rawJson5, sourceJson, {
388
- blankCommentMode,
389
- });
390
- // Write the synchronized result
391
- fs.writeFileSync(json5Path, updated);
392
- console.info(`✅ Synced ${relPath} → ${path.basename(json5Path)}`);
393
- if (warnOnBlankComments && blankCommentMode !== "remove") {
394
- warnAboutBlankComments(json5Path, collectBlankCommentLineNumbers(updated));
443
+ if (source === "package.json") {
444
+ const rawJson5 = fs.readFileSync(json5Path, "utf8");
445
+ const sourceJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
446
+ const updated = syncJson5(rawJson5, sourceJson, {
447
+ blankCommentMode,
448
+ });
449
+ const changed = writeFileIfChanged(json5Path, updated);
450
+ console.info(changed
451
+ ? `✅ Synced ${relPath} → ${path.basename(json5Path)}`
452
+ : `⏭️ Unchanged ${relPath} → ${path.basename(json5Path)}`);
453
+ if (warnOnBlankComments && blankCommentMode !== "remove") {
454
+ warnAboutBlankComments(json5Path, collectBlankCommentLineNumbers(updated));
455
+ }
456
+ }
457
+ else {
458
+ const sourceJson5 = JSON5.parse(fs.readFileSync(json5Path, "utf8"));
459
+ const updated = `${JSON.stringify(sourceJson5, null, 2)}\n`;
460
+ const changed = writeFileIfChanged(packageJsonPath, updated);
461
+ console.info(changed
462
+ ? `✅ Synced ${path.basename(json5Path)} → ${relPath}`
463
+ : `⏭️ Unchanged ${path.basename(json5Path)} → ${relPath}`);
395
464
  }
396
465
  }
397
466
  }
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "json5-manifest-sync",
3
- "version": "0.2.1",
4
- "description": "Sync package.json5 files from canonical package.json while preserving key comments.",
3
+ "version": "0.3.0",
4
+ "description": "Sync package.json and package.json5 manifests in either direction, preserving JSON5 comments where possible.",
5
5
  "author": {
6
6
  "name": "Bradley Baysinger",
7
7
  "url": "https://github.com/BBaysinger"
8
8
  },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/BBaysinger/json5-manifest-sync.git"
12
+ },
13
+ "homepage": "https://github.com/BBaysinger/json5-manifest-sync#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/BBaysinger/json5-manifest-sync/issues"
16
+ },
9
17
  "type": "module",
10
18
  "main": "./dist/json5-manifest-sync.js",
11
19
  "types": "./dist/json5-manifest-sync.d.ts",
@@ -38,8 +46,10 @@
38
46
  "node": ">=18"
39
47
  },
40
48
  "dependencies": {
49
+ "dotenv": "^18.0.10",
41
50
  "glob": "^13.0.6",
42
- "ignore": "^7.0.5"
51
+ "ignore": "^7.0.5",
52
+ "json5": "^2.2.3"
43
53
  },
44
54
  "devDependencies": {
45
55
  "@types/node": "^25.6.0",