@mnci/cli 4.6.0 → 4.6.2

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
@@ -429,15 +429,20 @@ that would just fail felt worse than being upfront that it doesn't exist yet.
429
429
  `{projectName}@{version}` tags, **tag-only git** (`commit: false`) — nothing
430
430
  is ever pushed to `main`; future runs resolve versions from tag names. Also
431
431
  fills in `namedInputs.sharedGlobals` with the root config files
432
- (`eslint.config.mjs`, `tsconfig.base.json`, `package.json`), without which
432
+ (`eslint.config.mjs`, `eslint.config.mnci.mjs`, `tsconfig.base.json`,
433
+ `package.json`), without which
433
434
  `nx affected` on a pull request is blind to them: they live in no project, so
434
435
  changing one marked only the root pseudo-project — which has no
435
436
  lint/typecheck/test/build target — and the affected-scoped verify step ran
436
437
  nothing at all while reporting green.
437
- 3. Writes `eslint.config.mjs` (one import from `@mnci/eslint-config` — the whole
438
- linting opinion, in one root config — plus a commented inventory naming every
439
- config block and how to override it — there is no formatter config, because
440
- ESLint is the formatter), `.npmrc` (publish auth — see **Publish auth** below),
438
+ 3. Writes **two** ESLint files, and only one of them is mnci's:
439
+ `eslint.config.mnci.mjs` holds the whole linting opinion (one import from
440
+ `@mnci/eslint-config`, plus a commented inventory naming every config block)
441
+ and is rewritten on every upgrade; `eslint.config.mjs` is the file ESLint
442
+ actually loads, imports that one, holds **your** blocks, and is written once
443
+ and then never touched again. See **Your half of the ESLint config** below.
444
+ There is no formatter config, because ESLint is the formatter. Also writes
445
+ `.npmrc` (publish auth — see **Publish auth** below),
441
446
  `commitlint.config.mjs`, a husky `commit-msg` hook, the chosen CI provider's
442
447
  pipeline file(s)
