@mnci/cli 1.18.0 → 1.19.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/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,31 @@ 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 (~30x faster) |
284
+ | Config files | `eslint.config.mjs`, `.prettierrc.mjs` | `oxlint.config.ts`, `.oxfmtrc.json`, `eslint.config.mjs` |
285
+ | VS Code extension | ESLint + Prettier | Oxc (covers both) + ESLint |
286
+
287
+ A pure swap was considered and rejected: it would leave a duplicate key in a CI
288
+ pipeline, a malformed `pyproject.toml`, and a publishable package's wrong manifest
289
+ all reported by nothing. Switching modes with `mnci upgrade --linter=<other>`
290
+ rewrites the config files **and deletes the mode you left**, so a workspace never
291
+ carries two formatter configs.
292
+
265
293
  **Finding and overriding a rule.** One root config importing a package is easy to
266
294
  own and hard to read: nothing in three lines says which of ~20 plugins reported
267
295
  the thing you disagree with. Every block in `@mnci/eslint-config` therefore
package/dist/cli.js CHANGED
@@ -1435,9 +1435,41 @@ function eslintToolchainDependencies(nxVersion) {
1435
1435
  eslint: ESLINT_VERSION,
1436
1436
  "@nx/eslint": nxVersion,
1437
1437
  "@nx/eslint-plugin": nxVersion,
1438
- "@mnci/eslint-config": eslintConfigSpec()
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
+ };
1447
+ }
1448
+ function rootLintTarget(linter) {
1449
+ if (linter !== "oxlint") return ROOT_LINT_TARGET;
1450
+ return {
1451
+ executor: "nx:run-commands",
1452
+ cache: true,
1453
+ options: {
1454
+ commands: ["oxlint .", ROOT_LINT_TARGET.options.command],
1455
+ parallel: false,
1456
+ cwd: "."
1457
+ }
1458
+ };
1459
+ }
1460
+ var OXLINT_VERSION = "^1.76.0";
1461
+ var OXFMT_VERSION = "^0.61.0";
1462
+ function oxlintToolchainDependencies() {
1463
+ return {
1464
+ "@mnci/oxlint-config": oxlintConfigSpec(),
1465
+ oxlint: OXLINT_VERSION,
1466
+ oxfmt: OXFMT_VERSION
1439
1467
  };
1440
1468
  }
1469
+ var OXLINT_CONFIG_VERSION = "^0.1.0";
1470
+ function oxlintConfigSpec() {
1471
+ return process.env.MNCI_OXLINT_CONFIG_SPEC ?? OXLINT_CONFIG_VERSION;
1472
+ }
1441
1473
  var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block, by the \`name\` it carries.
1442
1474
  //
1443
1475
  // mnci/ignores paths never linted (dist, coverage, .venv, \u2026)
@@ -1495,6 +1527,53 @@ ${ESLINT_BLOCK_INVENTORY}//
1495
1527
  // impossible to satisfy at the same time. Formatting lives in .prettierrc.mjs.
1496
1528
  export default mnci({ workspaceRoot: import.meta.dirname })
1497
1529
  `;
