@mnci/cli 1.18.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
@@ -30,6 +30,7 @@ first-party (or established community) Nx equivalent:
30
30
  mnci new my-repo # create a monorepo (prompts scope + registry)
31
31
  mnci new my-repo --yes --registry npm --scope @my
32
32
  mnci new my-repo --yes --registry npm --scope @my --nx-cloud # opt in to Nx Cloud
33
+ mnci new my-repo --yes --registry npm --scope @my --linter oxlint # Rust toolchain
33
34
 
34
35
  cd my-repo
35
36
  mnci add react-app web # @nx/react (Vite + Jest)
@@ -75,15 +76,17 @@ Every check corresponds to an invariant that has **actually** been violated, in
75
76
  this repo or in a workspace it generated. None are hypothetical; a check nobody has
76
77
  ever needed is noise that trains people to ignore the output.
77
78
 
78
- | Check | The failure it catches |
79
- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
80
- | Exactly one root ESLint config, and no per-project ones | The config fragmenting — every `@nx/*` generator writes one, so each project ends up linting against whichever config sits nearest |
81
- | No stray `.prettierrc` | It outranks `.prettierrc.mjs`, so the whole formatting opinion is silently discarded while both files look fine |
82
- | `@nx/eslint/plugin` registered in `nx.json` | Without it `npm run lint` exits 0 while linting nothing |
83
- | The **resolved** `eslint` major | A declared range and an installed version are different things — manifests once said `^10` while the pin said 9 |
84
- | `.npmrc` matches the recorded registry | The two registry kinds get different files; an Azure workspace also needs its scope routed |
85
- | `versionActions` on publishable Dart/Python packages | Its absence aborts `nx release` for the **whole** workspace, not just that project |
86
- | `nx sync:check` | A stale TypeScript project reference that was never committed |
79
+ | Check | The failure it catches |
80
+ | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
81
+ | Exactly one root ESLint config, and no per-project ones | The config fragmenting — every `@nx/*` generator writes one, so each project ends up linting against whichever config sits nearest |
82
+ | No stray `.prettierrc` | It outranks `.prettierrc.mjs`, so the whole formatting opinion is silently discarded while both files look fine |
83
+ | `@nx/eslint/plugin` registered in `nx.json` | Without it `npm run lint` exits 0 while linting nothing |
84
+ | The **resolved** `eslint` major | A declared range and an installed version are different things — manifests once said `^10` while the pin said 9 |
85
+ | `.npmrc` matches the recorded registry | The two registry kinds get different files; an Azure workspace also needs its scope routed |
86
+ | `versionActions` on publishable Dart/Python packages | Its absence aborts `nx release` for the **whole** workspace, not just that project |
87
+ | `nx sync:check` | A stale TypeScript project reference that was never committed |
88
+ | Only the chosen linter's config files present | Two formatter configs is not a visible error — each is valid, the CLI picks one, the editor may pick the other, and the two gates disagree silently |
89
+ | oxlint toolchain declared | `@mnci/oxlint-config` peers on `oxlint`, so a missing declaration turns `npm run lint` into "command not found" at the worst moment |
87
90
 
88
91
  Everything else is plain Nx, surfaced as a small curated set of root scripts —
89
92
  each a single cross-platform command:
@@ -262,6 +265,53 @@ contradicting each other. `mnci` runs Prettier itself at the end of `new` and ev
262
265
  generators emit semicolons and double quotes, and without that pass the first
263
266
  commit buries every real change under generator noise.
264
267
 
