@mnci/cli 1.19.0 → 1.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/README.md CHANGED
@@ -280,7 +280,8 @@ config (`@mnci/eslint-config`'s `nonJs()`) for the file types oxlint cannot read
280
280
  | JS/TS lint | ESLint, 452 rules | oxlint, 206 native + 225 bridged |
281
281
  | YAML/TOML/MD/CSS/HTML/JSON lint | ESLint | **ESLint** (unchanged) |
282
282
  | `@nx/dependency-checks` | yes | **yes** (unchanged) |
283
- | Formatter | Prettier | oxfmt (~30x faster) |
283
+ | Formatter | Prettier | oxfmt (~6x faster on this repo) |
284
+ | Formats `.toml` | no — Prettier has no TOML parser | **yes** |
284
285
  | Config files | `eslint.config.mjs`, `.prettierrc.mjs` | `oxlint.config.ts`, `.oxfmtrc.json`, `eslint.config.mjs` |
285
286
  | VS Code extension | ESLint + Prettier | Oxc (covers both) + ESLint |
286
287
 
@@ -290,6 +291,27 @@ all reported by nothing. Switching modes with `mnci upgrade --linter=<other>`
290
291
  rewrites the config files **and deletes the mode you left**, so a workspace never
291
292
  carries two formatter configs.
292
293
 
294
+ **Exactly one formatter is declared, and that is a fix rather than tidiness.**
295
+ `prettier` under `eslint`, `oxfmt` under `oxlint`, and switching modes drops the
296
+ other from `devDependencies`. prettier stays in `node_modules` either way, since
297
+ `@mnci/eslint-config` depends on it — so what matters is the _declaration_:
298
+ `esbenp.prettier-vscode` resolves the formatter from the **project's**
299
+ dependencies, so an oxlint workspace that still declared prettier is one where a
300
+ globally installed Prettier extension reformats on save against the opinion oxfmt
301
+ is not applying, while `npm run format:check` (oxfmt) reports the result as
302
+ unformatted. Both configs valid, both binaries resolvable, neither one wrong on
303
+ its own — the `.prettierrc` precedence bug in another costume. `mnci doctor`
304
+ checks it.
305
+
306
+ **oxfmt formats `.toml`; Prettier cannot.** `prettier` on a `.toml` exits with
307
+ `No parser could be inferred for file`, so an oxlint workspace gets a formatted
308
+ `pyproject.toml` and the default cannot — the one gap
309
+ `@mnci/eslint-config`'s parser-only TOML block has always documented as
310
+ unenforceable. It is the only file type where the two formatters differ in what
311
+ they can parse, which is why `[toml]` is pinned for format-on-save under `oxlint`
312
+ only: pinning it under `eslint` would route the file to a formatter that errors
313
+ on it.
314
+
293
315
  **Finding and overriding a rule.** One root config importing a package is easy to
294
316
  own and hard to read: nothing in three lines says which of ~20 plugins reported
295
317
  the thing you disagree with. Every block in `@mnci/eslint-config` therefore
package/dist/cli.js CHANGED
@@ -80,11 +80,12 @@ function runNpx(arguments_, cwd) {
80
80
  throw new Error(`npx ${arguments_.join(" ")} failed with exit code ${status}`);
81
81
  }
82
82
  }