1530
+ var ESLINT_CONFIG_NON_JS = `import { nonJs } from '@mnci/eslint-config'
1531
+
1532
+ // This workspace lints JS/TS with oxlint (see oxlint.config.ts). ESLint keeps
1533
+ // the file types oxlint cannot parse at all \u2014 YAML, TOML, Markdown, CSS, HTML,
1534
+ // JSON \u2014 plus @nx/dependency-checks on publishable manifests.
1535
+ //
1536
+ // The JS/TS rule blocks are deliberately NOT composed here. Running both linters
1537
+ // over the same file would report one defect twice under two different names.
1538
+ //
1539
+ // TO OVERRIDE, append a named block after the spread, exactly as the full config
1540
+ // documents. To change a JS/TS rule, edit oxlint.config.ts instead \u2014 this file
1541
+ // does not lint JS/TS.
1542
+ export default [...nonJs({ workspaceRoot: import.meta.dirname })]
1543
+ `;
1544
+ var OXLINT_CONFIG = `import { defineConfig } from 'oxlint'
1545
+ import mnci from '@mnci/oxlint-config'
1546
+
1547
+ // oxlint owns JS/TS/JSX here; eslint.config.mjs keeps everything oxlint cannot
1548
+ // parse. The contract @mnci/oxlint-config holds itself to: anything
1549
+ // @mnci/eslint-config accepts must pass oxlint, so switching linters never turns
1550
+ // a green codebase red.
1551
+ //
1552
+ // oxlint.config.ts, not .oxlintrc.json, and that is not a preference: .oxlintrc's
1553
+ // \`extends\` takes PATHS relative to the config file, so
1554
+ // \`extends: ["@mnci/oxlint-config"]\` fails with "No such file or directory".
1555
+ // defineConfig is the only shareable-config route oxlint offers.
1556
+ //
1557
+ // Type-aware rules (no-floating-promises and friends) are opt-in \u2014 pass
1558
+ // { typeAware: true } AND run oxlint with --type-aware. They report findings the
1559
+ // ESLint stack accepts, so they are off by default to keep the contract above.
1560
+ export default defineConfig({ extends: [mnci()] })
1561
+ `;
1562
+ var OXFMT_CONFIG = `${JSON.stringify(
1563
+ {
1564
+ $schema: "./node_modules/oxfmt/configuration_schema.json",
1565
+ semi: false,
1566
+ singleQuote: true,
1567
+ trailingComma: "none",
1568
+ arrowParens: "avoid",
1569
+ printWidth: 100,
1570
+ tabWidth: 2,
1571
+ useTabs: false
1572
+ },
1573
+ null,
1574
+ 2
1575
+ )}
1576
+ `;
1498
1577
  var PRETTIER_IGNORE = `node_modules
1499
1578
  dist
1500
1579
  dist-dev
@@ -1522,6 +1601,17 @@ var SHARED_VSCODE_EXTENSIONS = [
1522
1601
  "nrwl.angular-console",
1523
1602
  "firsttris.vscode-jest-runner"
1524
1603
  ];
1604
+ var FORMATTED_LANGUAGES = [
1605
+ "javascript",
1606
+ "javascriptreact",
1607
+ "typescript",
1608
+ "typescriptreact",
1609
+ "json",
1610
+ "jsonc",
1611
+ "yaml",
1612
+ "markdown",
1613
+ "css"
1614
+ ];
1525
1615
  function vscodeExtensions(linter) {
1526
1616
  return [...LINTER_EXTENSIONS[linter], ...SHARED_VSCODE_EXTENSIONS];
1527
1617
  }
@@ -1544,9 +1634,26 @@ function vscodeSettings(linter) {
1544
1634
  },
1545
1635
  "editor.defaultFormatter": formatter,
1546
1636
  "editor.formatOnSave": true,
1547
- "[json]": { "editor.defaultFormatter": formatter },
1548
- "[jsonc]": { "editor.defaultFormatter": formatter },
1549
- "[yaml]": { "editor.defaultFormatter": formatter }
1637
+ // Every language spelled out, and the global default above is NOT enough on
1638
+ // its own — which is the bug this fixes rather than a belt-and-braces habit.
1639
+ //
1640
+ // VS Code resolves a language-specific setting ahead of a general one, and it
1641
+ // does that comparison BEFORE scope. So a `[typescript]` block in someone's
1642
+ // USER settings — left over from any other project — outranks this file's
1643
+ // workspace-level `editor.defaultFormatter`, and format-on-save quietly uses
1644
+ // that other formatter instead. Nothing reports it: Prettier is installed,
1645
+ // the config resolves, `npm run format:check` still finds the 68 unformatted
1646
+ // files, and the editor simply never applies it.
1647
+ //
1648
+ // Reported from a real workspace where `.ts` files were not being formatted
1649
+ // while `.json`/`.jsonc`/`.yaml` were — exactly the three that had explicit
1650
+ // entries here and nothing else did.
1651
+ ...Object.fromEntries(
1652
+ FORMATTED_LANGUAGES.map((language) => [
1653
+ `[${language}]`,
1654
+ { "editor.defaultFormatter": formatter }
1655
+ ])
1656
+ )
1550
1657
  };
1551
1658
  }
1552
1659
  function vscodeWorkspace(workspaceName, linter, existingTasks) {
@@ -1574,11 +1681,12 @@ var ROOT_SCRIPTS = {
1574
1681
  "release:preview": "nx release --dry-run",
1575
1682
  prepare: "husky"
1576
1683
  };
1577
- function rootScripts() {
1684
+ function rootScripts(linter = "eslint") {
1685
+ const formatter = linter === "oxlint" ? "oxfmt" : "prettier";
1578
1686
  return {
1579
1687
  ...ROOT_SCRIPTS,
1580
- format: "prettier --write .",
1581
- "format:check": "prettier --check .",
1688
+ format: linter === "oxlint" ? "oxfmt ." : "prettier --write .",
1689
+ "format:check": `${formatter} --check .`,
1582
1690
  "python:install": `${PYTHON_INSTALL_GUARD} && ${PYTHON_WORKSPACE_INSTALL_GUARD}`
1583
1691
  };
1584
1692
  }
@@ -2123,6 +2231,9 @@ function removeNxScaffolding(workspaceRoot) {
2123
2231
  }
2124
2232
  removeProjectEslintConfigs(workspaceRoot);
2125
2233
  }
2234
+ function removeIfPresent(path) {
2235
+ (0, import_node_fs6.rmSync)(path, { recursive: true, force: true });
2236
+ }
2126
2237
  function removeProjectEslintConfigs(workspaceRoot) {
2127
2238
  const matches = (0, import_node_fs6.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
2128
2239
  cwd: workspaceRoot
@@ -2146,7 +2257,7 @@ function applyOverlay(workspaceRoot, options) {
2146
2257
  const manifest = readJson(manifestPath);
2147
2258
  const scripts = {
2148
2259
  ...manifest.scripts,
2149
- ...rootScripts()
2260
+ ...rootScripts(options.stack.linter)
2150
2261
  };
2151
2262
  const existingDevDeps = manifest.devDependencies;
2152
2263
  const devDeps = {
@@ -2154,6 +2265,9 @@ function applyOverlay(workspaceRoot, options) {
2154
2265
  ...TS_COMPILER_DEPENDENCIES,
2155
2266
  // The preset pins `nx` itself; the ESLint plugins must match it exactly.
2156
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(),
2157
2271
  prettier: PRETTIER_VERSION
2158
2272
  };
2159
2273
  const overrides = {
@@ -2166,7 +2280,7 @@ function applyOverlay(workspaceRoot, options) {
2166
2280
  includedScripts: existingNx?.includedScripts ?? [],
2167
2281
  targets: {
2168
2282
  ...existingNx?.targets,
2169
- lint: ROOT_LINT_TARGET
2283
+ lint: rootLintTarget(options.stack.linter)
2170
2284
  }
2171
2285
  };
2172
2286
  writeFileEnsured(
@@ -2185,8 +2299,17 @@ function applyOverlay(workspaceRoot, options) {
2185
2299
  const hookPath = (0, import_node_path11.join)(workspaceRoot, ".husky/commit-msg");
2186
2300
  writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
2187
2301
  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);
2302
+ if (options.stack.linter === "oxlint") {
2303
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "oxlint.config.ts"), OXLINT_CONFIG);
2304
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".oxfmtrc.json"), OXFMT_CONFIG);
2305
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG_NON_JS);
2306
+ removeIfPresent((0, import_node_path11.join)(workspaceRoot, ".prettierrc.mjs"));
2307
+ } else {
2308
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
2309
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierrc.mjs"), PRETTIER_CONFIG);
2310
+ removeIfPresent((0, import_node_path11.join)(workspaceRoot, "oxlint.config.ts"));
2311
+ removeIfPresent((0, import_node_path11.join)(workspaceRoot, ".oxfmtrc.json"));
2312
+ }
2190
2313
  writeFileEnsured(
2191
2314
  (0, import_node_path11.join)(workspaceRoot, ".devcontainer/devcontainer.json"),
2192
2315
  devcontainerJson(options.workspaceName, options.stack.linter)
@@ -2249,6 +2372,44 @@ function checkPrettierConfig(workspaceRoot) {
2249
2372
  remedy: "delete .prettierrc (or run `mnci upgrade`, which deletes it)"
2250
2373
  };
2251
2374
  }
2375
+ function checkLinterModeIsConsistent(workspaceRoot) {
2376
+ const linter = readMnciConfig(workspaceRoot).stack?.linter ?? "eslint";
2377
+ const oxlintFiles = ["oxlint.config.ts", ".oxfmtrc.json"].filter(
2378
+ (file) => fileExists((0, import_node_path12.join)(workspaceRoot, file))
2379
+ );
2380
+ const hasPrettier = fileExists((0, import_node_path12.join)(workspaceRoot, ".prettierrc.mjs"));
2381
+ const stray = linter === "oxlint" ? hasPrettier ? [".prettierrc.mjs"] : [] : oxlintFiles;
2382
+ return {
2383
+ check: `linter is ${linter} and only ${linter}'s config files are present`,
2384
+ ok: stray.length === 0,
2385
+ detail: `${stray.join(", ")} belongs to the other linter mode, so two configs are in play at once`,
2386
+ remedy: "run `mnci upgrade`, which removes the mode the workspace did not choose"
2387
+ };
2388
+ }
2389
+ function checkOxlintToolchainDeclared(workspaceRoot) {
2390
+ const linter = readMnciConfig(workspaceRoot).stack?.linter ?? "eslint";
2391
+ if (linter !== "oxlint") {
2392
+ return {
2393
+ check: "oxlint toolchain declared",
2394
+ ok: true,
2395
+ detail: "not an oxlint workspace",
2396
+ remedy: ""
2397
+ };
2398
+ }
2399
+ const manifest = readJson(
2400
+ (0, import_node_path12.join)(workspaceRoot, "package.json")
2401
+ );
2402
+ const devDeps = manifest?.devDependencies ?? {};
2403
+ const missing = ["oxlint", "oxfmt", "@mnci/oxlint-config"].filter(
2404
+ (dependency) => devDeps[dependency] === void 0
2405
+ );
2406
+ return {
2407
+ check: "oxlint toolchain declared in devDependencies",
2408
+ ok: missing.length === 0,
2409
+ detail: `missing: ${missing.join(", ")} \u2014 lint or format will fail with "command not found"`,
2410
+ remedy: "run `mnci upgrade`, which writes the toolchain the chosen linter needs"
2411
+ };
2412
+ }
2252
2413
  function checkEslintPlugin(nxJson) {
2253
2414
  const plugins = nxJson.plugins ?? [];
2254
2415
  const registered = plugins.some(
@@ -2343,6 +2504,8 @@ function collectFindings(workspaceRoot) {
2343
2504
  return [
2344
2505
  ...checkEslintConfigs(workspaceRoot),
2345
2506
  checkPrettierConfig(workspaceRoot),
2507
+ checkLinterModeIsConsistent(workspaceRoot),
2508
+ checkOxlintToolchainDeclared(workspaceRoot),
2346
2509
  checkEslintPlugin(nxJson),
2347
2510
  checkResolvedEslint(workspaceRoot),
2348
2511
  checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),