268
+ **Choosing the linter.** `--linter=eslint` (default) or `--linter=oxlint`, also a
269
+ prompt at `mnci new` and overridable on `mnci upgrade`. The formatter rides along
270
+ — ESLint pairs with Prettier, oxlint with oxfmt — because both are the same seven
271
+ Standard options, so the choice is about speed and toolchain, never about how the
272
+ code ends up looking.
273
+
274
+ **`oxlint` is a hybrid, not a swap**, and that is deliberate. oxlint parses
275
+ JS/TS/JSX/Vue and nothing else, so an oxlint workspace keeps a trimmed ESLint
276
+ config (`@mnci/eslint-config`'s `nonJs()`) for the file types oxlint cannot read:
277
+
278
+ | | `eslint` | `oxlint` |
279
+ | ------------------------------- | -------------------------------------- | -------------------------------------------------------- |
280
+ | JS/TS lint | ESLint, 452 rules | oxlint, 206 native + 225 bridged |
281
+ | YAML/TOML/MD/CSS/HTML/JSON lint | ESLint | **ESLint** (unchanged) |
282
+ | `@nx/dependency-checks` | yes | **yes** (unchanged) |
283
+ | Formatter | Prettier | oxfmt (~6x faster on this repo) |
284
+ | Formats `.toml` | no — Prettier has no TOML parser | **yes** |
285
+ | Config files | `eslint.config.mjs`, `.prettierrc.mjs` | `oxlint.config.ts`, `.oxfmtrc.json`, `eslint.config.mjs` |
286
+ | VS Code extension | ESLint + Prettier | Oxc (covers both) + ESLint |
287
+
288
+ A pure swap was considered and rejected: it would leave a duplicate key in a CI
289
+ pipeline, a malformed `pyproject.toml`, and a publishable package's wrong manifest
290
+ all reported by nothing. Switching modes with `mnci upgrade --linter=<other>`
291
+ rewrites the config files **and deletes the mode you left**, so a workspace never
292
+ carries two formatter configs.
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
+
265
315
  **Finding and overriding a rule.** One root config importing a package is easy to
266
316
  own and hard to read: nothing in three lines says which of ~20 plugins reported
267
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
 
@@ -1438,6 +1446,39 @@ function eslintToolchainDependencies(nxVersion) {
1438
1446
  "@mnci/eslint-config": eslintConfigSpec()
1439
1447
  };
1440
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
+ }
1457
+ function rootLintTarget(linter) {
1458
+ if (linter !== "oxlint") return ROOT_LINT_TARGET;
1459
+ return {
1460
+ executor: "nx:run-commands",
1461
+ cache: true,
1462
+ options: {
1463
+ commands: ["oxlint .", ROOT_LINT_TARGET.options.command],
1464
+ parallel: false,
1465
+ cwd: "."
1466
+ }
1467
+ };
1468
+ }
1469
+ var OXLINT_VERSION = "^1.76.0";
1470
+ var OXFMT_VERSION = "^0.61.0";
1471
+ function oxlintToolchainDependencies() {
1472
+ return {
1473
+ "@mnci/oxlint-config": oxlintConfigSpec(),
1474
+ oxlint: OXLINT_VERSION,
1475
+ oxfmt: OXFMT_VERSION
1476
+ };
1477
+ }
1478
+ var OXLINT_CONFIG_VERSION = "^0.1.0";
1479
+ function oxlintConfigSpec() {
1480
+ return process.env.MNCI_OXLINT_CONFIG_SPEC ?? OXLINT_CONFIG_VERSION;
1481
+ }
1441
1482
  var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block, by the \`name\` it carries.
1442
1483
  //
1443
1484
  // mnci/ignores paths never linted (dist, coverage, .venv, \u2026)
@@ -1495,6 +1536,53 @@ ${ESLINT_BLOCK_INVENTORY}//
1495
1536
  // impossible to satisfy at the same time. Formatting lives in .prettierrc.mjs.
1496
1537
  export default mnci({ workspaceRoot: import.meta.dirname })
1497
1538
  `;