83
- function runPrettier(cwd, target = ".") {
84
- const status = runShell("npx", ["prettier", "--write", "--log-level", "warn", target], cwd);
83
+ function runFormatter(cwd, linter, target = ".") {
84
+ const [command, arguments_] = linter === "oxlint" ? ["oxfmt", ["--write", target]] : ["prettier", ["--write", "--log-level", "warn", target]];
85
+ const status = runShell("npx", [command, ...arguments_], cwd);
85
86
  if (status !== 0) {
86
87
  logger.warn(
87
- `Prettier could not format '${target}' (exit code ${status}). The project was generated; run 'npm run format' to normalise it.`
88
+ `${command} could not format '${target}' (exit code ${status}). The project was generated; run 'npm run format' to normalise it.`
88
89
  );
89
90
  }
90
91
  }
@@ -1215,7 +1216,7 @@ async function runAdd(kind, name, options) {
1215
1216
  case "python-vendor": {
1216
1217
  await addPythonVendor(workspaceRoot, resolvedName, options, kindProvided);
1217
1218
  syncProjectReferences(workspaceRoot);
1218
- runPrettier(workspaceRoot);
1219
+ runFormatter(workspaceRoot, stack.linter);
1219
1220
  return;
1220
1221
  }
1221
1222
  default: {
@@ -1226,7 +1227,7 @@ async function runAdd(kind, name, options) {
1226
1227
  }
1227
1228
  }
1228
1229
  syncProjectReferences(workspaceRoot);
1229
- runPrettier(workspaceRoot);
1230
+ runFormatter(workspaceRoot, stack.linter);
1230
1231
  logger.success(`Added ${resolvedKind} '${resolvedName}'.`);
1231
1232
  }
1232
1233
  function syncProjectReferences(workspaceRoot) {
@@ -1243,7 +1244,14 @@ function readWorkspaceStack(workspaceRoot) {
1243
1244
  );
1244
1245
  const stack = nxJson.mnci?.stack;
1245
1246
  return {
1246
- testRunner: stack?.testRunner === "vitest" ? "vitest" : "jest"
1247
+ testRunner: stack?.testRunner === "vitest" ? "vitest" : "jest",
1248
+ // Defaults to `eslint` when absent, the same call `mnci upgrade` makes and
1249
+ // for the same reason: every workspace generated before the choice existed
1250
+ // has no persisted value and is an ESLint workspace. Reading it at all is
1251
+ // the fix — this function returned `testRunner` alone, so `add` finished by
1252
+ // formatting an oxlint workspace with Prettier, against Prettier's defaults,
1253
+ // since the overlay had deleted the config that would have corrected it.
1254
+ linter: stack?.linter === "oxlint" ? "oxlint" : "eslint"
1247
1255
  };
1248
1256
  }
1249
1257
 
@@ -1435,16 +1443,17 @@ function eslintToolchainDependencies(nxVersion) {
1435
1443
  eslint: ESLINT_VERSION,
1436
1444
  "@nx/eslint": nxVersion,
1437
1445
  "@nx/eslint-plugin": nxVersion,
1438
- "@mnci/eslint-config": eslintConfigSpec(),
1439
- // Declared rather than left to hoisting. `@mnci/eslint-config` depends on
1440
- // prettier, so `npx prettier` works either way — but the VS Code extension
1441
- // resolves prettier from the PROJECT's dependencies and silently falls back
1442
- // to its own bundled copy when it finds none. A workspace should declare the
1443
- // formatter it formats with instead of relying on npm flattening someone
1444
- // else's transitive dependency to the root.
1445
- prettier: PRETTIER_VERSION
1446
+ "@mnci/eslint-config": eslintConfigSpec()
1446
1447
  };
1447
1448
  }