443
448
  (`azure-pipelines.yml` and/or `.github/workflows/ci.yml`, `--ci`, default
@@ -460,7 +465,8 @@ same options `new` would have and calls the exact same `applyOverlay` `new`
460
465
  itself calls — the one function that does every bit of `mnci`-owned file
461
466
  writing (`nx.json`'s `release`/`sync`/`generators`/`namedInputs.sharedGlobals`/
462
467
  `mnci` blocks, `.npmrc`,
463
- `eslint.config.mjs`, `commitlint.config.mjs`,
468
+ `eslint.config.mnci.mjs` (**not** `eslint.config.mjs` — see below),
469
+ `commitlint.config.mjs`,
464
470
  `.husky/commit-msg`, the CI pipeline file(s), `.devcontainer/devcontainer.json`, the
465
471
  `<workspace-name>.code-workspace` file, and the curated root `package.json`
466
472
  scripts). Nothing else in the workspace — app/lib source, `project.json` targets
@@ -532,8 +538,52 @@ stack:
532
538
  | --------------- | ------------------ | ------- | ---------------------------------------------------------------------------------------- |
533
539
  | `--test-runner` | `jest` \| `vitest` | `jest` | `nx.json` generator `unitTestRunner` default; the hand-built function app follows it too |
534
540
 
541
+ ### Your half of the ESLint config
542
+
543
+ There are two root ESLint files, and the split exists because the old
544
+ single-file layout lost work. `eslint.config.mjs` used to be mnci-owned and
545
+ rewritten wholesale on every `mnci upgrade` — so a block appended to it, in the
546
+ way a comment mnci itself wrote three lines above described, was deleted
547
+ without a word. Worse, `upgrade` then tells you to run `npm run format`, so the
548
+ first thing that happens after your overrides vanish is every file in the
549
+ repository being rewritten against the rules you thought you had changed.
550
+
551
+ So:
552
+
553
+ | File | Owner | On `mnci upgrade` |
554
+ | --- | --- | --- |
555
+ | `eslint.config.mnci.mjs` | mnci | rewritten every time |
556
+ | `eslint.config.mjs` | you | written once, then never touched |
557
+
558
+ `eslint.config.mjs` is what ESLint loads, because `eslint.config.mjs` is
559
+ ESLint's own default filename — the file the tool looks for has to be the one
560
+ you own, or the tool's default is the one mnci overwrites.
561
+
562
+ ```js
563
+ import mnci from './eslint.config.mnci.mjs'
564
+
565
+ export default [
566
+ ...mnci(),
567
+ { name: 'local/legacy-app-allows-any', files: ['apps/legacy/**/*.ts'], rules: { … } },
568
+ ]
569
+ ```
570
+
571
+ The owned file exports a **function**, not a resolved array, so options still
572
+ reach `@mnci/eslint-config` from the file you own:
573
+ `...mnci({ verticalSlices: ['packages/*/src/**/*.ts'] })`. An array would have
574
+ had nowhere to receive them — mnci's own repository passes `verticalSlices`,
575
+ which is how that was caught.
576
+
577
+ **Upgrading an existing workspace.** If your `eslint.config.mjs` is still the
578
+ old single-file one and you never edited it, `mnci upgrade` moves it onto the
579
+ split for you: it replaces the file only when it provably holds nothing but
580
+ mnci's own output. If you did edit it, it is left exactly as it is and
581
+ `mnci doctor` reports that it no longer imports the rules, with the line to
582
+ add. mnci does not rewrite that file any more — which is the point, and also
583
+ why it cannot do this part for you.
584
+
535
585
  **Linting and formatting are unified across the workspace, from exactly one
536
- config file each.** The root `eslint.config.mjs` is three lines importing
586
+ pair of config files.** The rules are three lines importing
537
587
  [`@mnci/eslint-config`](../eslint-config/README.md); every `@nx/*` generator
538
588
  drops a config into the project it creates, and `mnci add` deletes it. Projects
539
589
  still get their `lint` target: `@nx/eslint/plugin` infers it by mapping config
package/dist/cli.js CHANGED
@@ -419,6 +419,7 @@ function withEslintPlugin(nxJson) {
419
419
  }
420
420
  var SHARED_GLOBAL_INPUTS = [
421
421
  "{workspaceRoot}/eslint.config.mjs",
422
+ "{workspaceRoot}/eslint.config.mnci.mjs",
422
423
  "{workspaceRoot}/tsconfig.base.json",
423
424
  "{workspaceRoot}/package.json"
424
425
  ];
@@ -599,26 +600,15 @@ var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block,
599
600
  //
600
601
  // To list them as ESLint actually resolves them: npx eslint --inspect-config
601
602
  `;
602
- var ESLINT_CONFIG = `import mnci from '@mnci/eslint-config'
603
+ var ESLINT_MNCI_FILENAME = "eslint.config.mnci.mjs";
604
+ var ESLINT_USER_FILENAME = "eslint.config.mjs";
605
+ var ESLINT_MNCI_CONFIG = `// GENERATED BY mnci. Every \`mnci upgrade\` overwrites this file.
606
+ //
607
+ // Do not edit it: put your own blocks in eslint.config.mjs next to it, which
608
+ // imports this one and which mnci never touches once it exists.
609
+ import mnci from '@mnci/eslint-config'
603
610
 
604
611
  ${ESLINT_BLOCK_INVENTORY}//
605
- // TO OVERRIDE a rule, append a block AFTER the spread \u2014 later blocks win, so one
606
- // of your own beats anything above it. Give it a name, so the inspector shows
607
- // where the change came from:
608
- //
609
- // export default [
610
- // ...mnci({ workspaceRoot: import.meta.dirname }),
611
- // {
612
- // name: 'local/legacy-app-allows-any',
613
- // files: ['apps/legacy/**/*.ts'],
614
- // rules: { '@typescript-eslint/no-explicit-any': 'off' }
615
- // }
616
- // ]
617
- //
618
- // Do NOT edit @mnci/eslint-config inside node_modules, and do not fork it: it is
619
- // a dependency, so \`npm update\` brings rule fixes in the way it brings any
620
- // other. An override here survives that; an edit to the package does not.
621
- //
622
612
  // FORMATTING IS LINTING HERE. There is no Prettier, no oxfmt and no
623
613
  // \`format:check\` \u2014 \`npm run lint\` reports indentation, quotes and spacing as
624
614
  // ordinary errors, and \`npm run format\` is \`eslint . --fix\`. So do not add a
@@ -630,8 +620,64 @@ ${ESLINT_BLOCK_INVENTORY}//
630
620
  // enough on its own \u2014 neither needs a config file, and with none present they
631
621
  // format against their own defaults (semicolons, double quotes), which is the
632
622
  // inverse of Standard.
633
- export default mnci({ workspaceRoot: import.meta.dirname })
623
+ //
624
+ // Exported as a FUNCTION, not as the resolved array. @mnci/eslint-config takes
625
+ // options \u2014 \`verticalSlices\` is the one a workspace is most likely to want \u2014
626
+ // and an already-resolved array has nowhere to receive them. Calling it from
627
+ // eslint.config.mjs is what keeps every option reachable from the file you own.
628
+ //
629
+ // \`workspaceRoot\` is resolved HERE because both files sit at the root, and it
630
+ // is what enables the @nx/dependency-checks block.
631
+ //
632
+ // A named function declaration, exported inline. Both shapes matter and both
633
+ // are enforced by this very config: \`unicorn/no-anonymous-default-export\`
634
+ // rejects an anonymous arrow, and \`unicorn/default-export-style\` rejects
635
+ // declaring one above the export and referring to it. Either mistake fails a
636
+ // generated workspace's own lint on its first run.
637
+ export default function mnciConfig (options = {}) {
638
+ return mnci({ workspaceRoot: import.meta.dirname, ...options })
639
+ }
640
+ `;
641
+ var ESLINT_USER_CONFIG = `// This file is YOURS. mnci writes it once and never touches it again, so
642
+ // anything you add here survives \`mnci upgrade\`.
643
+ //
644
+ // The rules live in ./eslint.config.mnci.mjs, which mnci DOES rewrite on every
645
+ // upgrade \u2014 so put your changes here, not there.
646
+ import mnci from './${ESLINT_MNCI_FILENAME}'
647
+
648
+ // TO CONFIGURE the shared rules, pass options to mnci() below \u2014 e.g.
649
+ // \`...mnci({ verticalSlices: ['packages/*/src/**/*.ts'] })\`.
650
+ //
651
+ // TO OVERRIDE a rule, append a block AFTER the spread \u2014 later blocks win, so
652
+ // one of your own beats anything above it. Give it a name, so
653
+ // \`npx eslint --inspect-config\` shows where the change came from:
654
+ //
655
+ // {
656
+ // name: 'local/legacy-app-allows-any',
657
+ // files: ['apps/legacy/**/*.ts'],
658
+ // rules: { '@typescript-eslint/no-explicit-any': 'off' }
659
+ // }
660
+ //
661
+ // Do NOT edit @mnci/eslint-config inside node_modules, and do not fork it: it
662
+ // is a dependency, so \`npm update\` brings rule fixes in the way it brings any
663
+ // other. An override here survives that; an edit to the package does not.
664
+ export default [
665
+ ...mnci(),
666
+ ]
634
667
  `;
668
+ function isUnmodifiedMnciEslintConfig(content) {
669
+ const code = content.split("\n").map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("//"));
670
+ return code.length === 2 && code[0] === "import mnci from '@mnci/eslint-config'" && code[1] === "export default mnci({ workspaceRoot: import.meta.dirname })";
671
+ }
672
+ function writeEslintEntryPoint(workspaceRoot, onProgress) {
673
+ const path = (0, import_node_path2.join)(workspaceRoot, ESLINT_USER_FILENAME);
674
+ if ((0, import_node_fs2.existsSync)(path) && !isUnmodifiedMnciEslintConfig((0, import_node_fs2.readFileSync)(path, "utf8"))) {
675
+ onProgress(`${ESLINT_USER_FILENAME} \u2014 kept as it is, it is yours`);
676
+ return;
677
+ }
678
+ onProgress(`${ESLINT_USER_FILENAME} \u2014 the entry point, yours to edit from now on`);
679
+ writeFileEnsured(path, ESLINT_USER_CONFIG);
680
+ }
635
681
  var VSCODE_RECOMMENDED_EXTENSIONS = [
636
682
  "dbaeumer.vscode-eslint",
637
683
  "nrwl.angular-console",
@@ -1620,8 +1666,9 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
1620
1666
  const hookPath = (0, import_node_path2.join)(workspaceRoot, ".husky/commit-msg");
1621
1667
  writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
1622
1668
  markExecutable(hookPath);
1623
- onProgress("eslint.config.mjs \u2014 the shared lint AND formatting opinion");
1624
- writeFileEnsured((0, import_node_path2.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
1669
+ onProgress(`${ESLINT_MNCI_FILENAME} \u2014 the shared lint AND formatting opinion`);
1670
+ writeFileEnsured((0, import_node_path2.join)(workspaceRoot, ESLINT_MNCI_FILENAME), ESLINT_MNCI_CONFIG);
1671
+ writeEslintEntryPoint(workspaceRoot, onProgress);
1625
1672
  ensureEslintCacheIgnored(workspaceRoot);
1626
1673
  for (const retired of RETIRED_FORMATTER_FILES) {
1627
1674
  removeIfPresent((0, import_node_path2.join)(workspaceRoot, retired));
@@ -4485,6 +4532,21 @@ function checkEslintConfigs(workspaceRoot) {
4485
4532
  ok: projectConfigs.length === 0,
4486
4533
  detail: `found ${projectConfigs.length}: ${projectConfigs.join(", ")}`,
4487
4534
  remedy: "run `mnci upgrade`, which sweeps {apps,libs,packages}/*/eslint.config.*"
4535
+ },
4536
+ ...checkEslintEntryPointReachesTheRules(workspaceRoot)
4537
+ ];
4538
+ }
4539
+ function checkEslintEntryPointReachesTheRules(workspaceRoot) {
4540
+ const rules = (0, import_node_path19.join)(workspaceRoot, ESLINT_MNCI_FILENAME);
4541
+ if (!(0, import_node_fs12.existsSync)(rules)) return [];
4542
+ const entryPoint = (0, import_node_path19.join)(workspaceRoot, ESLINT_USER_FILENAME);
4543
+ const source = (0, import_node_fs12.existsSync)(entryPoint) ? (0, import_node_fs12.readFileSync)(entryPoint, "utf8") : "";
4544
+ return [
4545
+ {
4546
+ check: `${ESLINT_USER_FILENAME} imports the mnci rules`,
4547
+ ok: source.includes(ESLINT_MNCI_FILENAME),
4548
+ detail: source === "" ? `${ESLINT_USER_FILENAME} is missing, so nothing loads the rules` : `${ESLINT_USER_FILENAME} never mentions ${ESLINT_MNCI_FILENAME}`,
4549
+ remedy: `make its first import \`import mnci from './${ESLINT_MNCI_FILENAME}'\` and spread \`...mnci()\` into the exported array, keeping your own blocks after it \u2014 mnci does not rewrite this file, so it cannot do this for you`
4488
4550
  }
4489
4551
  ];
4490
4552
  }