1539
+ var ESLINT_CONFIG_NON_JS = `import { nonJs } from '@mnci/eslint-config'
1540
+
1541
+ // This workspace lints JS/TS with oxlint (see oxlint.config.ts). ESLint keeps
1542
+ // the file types oxlint cannot parse at all \u2014 YAML, TOML, Markdown, CSS, HTML,
1543
+ // JSON \u2014 plus @nx/dependency-checks on publishable manifests.
1544
+ //
1545
+ // The JS/TS rule blocks are deliberately NOT composed here. Running both linters
1546
+ // over the same file would report one defect twice under two different names.
1547
+ //
1548
+ // TO OVERRIDE, append a named block after the spread, exactly as the full config
1549
+ // documents. To change a JS/TS rule, edit oxlint.config.ts instead \u2014 this file
1550
+ // does not lint JS/TS.
1551
+ export default [...nonJs({ workspaceRoot: import.meta.dirname })]
1552
+ `;
1553
+ var OXLINT_CONFIG = `import { defineConfig } from 'oxlint'
1554
+ import mnci from '@mnci/oxlint-config'
1555
+
1556
+ // oxlint owns JS/TS/JSX here; eslint.config.mjs keeps everything oxlint cannot
1557
+ // parse. The contract @mnci/oxlint-config holds itself to: anything
1558
+ // @mnci/eslint-config accepts must pass oxlint, so switching linters never turns
1559
+ // a green codebase red.
1560
+ //
1561
+ // oxlint.config.ts, not .oxlintrc.json, and that is not a preference: .oxlintrc's
1562
+ // \`extends\` takes PATHS relative to the config file, so
1563
+ // \`extends: ["@mnci/oxlint-config"]\` fails with "No such file or directory".
1564
+ // defineConfig is the only shareable-config route oxlint offers.
1565
+ //
1566
+ // Type-aware rules (no-floating-promises and friends) are opt-in \u2014 pass
1567
+ // { typeAware: true } AND run oxlint with --type-aware. They report findings the
1568
+ // ESLint stack accepts, so they are off by default to keep the contract above.
1569
+ export default defineConfig({ extends: [mnci()] })
1570
+ `;
1571
+ var OXFMT_CONFIG = `${JSON.stringify(
1572
+ {
1573
+ $schema: "./node_modules/oxfmt/configuration_schema.json",
1574
+ semi: false,
1575
+ singleQuote: true,
1576
+ trailingComma: "none",
1577
+ arrowParens: "avoid",
1578
+ printWidth: 100,
1579
+ tabWidth: 2,
1580
+ useTabs: false
1581
+ },
1582
+ null,
1583
+ 2
1584
+ )}
1585
+ `;
1498
1586
  var PRETTIER_IGNORE = `node_modules
1499
1587
  dist
1500
1588
  dist-dev
@@ -1522,6 +1610,22 @@ var SHARED_VSCODE_EXTENSIONS = [
1522
1610
  "nrwl.angular-console",
1523
1611
  "firsttris.vscode-jest-runner"
1524
1612
  ];
1613
+ var FORMATTED_LANGUAGES = [
1614
+ "javascript",
1615
+ "javascriptreact",
1616
+ "typescript",
1617
+ "typescriptreact",
1618
+ "json",
1619
+ "jsonc",
1620
+ "yaml",
1621
+ "markdown",
1622
+ "css",
1623
+ "html"
1624
+ ];
1625
+ var OXFMT_ONLY_LANGUAGES = ["toml"];
1626
+ function formattedLanguages(linter) {
1627
+ return [...FORMATTED_LANGUAGES, ...linter === "oxlint" ? OXFMT_ONLY_LANGUAGES : []];
1628
+ }
1525
1629
  function vscodeExtensions(linter) {
1526
1630
  return [...LINTER_EXTENSIONS[linter], ...SHARED_VSCODE_EXTENSIONS];
1527
1631
  }
@@ -1544,9 +1648,26 @@ function vscodeSettings(linter) {
1544
1648
  },
1545
1649
  "editor.defaultFormatter": formatter,
1546
1650
  "editor.formatOnSave": true,
1547
- "[json]": { "editor.defaultFormatter": formatter },
1548
- "[jsonc]": { "editor.defaultFormatter": formatter },
1549
- "[yaml]": { "editor.defaultFormatter": formatter }
1651
+ // Every language spelled out, and the global default above is NOT enough on
1652
+ // its own — which is the bug this fixes rather than a belt-and-braces habit.
1653
+ //
1654
+ // VS Code resolves a language-specific setting ahead of a general one, and it
1655
+ // does that comparison BEFORE scope. So a `[typescript]` block in someone's
1656
+ // USER settings — left over from any other project — outranks this file's
1657
+ // workspace-level `editor.defaultFormatter`, and format-on-save quietly uses
1658
+ // that other formatter instead. Nothing reports it: Prettier is installed,
1659
+ // the config resolves, `npm run format:check` still finds the 68 unformatted
1660
+ // files, and the editor simply never applies it.
1661
+ //
1662
+ // Reported from a real workspace where `.ts` files were not being formatted
1663
+ // while `.json`/`.jsonc`/`.yaml` were — exactly the three that had explicit
1664
+ // entries here and nothing else did.
1665
+ ...Object.fromEntries(
1666
+ formattedLanguages(linter).map((language) => [
1667
+ `[${language}]`,
1668
+ { "editor.defaultFormatter": formatter }
1669
+ ])
1670
+ )
1550
1671
  };
1551
1672
  }
1552
1673
  function vscodeWorkspace(workspaceName, linter, existingTasks) {
@@ -1574,11 +1695,12 @@ var ROOT_SCRIPTS = {
1574
1695
  "release:preview": "nx release --dry-run",
1575
1696
  prepare: "husky"
1576
1697
  };
1577
- function rootScripts() {
1698
+ function rootScripts(linter = "eslint") {
1699
+ const formatter = linter === "oxlint" ? "oxfmt" : "prettier";
1578
1700
  return {
1579
1701
  ...ROOT_SCRIPTS,
1580
- format: "prettier --write .",
1581
- "format:check": "prettier --check .",
1702
+ format: linter === "oxlint" ? "oxfmt ." : "prettier --write .",
1703
+ "format:check": `${formatter} --check .`,
1582
1704
  "python:install": `${PYTHON_INSTALL_GUARD} && ${PYTHON_WORKSPACE_INSTALL_GUARD}`
1583
1705
  };
1584
1706
  }
@@ -2123,6 +2245,9 @@ function removeNxScaffolding(workspaceRoot) {
2123
2245
  }
2124
2246
  removeProjectEslintConfigs(workspaceRoot);
2125
2247
  }
2248
+ function removeIfPresent(path) {
2249
+ (0, import_node_fs6.rmSync)(path, { recursive: true, force: true });
2250
+ }
2126
2251
  function removeProjectEslintConfigs(workspaceRoot) {
2127
2252
  const matches = (0, import_node_fs6.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
2128
2253
  cwd: workspaceRoot
@@ -2146,16 +2271,23 @@ function applyOverlay(workspaceRoot, options) {
2146
2271
  const manifest = readJson(manifestPath);
2147
2272
  const scripts = {
2148
2273
  ...manifest.scripts,
2149
- ...rootScripts()
2274
+ ...rootScripts(options.stack.linter)
2150
2275
  };
2151
2276
  const existingDevDeps = manifest.devDependencies;
2152
- const devDeps = {
2153
- ...existingDevDeps,
2154
- ...TS_COMPILER_DEPENDENCIES,
2155
- // The preset pins `nx` itself; the ESLint plugins must match it exactly.
2156
- ...eslintToolchainDependencies(existingDevDeps?.nx ?? "latest"),
2157
- prettier: PRETTIER_VERSION
2158
- };
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
+ );
2159
2291
  const overrides = {
2160
2292
  ...manifest.overrides,
2161
2293
  ...ESLINT_PEER_OVERRIDES
@@ -2166,7 +2298,7 @@ function applyOverlay(workspaceRoot, options) {
2166
2298
  includedScripts: existingNx?.includedScripts ?? [],
2167
2299
  targets: {
2168
2300
  ...existingNx?.targets,
2169
- lint: ROOT_LINT_TARGET
2301
+ lint: rootLintTarget(options.stack.linter)
2170
2302
  }
2171
2303
  };
2172
2304
  writeFileEnsured(
@@ -2185,8 +2317,17 @@ function applyOverlay(workspaceRoot, options) {
2185
2317
  const hookPath = (0, import_node_path11.join)(workspaceRoot, ".husky/commit-msg");
2186
2318
  writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
2187
2319
  markExecutable(hookPath);
2188
- writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
2189
- writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierrc.mjs"), PRETTIER_CONFIG);
2320
+ if (options.stack.linter === "oxlint") {
2321
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "oxlint.config.ts"), OXLINT_CONFIG);
2322
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".oxfmtrc.json"), OXFMT_CONFIG);
2323
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG_NON_JS);
2324
+ removeIfPresent((0, import_node_path11.join)(workspaceRoot, ".prettierrc.mjs"));
2325
+ } else {
2326
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
2327
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierrc.mjs"), PRETTIER_CONFIG);
2328
+ removeIfPresent((0, import_node_path11.join)(workspaceRoot, "oxlint.config.ts"));
2329
+ removeIfPresent((0, import_node_path11.join)(workspaceRoot, ".oxfmtrc.json"));
2330
+ }
2190
2331
  writeFileEnsured(
2191
2332
  (0, import_node_path11.join)(workspaceRoot, ".devcontainer/devcontainer.json"),
2192
2333
  devcontainerJson(options.workspaceName, options.stack.linter)
@@ -2249,6 +2390,58 @@ function checkPrettierConfig(workspaceRoot) {
2249
2390
  remedy: "delete .prettierrc (or run `mnci upgrade`, which deletes it)"
2250
2391
  };
2251
2392
  }
2393
+ function checkLinterModeIsConsistent(workspaceRoot) {
2394
+ const linter = readMnciConfig(workspaceRoot).stack?.linter ?? "eslint";
2395
+ const oxlintFiles = ["oxlint.config.ts", ".oxfmtrc.json"].filter(
2396
+ (file) => fileExists((0, import_node_path12.join)(workspaceRoot, file))
2397
+ );
2398
+ const hasPrettier = fileExists((0, import_node_path12.join)(workspaceRoot, ".prettierrc.mjs"));
2399
+ const stray = linter === "oxlint" ? hasPrettier ? [".prettierrc.mjs"] : [] : oxlintFiles;
2400
+ return {
2401
+ check: `linter is ${linter} and only ${linter}'s config files are present`,
2402
+ ok: stray.length === 0,
2403
+ detail: `${stray.join(", ")} belongs to the other linter mode, so two configs are in play at once`,
2404
+ remedy: "run `mnci upgrade`, which removes the mode the workspace did not choose"
2405
+ };
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
+ }
2424
+ function checkOxlintToolchainDeclared(workspaceRoot) {
2425
+ const linter = readMnciConfig(workspaceRoot).stack?.linter ?? "eslint";
2426
+ if (linter !== "oxlint") {
2427
+ return {
2428
+ check: "oxlint toolchain declared",
2429
+ ok: true,
2430
+ detail: "not an oxlint workspace",
2431
+ remedy: ""
2432
+ };
2433
+ }
2434
+ const devDeps = declaredDevDependencies(workspaceRoot);
2435
+ const missing = ["oxlint", "oxfmt", "@mnci/oxlint-config"].filter(
2436
+ (dependency) => devDeps[dependency] === void 0
2437
+ );
2438
+ return {
2439
+ check: "oxlint toolchain declared in devDependencies",
2440
+ ok: missing.length === 0,
2441
+ detail: `missing: ${missing.join(", ")} \u2014 lint or format will fail with "command not found"`,
2442
+ remedy: "run `mnci upgrade`, which writes the toolchain the chosen linter needs"
2443
+ };
2444
+ }
2252
2445
  function checkEslintPlugin(nxJson) {
2253
2446
  const plugins = nxJson.plugins ?? [];
2254
2447
  const registered = plugins.some(
@@ -2343,6 +2536,9 @@ function collectFindings(workspaceRoot) {
2343
2536
  return [
2344
2537
  ...checkEslintConfigs(workspaceRoot),
2345
2538
  checkPrettierConfig(workspaceRoot),
2539
+ checkLinterModeIsConsistent(workspaceRoot),
2540
+ checkOneFormatterDeclared(workspaceRoot),
2541
+ checkOxlintToolchainDeclared(workspaceRoot),
2346
2542
  checkEslintPlugin(nxJson),
2347
2543
  checkResolvedEslint(workspaceRoot),
2348
2544
  checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
@@ -2458,8 +2654,10 @@ async function runNew(name, options) {
2458
2654
  throw new Error(`npm install of the toolchain failed with exit code ${installStatus}`);
2459
2655
  }
2460
2656
  runShell("npx", ["husky"], workspaceRoot);
2461
- logger.step("Formatting the workspace (Prettier, JavaScript Standard Style)");
2462
- runPrettier(workspaceRoot);
2657
+ logger.step(
2658
+ `Formatting the workspace (${stack.linter === "oxlint" ? "oxfmt" : "Prettier"}, JavaScript Standard Style)`
2659
+ );
2660
+ runFormatter(workspaceRoot, stack.linter);
2463
2661
  logger.success("Done. Next steps:");
2464
2662
  logger.info(` cd ${workspaceName}`);
2465
2663
  logger.info(" mnci add react-app web # or: react-lib, react-internal-lib, node-app,");
@@ -2565,7 +2763,7 @@ function runUpgrade(workspaceRoot, options) {
2565
2763
  "Re-applying the MoNecromanCI overlay (release config, .npmrc, commitlint, pipeline, stack)"
2566
2764
  );
2567
2765
  applyOverlay(workspaceRoot, resolved);
2568
- runPrettier(workspaceRoot);
2766
+ runFormatter(workspaceRoot, resolved.stack.linter);
2569
2767
  logger.success("Done. Review the changes with `git diff` before committing.");
2570
2768
  }
2571
2769