1449
+ var LINTER_ONLY_DEPENDENCIES = {
1450
+ eslint: ["prettier"],
1451
+ oxlint: ["@mnci/oxlint-config", "oxlint", "oxfmt"]
1452
+ };
1453
+ function withoutStaleLinterDependencies(devDeps, linter) {
1454
+ const stale = new Set(LINTER_ONLY_DEPENDENCIES[linter === "oxlint" ? "eslint" : "oxlint"]);
1455
+ return Object.fromEntries(Object.entries(devDeps).filter(([name]) => !stale.has(name)));
1456
+ }
1448
1457
  function rootLintTarget(linter) {
1449
1458
  if (linter !== "oxlint") return ROOT_LINT_TARGET;
1450
1459
  return {
@@ -1610,8 +1619,13 @@ var FORMATTED_LANGUAGES = [
1610
1619
  "jsonc",
1611
1620
  "yaml",
1612
1621
  "markdown",
1613
- "css"
1622
+ "css",
1623
+ "html"
1614
1624
  ];
1625
+ var OXFMT_ONLY_LANGUAGES = ["toml"];
1626
+ function formattedLanguages(linter) {
1627
+ return [...FORMATTED_LANGUAGES, ...linter === "oxlint" ? OXFMT_ONLY_LANGUAGES : []];
1628
+ }
1615
1629
  function vscodeExtensions(linter) {
1616
1630
  return [...LINTER_EXTENSIONS[linter], ...SHARED_VSCODE_EXTENSIONS];
1617
1631
  }
@@ -1649,7 +1663,7 @@ function vscodeSettings(linter) {
1649
1663
  // while `.json`/`.jsonc`/`.yaml` were — exactly the three that had explicit
1650
1664
  // entries here and nothing else did.
1651
1665
  ...Object.fromEntries(
1652
- FORMATTED_LANGUAGES.map((language) => [
1666
+ formattedLanguages(linter).map((language) => [
1653
1667
  `[${language}]`,
1654
1668
  { "editor.defaultFormatter": formatter }
1655
1669
  ])
@@ -2260,16 +2274,20 @@ function applyOverlay(workspaceRoot, options) {
2260
2274
  ...rootScripts(options.stack.linter)
2261
2275
  };
2262
2276
  const existingDevDeps = manifest.devDependencies;
2263
- const devDeps = {
2264
- ...existingDevDeps,
2265
- ...TS_COMPILER_DEPENDENCIES,
2266
- // The preset pins `nx` itself; the ESLint plugins must match it exactly.
2267
- ...eslintToolchainDependencies(existingDevDeps?.nx ?? "latest"),
2268
- // Additive, not a replacement: the hybrid keeps the whole ESLint toolchain
2269
- // for the file types oxlint cannot parse.
2270
- ...options.stack.linter === "oxlint" && oxlintToolchainDependencies(),
2271
- prettier: PRETTIER_VERSION
2272
- };
2277
+ const devDeps = withoutStaleLinterDependencies(
2278
+ {
2279
+ ...existingDevDeps,
2280
+ ...TS_COMPILER_DEPENDENCIES,
2281
+ // The preset pins `nx` itself; the ESLint plugins must match it exactly.
2282
+ ...eslintToolchainDependencies(existingDevDeps?.nx ?? "latest"),
2283
+ // Additive for the linter, exclusive for the formatter: the hybrid keeps
2284
+ // the whole ESLint toolchain for the file types oxlint cannot parse, but
2285
+ // exactly one of prettier/oxfmt formats the workspace. Declaring both is
2286
+ // how an editor ends up applying the one CI does not check.
2287
+ ...options.stack.linter === "oxlint" ? oxlintToolchainDependencies() : { prettier: PRETTIER_VERSION }
2288
+ },
2289
+ options.stack.linter
2290
+ );
2273
2291
  const overrides = {
2274
2292
  ...manifest.overrides,
2275
2293
  ...ESLINT_PEER_OVERRIDES
@@ -2386,6 +2404,23 @@ function checkLinterModeIsConsistent(workspaceRoot) {
2386
2404
  remedy: "run `mnci upgrade`, which removes the mode the workspace did not choose"
2387
2405
  };
2388
2406
  }
2407
+ function declaredDevDependencies(workspaceRoot) {
2408
+ const manifestPath = (0, import_node_path12.join)(workspaceRoot, "package.json");
2409
+ if (!fileExists(manifestPath)) return {};
2410
+ const manifest = readJson(manifestPath);
2411
+ return manifest.devDependencies ?? {};
2412
+ }
2413
+ function checkOneFormatterDeclared(workspaceRoot) {
2414
+ const linter = readMnciConfig(workspaceRoot).stack?.linter ?? "eslint";
2415
+ const stale = linter === "oxlint" ? "prettier" : "oxfmt";
2416
+ const declared = declaredDevDependencies(workspaceRoot)[stale] !== void 0;
2417
+ return {
2418
+ check: `only ${linter === "oxlint" ? "oxfmt" : "prettier"} is declared as the formatter`,
2419
+ ok: !declared,
2420
+ detail: `${stale} is declared but nothing runs it \u2014 the editor may still resolve and apply it, disagreeing with \`npm run format:check\``,
2421
+ remedy: "run `mnci upgrade`, which drops the formatter of the mode you left"
2422
+ };
2423
+ }
2389
2424
  function checkOxlintToolchainDeclared(workspaceRoot) {
2390
2425
  const linter = readMnciConfig(workspaceRoot).stack?.linter ?? "eslint";
2391
2426
  if (linter !== "oxlint") {
@@ -2396,10 +2431,7 @@ function checkOxlintToolchainDeclared(workspaceRoot) {
2396
2431
  remedy: ""
2397
2432
  };
2398
2433
  }
2399
- const manifest = readJson(
2400
- (0, import_node_path12.join)(workspaceRoot, "package.json")
2401
- );
2402
- const devDeps = manifest?.devDependencies ?? {};
2434
+ const devDeps = declaredDevDependencies(workspaceRoot);
2403
2435
  const missing = ["oxlint", "oxfmt", "@mnci/oxlint-config"].filter(
2404
2436
  (dependency) => devDeps[dependency] === void 0
2405
2437
  );
@@ -2505,6 +2537,7 @@ function collectFindings(workspaceRoot) {
2505
2537
  ...checkEslintConfigs(workspaceRoot),
2506
2538
  checkPrettierConfig(workspaceRoot),
2507
2539
  checkLinterModeIsConsistent(workspaceRoot),
2540
+ checkOneFormatterDeclared(workspaceRoot),
2508
2541
  checkOxlintToolchainDeclared(workspaceRoot),
2509
2542
  checkEslintPlugin(nxJson),
2510
2543
  checkResolvedEslint(workspaceRoot),
@@ -2621,8 +2654,10 @@ async function runNew(name, options) {
2621
2654
  throw new Error(`npm install of the toolchain failed with exit code ${installStatus}`);
2622
2655
  }
2623
2656
  runShell("npx", ["husky"], workspaceRoot);
2624
- logger.step("Formatting the workspace (Prettier, JavaScript Standard Style)");
2625
- runPrettier(workspaceRoot);
2657
+ logger.step(
2658
+ `Formatting the workspace (${stack.linter === "oxlint" ? "oxfmt" : "Prettier"}, JavaScript Standard Style)`
2659
+ );
2660
+ runFormatter(workspaceRoot, stack.linter);
2626
2661
  logger.success("Done. Next steps:");
2627
2662
  logger.info(` cd ${workspaceName}`);
2628
2663
  logger.info(" mnci add react-app web # or: react-lib, react-internal-lib, node-app,");
@@ -2728,7 +2763,7 @@ function runUpgrade(workspaceRoot, options) {
2728
2763
  "Re-applying the MoNecromanCI overlay (release config, .npmrc, commitlint, pipeline, stack)"
2729
2764
  );
2730
2765
  applyOverlay(workspaceRoot, resolved);
2731
- runPrettier(workspaceRoot);
2766
+ runFormatter(workspaceRoot, resolved.stack.linter);
2732
2767
  logger.success("Done. Review the changes with `git diff` before committing.");
2733
2768
  }
2734
2769