@mnci/cli 4.5.0 → 4.6.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
@@ -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
@@ -313,8 +313,24 @@ function npmrcContent(registry, scope) {
313
313
  if (registry.kind === "npm") {
314
314
  return `; Publish authentication for the public npm registry.
315
315
  ;
316
- ; NODE_AUTH_TOKEN is exported by the generated CI's release step; an unset value
317
- ; is harmless locally, so no token is needed for day-to-day work.
316
+ ; NODE_AUTH_TOKEN is exported by the generated CI's release step. Nothing needs
317
+ ; it for day-to-day work here - installing, building and testing never
318
+ ; authenticate.
319
+ ;
320
+ ; It is NOT harmless the moment you DO want to authenticate from this directory,
321
+ ; which an earlier version of this comment claimed. A project .npmrc wins over
322
+ ; your user one, so with NODE_AUTH_TOKEN unset this line hands npm an empty
323
+ ; token and overrides 'npm login': inside this repo 'npm whoami' fails with a
324
+ ; 401 and any write is refused. The refusal is easy to misread, because for a
325
+ ; scoped package the registry answers a write you are not allowed to make with
326
+ ;
327
+ ; npm error 404 Not Found - PUT https://registry.npmjs.org/@scope%2fthing
328
+ ;
329
+ ; and everything above it is npm listing what it intended to send, so a long
330
+ ; healthy-looking run still changes nothing.
331
+ ;
332
+ ; To publish or deprecate by hand, run it from outside this directory, or export
333
+ ; a real token first.
318
334
  //registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}
319
335
 
320
336
  ; There is deliberately NO '${scope}:registry' line here. npmjs.org is already
@@ -403,6 +419,7 @@ function withEslintPlugin(nxJson) {
403
419
  }
404
420
  var SHARED_GLOBAL_INPUTS = [
405
421
  "{workspaceRoot}/eslint.config.mjs",
422
+ "{workspaceRoot}/eslint.config.mnci.mjs",
406
423
  "{workspaceRoot}/tsconfig.base.json",
407
424
  "{workspaceRoot}/package.json"
408
425
  ];
@@ -583,26 +600,15 @@ var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block,
583
600
  //
584
601
  // To list them as ESLint actually resolves them: npx eslint --inspect-config
585
602
  `;
586
- 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'
587
610
 
588
611
  ${ESLINT_BLOCK_INVENTORY}//
589
- // TO OVERRIDE a rule, append a block AFTER the spread \u2014 later blocks win, so one
590
- // of your own beats anything above it. Give it a name, so the inspector shows
591
- // where the change came from:
592
- //
593
- // export default [
594
- // ...mnci({ workspaceRoot: import.meta.dirname }),
595
- // {
596
- // name: 'local/legacy-app-allows-any',
597
- // files: ['apps/legacy/**/*.ts'],
598
- // rules: { '@typescript-eslint/no-explicit-any': 'off' }
599
- // }
600
- // ]
601
- //
602
- // Do NOT edit @mnci/eslint-config inside node_modules, and do not fork it: it is
603
- // a dependency, so \`npm update\` brings rule fixes in the way it brings any
604
- // other. An override here survives that; an edit to the package does not.
605
- //
606
612
  // FORMATTING IS LINTING HERE. There is no Prettier, no oxfmt and no
607
613
  // \`format:check\` \u2014 \`npm run lint\` reports indentation, quotes and spacing as
608
614
  // ordinary errors, and \`npm run format\` is \`eslint . --fix\`. So do not add a
@@ -614,8 +620,64 @@ ${ESLINT_BLOCK_INVENTORY}//
614
620
  // enough on its own \u2014 neither needs a config file, and with none present they
615
621
  // format against their own defaults (semicolons, double quotes), which is the
616
622
  // inverse of Standard.
617
- 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
+ }
618
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
+ ]
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
+ }
619
681
  var VSCODE_RECOMMENDED_EXTENSIONS = [
620
682
  "dbaeumer.vscode-eslint",
621
683
  "nrwl.angular-console",
@@ -873,6 +935,10 @@ var NPM_AUDIT_STEP = `node -e "const cp=require('node:child_process');const r=cp
873
935
  var VERIFY_TARGETS = "lint,typecheck,test,build";
874
936
  var AFFECTED_OR_ALL_GUARD = `node -e "const cp=require('node:child_process');const T='${VERIFY_TARGETS}';const all=()=>process.exit(cp.spawnSync('npx nx run-many -t '+T,{stdio:'inherit',shell:true}).status ?? 1);const ref=process.env.GITHUB_BASE_REF||process.env.SYSTEM_PULLREQUEST_TARGETBRANCH||'';if(!ref){console.log('Not a pull request - verifying EVERY project.');all()}const target=ref.replace('refs/heads/','');const mergeBase=r=>{const o=cp.spawnSync('git',['merge-base',r,'HEAD'],{encoding:'utf8'});return o.status===0?o.stdout.trim():''};let base=mergeBase('origin/'+target);if(!base){console.log('No origin/'+target+' ref - fetching it to resolve a merge-base.');cp.spawnSync('git',['fetch','--no-tags','origin',target],{stdio:'inherit'});base=mergeBase('FETCH_HEAD')}if(!base){console.log('Could not resolve a merge-base with '+target+' - verifying EVERY project.');all()}console.log('Pull request against '+target+' - verifying projects affected since '+base);process.exit(cp.spawnSync('npx nx affected -t '+T+' --base='+base,{stdio:'inherit',shell:true}).status ?? 1)"`;
875
937
  var PACK_APPS_GUARD = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const hasProjectJson=fs.globSync('apps/*/project.json').length>0;const hasInlineNx=fs.globSync('apps/*/package.json').some((f)=>{try{return Boolean(JSON.parse(fs.readFileSync(f,'utf8')).nx)}catch{return false}});const hasCsproj=fs.globSync('apps/*/*.csproj').length>0;if(!hasProjectJson&&!hasInlineNx&&!hasCsproj){console.log('No apps to pack - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('npx nx run-many -t package',{stdio:'inherit',shell:true}).status ?? 1)"`;
938
+ var NPM_AUTH_PREFLIGHT = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(fs.globSync('packages/*/package.json').length===0){console.log('No npm packages to release - skipping.');process.exit(0)}if(!process.env.NODE_AUTH_TOKEN){console.error('NPM_TOKEN is empty or unset, so the publish would fail AFTER nx release has already tagged. Add it as a repository secret named exactly NPM_TOKEN - under Actions, not the Dependabot or Codespaces tab, and not an Environment secret. Use an npm Automation token - a Publish token is refused by 2FA in CI.');process.exit(1)}const r=cp.spawnSync('npm',['whoami','--registry=https://registry.npmjs.org/'],{encoding:'utf8',shell:process.platform==='win32'});if(r.status!==0){console.error('NPM_TOKEN is set but the registry rejected it - '+((r.stderr||'')+(r.stdout||'')).trim()+'. Check that it has not expired and that it grants publish rights on this scope.');process.exit(1)}console.log('npm auth OK as '+r.stdout.trim())"`;
939
+ function npmAuthPreflight(registryKind) {
940
+ return registryKind === "npm" ? NPM_AUTH_PREFLIGHT : "";
941
+ }
876
942
  var SHALLOW_CLONE_GUARD = `node -e "const r=require('node:child_process').spawnSync('git',['rev-parse','--is-shallow-repository'],{encoding:'utf8'});if(r.status!==0){console.error('Could not determine whether this checkout has full history (git rev-parse --is-shallow-repository failed) - refusing to release. '+(r.stderr||'').trim());process.exit(1)}if(r.stdout.trim()==='true'){console.error('This checkout is a shallow clone. nx release resolves the current version of each package from its git tag, and silently falls back to the permanently-stale on-disk version when a tag cannot be found - which can propose, and publish, a version DOWNGRADE. Fetch full history before releasing - set fetchDepth (Azure) or fetch-depth (GitHub) to 0.');process.exit(1)}"`;
877
943
  function releaseGuard(pythonPublishEnv, nugetPublishEnv) {
878
944
  return String.raw`node -e "const fs=require('node:fs'),cp=require('node:child_process');const npmCount=fs.globSync('packages/*/package.json').length;const csharpCount=fs.globSync('packages/*/*.csproj').length;const pythonCount=fs.globSync('python-packages/*/pyproject.toml').length;const dartCount=fs.globSync('packages/*/pubspec.yaml').length;const hasNpm=npmCount>0;const hasPython=pythonCount>0;const hasCsharp=csharpCount>0;const hasDart=dartCount>0;if(!hasNpm&&!hasPython&&!hasCsharp&&!hasDart){console.log('Nothing to release - skipping.');process.exit(0)}const specifier=process.env.RELEASE_SPECIFIER||'';let releaseCmd='npx nx release --yes';if(specifier){if(!/^(major|minor|patch|\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?)$/.test(specifier)){console.error('RELEASE_SPECIFIER value \''+specifier+'\' is invalid - use major, minor, patch, or an exact version like 1.2.3.');process.exit(1)}const releaseProjectCount=npmCount+csharpCount+pythonCount+dartCount;if(/^(major|minor|patch)$/.test(specifier)&&releaseProjectCount>1){console.error('RELEASE_SPECIFIER is a keyword (\''+specifier+'\') but this workspace has '+releaseProjectCount+' releasable packages - a keyword under-bumps interdependent packages, because nx computes the dependency-bump pass from a stale cached version. Set RELEASE_SPECIFIER to an exact version instead, or clear it.');process.exit(1)}releaseCmd='npx nx release '+specifier+' --yes'}const env={...process.env};${pythonPublishEnv}${nugetPublishEnv}process.exit(cp.spawnSync(releaseCmd,{stdio:'inherit',shell:true,env}).status ?? 1)"`;
@@ -1148,7 +1214,17 @@ steps:
1148
1214
  - script: ${SHALLOW_CLONE_GUARD}
1149
1215
  displayName: Verify this is a full checkout (nx release needs the real tag history)
1150
1216
  condition: ${onMain}
1151
-
1217
+ ${npmAuthPreflight(registryKind) ? `
1218
+ # Prove npm will accept us BEFORE anything irreversible happens. nx release
1219
+ # tags first and publishes last, so a rejected token leaves a version tagged
1220
+ # with nothing on the registry - and that number is then skipped forever,
1221
+ # because the next run resolves the current version from the newest tag.
1222
+ - script: ${npmAuthPreflight(registryKind)}
1223
+ displayName: Preflight \u2014 npm must accept the token before anything is tagged
1224
+ condition: ${onMain}
1225
+ env:
1226
+ NODE_AUTH_TOKEN: $(NPM_TOKEN)
1227
+ ` : ""}
1152
1228
  # Version + tag + publish, in one release, for npm (packages/*), Python
1153
1229
  # (python-packages/*) AND C# (packages/*/*.csproj) \u2014 conventional commits,
1154
1230
  # tag-only push. Portable guard: nx release errors on an empty scope, so
@@ -1374,7 +1450,18 @@ jobs:
1374
1450
  - run: ${SHALLOW_CLONE_GUARD}
1375
1451
  name: Verify this is a full checkout (nx release needs the real tag history)
1376
1452
  if: \${{ ${onMain} }}
1377
-
1453
+ ${npmAuthPreflight(registryKind) ? `
1454
+ # Prove npm will accept us BEFORE anything irreversible happens. nx
1455
+ # release tags first and publishes last, so a rejected token leaves a
1456
+ # version tagged with nothing on the registry - and that number is then
1457
+ # skipped forever, because the next run resolves the current version
1458
+ # from the newest tag.
1459
+ - run: ${npmAuthPreflight(registryKind)}
1460
+ name: Preflight \u2014 npm must accept the token before anything is tagged
1461
+ if: \${{ ${onMain} }}
1462
+ env:
1463
+ NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
1464
+ ` : ""}
1378
1465
  # Version + tag + publish, in one release, for npm (packages/*), Python
1379
1466
  # (python-packages/*) AND C# (packages/*/*.csproj) \u2014 conventional
1380
1467
  # commits, tag-only push. Portable guard: nx release errors on an empty
@@ -1579,8 +1666,9 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
1579
1666
  const hookPath = (0, import_node_path2.join)(workspaceRoot, ".husky/commit-msg");
1580
1667
  writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
1581
1668
  markExecutable(hookPath);
1582
- onProgress("eslint.config.mjs \u2014 the shared lint AND formatting opinion");
1583
- 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);
1584
1672
  ensureEslintCacheIgnored(workspaceRoot);
1585
1673
  for (const retired of RETIRED_FORMATTER_FILES) {
1586
1674
  removeIfPresent((0, import_node_path2.join)(workspaceRoot, retired));
@@ -4444,6 +4532,21 @@ function checkEslintConfigs(workspaceRoot) {
4444
4532
  ok: projectConfigs.length === 0,
4445
4533
  detail: `found ${projectConfigs.length}: ${projectConfigs.join(", ")}`,
4446
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`
4447
4550
  }
4448
4551
  ];
4449
4552
  }