@mnci/cli 1.10.0 → 1.11.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
@@ -121,9 +121,11 @@ that would just fail felt worse than being upfront that it doesn't exist yet.
121
121
  default for): independent versioning from **conventional commits**,
122
122
  `{projectName}@{version}` tags, **tag-only git** (`commit: false`) — nothing
123
123
  is ever pushed to `main`; future runs resolve versions from tag names.
124
- 3. Writes `.npmrc` (Azure Artifacts feed or public npm — scope routing makes
125
- accidental public publishes impossible), `commitlint.config.mjs`, a husky
126
- `commit-msg` hook, the chosen CI provider's pipeline file(s)
124
+ 3. Writes `eslint.config.mjs` (three lines importing `@mnci/eslint-config` —
125
+ the whole linting opinion, in one root config), `.prettierrc.json` +
126
+ `.prettierignore`, `.npmrc` (comment-only — see **Known gaps** below),
127
+ `commitlint.config.mjs`, a husky `commit-msg` hook, the chosen CI provider's
128
+ pipeline file(s)
127
129
  (`azure-pipelines.yml` and/or `.github/workflows/ci.yml`, `--ci`, default
128
130
  `azure`; `github`/`both` also gets `.github/dependabot.yml` — weekly
129
131
  dependency-update PRs), a `<workspace-name>.code-workspace` file (VS Code
@@ -143,9 +145,23 @@ this existed; nothing let an already-generated workspace pick one up.
143
145
  same options `new` would have and calls the exact same `applyOverlay` `new`
144
146
  itself calls — the one function that does every bit of `mnci`-owned file
145
147
  writing (`nx.json`'s `release`/`sync`/`generators`/`mnci` blocks, `.npmrc`,
146
- `commitlint.config.mjs`, `.husky/commit-msg`, the CI pipeline file(s), and the
147
- curated root `package.json` scripts). Nothing else in the workspace — app/lib
148
- source, `project.json` targets from `mnci add` — is ever touched.
148
+ `eslint.config.mjs`, `.prettierrc.json`, `commitlint.config.mjs`,
149
+ `.husky/commit-msg`, the CI pipeline file(s), and the curated root
150
+ `package.json` scripts). Nothing else in the workspace — app/lib source,
151
+ `project.json` targets from `mnci add` — is ever touched.
152
+
153
+ `upgrade` also **deletes** things, which is stronger than the overwriting it
154
+ has always done — one more reason to run `git diff` first, as the command's own
155
+ output tells you to:
156
+
157
+ - `create-nx-workspace`'s `.prettierrc` and `.vscode/`, both superseded (by
158
+ `.prettierrc.json`, which Prettier would otherwise never reach, and by the
159
+ `.code-workspace` file).
160
+ - **every per-project `eslint.config.*` under `apps/`, `libs/` and
161
+ `packages/`.** This is the migration path for a workspace generated before
162
+ mnci owned linting: without it an upgrade would install the root config while
163
+ each project kept linting against its own stale copy. The root config is never
164
+ touched, and neither is a config anywhere outside those three directories.
149
165
 
150
166
  ```sh
151
167
  mnci upgrade # re-apply from persisted config alone
@@ -182,13 +198,33 @@ stack:
182
198
  | --------------- | ------------------ | ------- | ---------------------------------------------------------------------------------------- |
183
199
  | `--test-runner` | `jest` \| `vitest` | `jest` | `nx.json` generator `unitTestRunner` default; the hand-built function app follows it too |
184
200
 
185
- **Linting and formatting are unified across the workspace**: every project uses
186
- **ESLint + Prettier** with the JavaScript Standard Style configuration (no
187
- semicolons, single quotes, 2-space indents). ESLint is a per-project Nx target
188
- handling code quality (correctness only), while Prettier handles all formatting.
189
- Both are automatically configured; `npm run lint` checks code quality, `npm run
190
- format` (write) and `npm run format:check` (CI-safe) handle formatting. The CI
191
- runs lint/test/build only — formatting is left as a local/pre-commit step.
201
+ **Linting and formatting are unified across the workspace, from exactly one
202
+ config file each.** The root `eslint.config.mjs` is three lines importing
203
+ [`@mnci/eslint-config`](../eslint-config/README.md); every `@nx/*` generator
204
+ drops a config into the project it creates, and `mnci add` deletes it. Projects
205
+ still get their `lint` target: `@nx/eslint/plugin` infers it by mapping config
206
+ _directories_ onto the project roots beneath them, so the root config covers
207
+ them all. (Verified, not assumed — and the e2e enforces both "every project has
208
+ a `lint` target" and "the root config genuinely reports violations in a project
209
+ with no config of its own", because a future Nx change there would silently
210
+ switch linting off workspace-wide.)
211
+
212
+ ESLint handles code quality only; **Prettier owns all formatting**, configured
213
+ for JavaScript Standard Style (no semicolons, single quotes, 2-space indents, no
214
+ trailing commas). `mnci` runs Prettier itself at the end of `new` and every
215
+ `add`, so a generated workspace passes its own `format:check` immediately — Nx's
216
+ generators emit semicolons and double quotes, and without that pass the first
217
+ commit buries every real change under generator noise.
218
+
219
+ One caveat worth knowing: `space-before-function-paren`, Standard's signature
220
+ rule, is **not** enforced. Prettier rewrites `function f (a)` to `function f(a)`
221
+ on every run and has closed the corresponding option permanently, so enabling it
222
+ would make `npm run lint` and `npm run format:check` mutually unsatisfiable.
223
+ Choosing Prettier means accepting its call there.
224
+
225
+ `npm run lint` checks code quality; `npm run format` (write) and
226
+ `npm run format:check` (CI-safe) handle formatting. The CI runs lint/test/build
227
+ only — formatting is left as a local/pre-commit step.
192
228
 
193
229
  TypeScript is not a question — every workspace runs the **dual compiler** from
194
230
  [Nx's TS 7 guide](https://nx.dev/docs/technologies/typescript/guides/typescript-7):
@@ -466,6 +502,13 @@ than a surprise:
466
502
 
467
503
  ## Known gaps (accepted for the experiment)
468
504
 
505
+ - **The generated `.npmrc` is empty (comments only), so `npm publish` will not
506
+ authenticate.** This is a deliberate deferral, not an oversight: the previous
507
+ file claimed here that scope routing made accidental public publishes
508
+ impossible, when in fact no `@scope:registry` line was ever emitted. Rather
509
+ than ship half-wired auth, the CI token export (`NODE_AUTH_TOKEN`/`PAT`) is
510
+ left in place so wiring it later is one line, and the generated `.npmrc`
511
+ carries a comment saying so. Publish-path e2e assertions are relaxed to match.
469
512
  - No `doctor`/`resurrect`/`spell` — out of scope until the model is proven.
470
513
  - **Flutter apps build for web only.** Android would require the Android SDK and
471
514
  NDK on every build agent; iOS is impossible on a Linux agent regardless. Add
package/dist/cli.js CHANGED
@@ -43,6 +43,27 @@ var import_prompts4 = require("@inquirer/prompts");
43
43
 
44
44
  // src/nx.ts
45
45
  var import_cross_spawn = __toESM(require("cross-spawn"));
46
+
47
+ // src/util/logger.ts
48
+ var logger = {
49
+ info(message) {
50
+ console.log(message);
51
+ },
52
+ step(message) {
53
+ console.log(`\u2192 ${message}`);
54
+ },
55
+ success(message) {
56
+ console.log(`\u2713 ${message}`);
57
+ },
58
+ warn(message) {
59
+ console.warn(`! ${message}`);
60
+ },
61
+ error(message) {
62
+ console.error(`\u2717 ${message}`);
63
+ }
64
+ };
65
+
66
+ // src/nx.ts
46
67
  function runShell(command, arguments_, cwd) {
47
68
  const result = import_cross_spawn.default.sync(command, arguments_, { stdio: "inherit", cwd });
48
69
  return result.status ?? 1;
@@ -59,6 +80,14 @@ function runNpx(arguments_, cwd) {
59
80
  throw new Error(`npx ${arguments_.join(" ")} failed with exit code ${status}`);
60
81
  }
61
82
  }
83
+ function runPrettier(cwd, target = ".") {
84
+ const status = runShell("npx", ["prettier", "--write", "--log-level", "warn", target], cwd);
85
+ if (status !== 0) {
86
+ logger.warn(
87
+ `Prettier could not format '${target}' (exit code ${status}). The project was generated; run 'npm run format' to normalise it.`
88
+ );
89
+ }
90
+ }
62
91
 
63
92
  // src/prompts.ts
64
93
  var import_prompts = require("@inquirer/prompts");
@@ -136,25 +165,6 @@ function fileExists(path) {
136
165
  return (0, import_node_fs.existsSync)(path);
137
166
  }
138
167
 
139
- // src/util/logger.ts
140
- var logger = {
141
- info(message) {
142
- console.log(message);
143
- },
144
- step(message) {
145
- console.log(`\u2192 ${message}`);
146
- },
147
- success(message) {
148
- console.log(`\u2713 ${message}`);
149
- },
150
- warn(message) {
151
- console.warn(`! ${message}`);
152
- },
153
- error(message) {
154
- console.error(`\u2717 ${message}`);
155
- }
156
- };
157
-
158
168
  // src/util/names.ts
159
169
  function assertValidProjectName(name, label) {
160
170
  if (!/^[a-z][a-z0-9-]*$/.test(name)) {
@@ -202,6 +212,20 @@ function addNxTargets(manifestPath, newTargets) {
202
212
  toJson({ ...manifest, nx: { ...nx, targets: { ...targets, ...newTargets } } })
203
213
  );
204
214
  }
215
+ var ESLINT_CONFIG_FILENAMES = [
216
+ "eslint.config.js",
217
+ "eslint.config.mjs",
218
+ "eslint.config.cjs",
219
+ "eslint.config.ts",
220
+ "eslint.config.mts",
221
+ "eslint.config.cts"
222
+ ];
223
+ function removeGeneratedEslintConfig(workspaceRoot, projectRoot) {
224
+ for (const filename of ESLINT_CONFIG_FILENAMES) {
225
+ (0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, projectRoot, filename), { force: true });
226
+ }
227
+ (0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, ".vscode"), { recursive: true, force: true });
228
+ }
205
229
  function findCodeWorkspaceFile(workspaceRoot) {
206
230
  try {
207
231
  const entry = (0, import_node_fs2.readdirSync)(workspaceRoot).find((file) => file.endsWith(".code-workspace"));
@@ -377,7 +401,6 @@ function goLintTarget() {
377
401
  function goBuildTarget(name) {
378
402
  return {
379
403
  executor: "@nx-go/nx-go:build",
380
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
381
404
  outputs: [`{workspaceRoot}/dist/apps/${name}`],
382
405
  options: { outputPath: `../../dist/apps/${name}/${name}` }
383
406
  };
@@ -388,7 +411,6 @@ function goPackageTarget(tag, name) {
388
411
  return {
389
412
  executor: "nx:run-commands",
390
413
  dependsOn: ["build"],
391
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
392
414
  outputs: [`{workspaceRoot}/${zip}`],
393
415
  options: { command }
394
416
  };
@@ -516,7 +538,7 @@ function runNodeApp(workspaceRoot, name, stack, framework) {
516
538
  `apps/${name}`,
517
539
  "--bundler=esbuild",
518
540
  `--unitTestRunner=${stack.testRunner}`,
519
- "--linter=eslint",
541
+ "--linter=none",
520
542
  "--e2eTestRunner=none",
521
543
  `--framework=${framework}`,
522
544
  "--no-interactive"
@@ -530,7 +552,6 @@ function nodeAppPackageTarget(name) {
530
552
  return {
531
553
  executor: "nx:run-commands",
532
554
  dependsOn: ["build"],
533
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
534
555
  outputs: [`{workspaceRoot}/${zip}`],
535
556
  options: { command }
536
557
  };
@@ -541,6 +562,7 @@ function addNodeApp(workspaceRoot, name, stack, framework = "none") {
541
562
  addNxTargets((0, import_node_path5.join)(workspaceRoot, "apps", name, "package.json"), {
542
563
  package: nodeAppPackageTarget(name)
543
564
  });
565
+ removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
544
566
  registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
545
567
  }
546
568
  function ensureAzureFunctionsPackage(workspaceRoot) {
@@ -564,10 +586,11 @@ var NODE_FUNCTION_APP_HOST_JSON = `{
564
586
  }
565
587
  }
566
588
  `;
567
- var NODE_FUNCTION_APP_HELLO = `import { app, HttpRequest, HttpResponseInit } from '@azure/functions'
589
+ var NODE_FUNCTION_APP_HELLO = `import { app } from '@azure/functions'
590
+ import type { HttpRequest, HttpResponseInit } from '@azure/functions'
568
591
  import { buildGreeting } from './greeting'
569
592
 
570
- async function hello (request: HttpRequest): Promise<HttpResponseInit> {
593
+ async function hello(request: HttpRequest): Promise<HttpResponseInit> {
571
594
  const name = request.query.get('name') ?? 'world'
572
595
  return { body: buildGreeting(name) }
573
596
  }
@@ -614,7 +637,6 @@ function nodeFunctionAppPackageTarget(name) {
614
637
  return {
615
638
  executor: "nx:run-commands",
616
639
  dependsOn: ["build"],
617
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
618
640
  outputs: [`{workspaceRoot}/${zip}`],
619
641
  options: { command }
620
642
  };
@@ -648,62 +670,12 @@ function addNodeFunctionApp(workspaceRoot, name, stack) {
648
670
  package: nodeFunctionAppPackageTarget(name),
649
671
  start: nodeFunctionAppStartTarget(name)
650
672
  });
673
+ removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
651
674
  registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
652
675
  }
653
676
 
654
677
  // src/commands/add/npmLib.ts
655
678
  var import_node_path6 = require("path");
656
- var NPM_LIB_ESLINT_CONFIG = `import { globSync, readFileSync } from 'node:fs';
657
- import { dirname, join } from 'node:path';
658
- import { fileURLToPath } from 'node:url';
659
- import baseConfig from '../../eslint.config.mjs';
660
-
661
- // Private workspace libs are compiled INTO this package's rollup bundle and
662
- // never declared in the manifest (a consumer could not install them), so the
663
- // dependency check must ignore them. Computed at lint time: adding a new
664
- // internal lib never requires touching this file.
665
- const workspaceRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
666
- const privateWorkspacePackages = globSync(['libs/*/package.json', 'packages/*/package.json'], { cwd: workspaceRoot })
667
- .map((manifestPath) => JSON.parse(readFileSync(join(workspaceRoot, manifestPath), 'utf8')))
668
- .filter((manifest) => manifest.private === true)
669
- .map((manifest) => manifest.name);
670
-
671
- export default [
672
- ...baseConfig,
673
- {
674
- files: ['**/*.json'],
675
- rules: {
676
- '@nx/dependency-checks': [
677
- 'error',
678
- {
679
- ignoredDependencies: privateWorkspacePackages,
680
- ignoredFiles: [
681
- '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
682
- '{projectRoot}/rollup.config.{js,ts,mjs,mts,cjs,cts}',
683
- // Same reasoning as the two config files above, extended to the test
684
- // toolchain: rollup bundles from the entry point only, so neither the
685
- // Vitest config nor the spec files reach the published package \u2014 their
686
- // imports must not drive the published manifest.
687
- //
688
- // Without this, a vitest-stack workspace fails \`npm run lint\` on a
689
- // freshly generated npm-lib: Nx's own spec imports \`vitest\`, and
690
- // @nx/dependency-checks then demands it be declared as a runtime
691
- // dependency of a package that never uses it at runtime.
692
- '{projectRoot}/vitest.config.{js,ts,mjs,mts,cjs,cts}',
693
- '{projectRoot}/**/*.spec.{js,ts,jsx,tsx}',
694
- ],
695
- },
696
- ],
697
- },
698
- languageOptions: {
699
- parser: await import('jsonc-eslint-parser'),
700
- },
701
- },
702
- {
703
- ignores: ['**/out-tsc'],
704
- },
705
- ];
706
- `;
707
679
  async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
708
680
  const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
709
681
  runNx(
@@ -715,16 +687,13 @@ async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
715
687
  `--importPath=${scope}/${name}`,
716
688
  "--bundler=rollup",
717
689
  `--unitTestRunner=${stack.testRunner}`,
718
- "--linter=eslint",
690
+ "--linter=none",
719
691
  "--no-interactive"
720
692
  ],
721
693
  workspaceRoot
722
694
  );
723
695
  markPublic((0, import_node_path6.join)(workspaceRoot, "packages", name, "package.json"));
724
- writeFileEnsured(
725
- (0, import_node_path6.join)(workspaceRoot, "packages", name, "eslint.config.mjs"),
726
- NPM_LIB_ESLINT_CONFIG
727
- );
696
+ removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
728
697
  registerProjectCommands(workspaceRoot, name, { build: true });
729
698
  }
730
699
  function markPublic(manifestPath) {
@@ -805,7 +774,6 @@ function pythonAppPackageTarget(name) {
805
774
  return {
806
775
  executor: "nx:run-commands",
807
776
  dependsOn: ["build"],
808
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
809
777
  outputs: [`{workspaceRoot}/${zip}`],
810
778
  options: { command }
811
779
  };
@@ -816,7 +784,6 @@ function pythonFunctionAppPackageTarget(name, moduleDirectory) {
816
784
  const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFile('${root}/function_app.py');z.addLocalFile('${root}/host.json');z.addLocalFile('${root}/requirements.txt');z.addLocalFolder('${root}/${moduleDirectory}','${moduleDirectory}');z.writeZip('${zip}')"`;
817
785
  return {
818
786
  executor: "nx:run-commands",
819
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
820
787
  outputs: [`{workspaceRoot}/${zip}`],
821
788
  options: { command }
822
789
  };
@@ -976,7 +943,6 @@ function reactAppTargets(name) {
976
943
  for (const environment of REACT_ENVIRONMENTS) {
977
944
  targets[`build-${environment}`] = {
978
945
  executor: "nx:run-commands",
979
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation -- {workspaceRoot} is an Nx output token
980
946
  outputs: [`{workspaceRoot}/apps/${name}/dist-${environment}`],
981
947
  options: {
982
948
  command: `vite build --mode ${environment} --outDir dist-${environment}`,
@@ -991,7 +957,6 @@ function reactAppTargets(name) {
991
957
  executor: "nx:run-commands",
992
958
  dependsOn: REACT_ENVIRONMENTS.map((environment) => `build-${environment}`),
993
959
  outputs: REACT_ENVIRONMENTS.map(
994
- // eslint-disable-next-line unicorn/no-incorrect-template-string-interpolation
995
960
  (environment) => `{workspaceRoot}/dist/drop/react-app-${name}-${environment}.zip`
996
961
  ),
997
962
  options: {
@@ -1025,7 +990,7 @@ function addReactApp(workspaceRoot, name, stack) {
1025
990
  `apps/${name}`,
1026
991
  "--bundler=vite",
1027
992
  `--unitTestRunner=${stack.testRunner}`,
1028
- "--linter=eslint",
993
+ "--linter=none",
1029
994
  "--style=css",
1030
995
  "--e2eTestRunner=none",
1031
996
  "--no-interactive"
@@ -1039,6 +1004,7 @@ function addReactApp(workspaceRoot, name, stack) {
1039
1004
  }
1040
1005
  allowEnvFiles(workspaceRoot);
1041
1006
  addNxTargets((0, import_node_path8.join)(reactAppRoot, "package.json"), reactAppTargets(name));
1007
+ removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
1042
1008
  registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
1043
1009
  }
1044
1010
 
@@ -1100,12 +1066,13 @@ async function runAdd(kind, name, options) {
1100
1066
  `libs/${resolvedName}`,
1101
1067
  "--bundler=tsc",
1102
1068
  `--unitTestRunner=${stack.testRunner}`,
1103
- "--linter=eslint",
1069
+ "--linter=none",
1104
1070
  "--no-interactive"
1105
1071
  ],
1106
1072
  workspaceRoot
1107
1073
  );
1108
1074
  markPrivate((0, import_node_path9.join)(workspaceRoot, "libs", resolvedName, "package.json"));
1075
+ removeGeneratedEslintConfig(workspaceRoot, `libs/${resolvedName}`);
1109
1076
  registerProjectCommands(workspaceRoot, resolvedName, { build: true });
1110
1077
  break;
1111
1078
  }
@@ -1156,6 +1123,7 @@ async function runAdd(kind, name, options) {
1156
1123
  case "python-vendor": {
1157
1124
  await addPythonVendor(workspaceRoot, resolvedName, options, kindProvided);
1158
1125
  syncProjectReferences(workspaceRoot);
1126
+ runPrettier(workspaceRoot);
1159
1127
  return;
1160
1128
  }
1161
1129
  default: {
@@ -1166,6 +1134,7 @@ async function runAdd(kind, name, options) {
1166
1134
  }
1167
1135
  }
1168
1136
  syncProjectReferences(workspaceRoot);
1137
+ runPrettier(workspaceRoot);
1169
1138
  logger.success(`Added ${resolvedKind} '${resolvedName}'.`);
1170
1139
  }
1171
1140
  function syncProjectReferences(workspaceRoot) {
@@ -1198,6 +1167,7 @@ var import_prompts7 = require("@inquirer/prompts");
1198
1167
  var import_node_path11 = require("path");
1199
1168
 
1200
1169
  // src/overlay.ts
1170
+ var import_node_fs6 = require("fs");
1201
1171
  var import_node_path10 = require("path");
1202
1172
  var DEFAULT_STACK = { testRunner: "jest" };
1203
1173
  var PRETTIER_VERSION = "^3.8.1";
@@ -1205,34 +1175,16 @@ var TS_COMPILER_DEPENDENCIES = {
1205
1175
  "@typescript/native": "npm:typescript@^7.0.2",
1206
1176
  typescript: "npm:@typescript/typescript6@^6.0.2"
1207
1177
  };
1208
- function npmrcContent(registry, scope) {
1209
- const scopeName = scope.replace(/^@/, "");
1210
- const lines = [
1211
- "registry=https://registry.npmjs.org/",
1212
- "; Community Nx plugins (e.g. the Azure Functions one) peer on the previous",
1213
- "; Nx major for a while after each release; accept the resolved tree.",
1214
- "legacy-peer-deps=true"
1215
- ];
1216
- if (registry.kind === "azure-artifacts") {
1217
- const base = `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}`;
1218
- const registryHost = `${base}/npm/registry/`.replace(/^https:\/\//, "");
1219
- const feedHost = `${base}/`.replace(/^https:\/\//, "");
1220
- lines.push(
1221
- `@${scopeName}:registry=${base}/npm/registry/`,
1222
- "; Azure Artifacts auth \u2014 the base64 PAT is expanded at runtime from the",
1223
- "; PAT env var (CI: your PAT variable group; locally: export PAT=<base64 token>).",
1224
- `//${registryHost}:username=${registry.organization}`,
1225
- `//${registryHost}:_password=\${PAT}`,
1226
- `//${registryHost}:email=npm@example.com`,
1227
- `//${feedHost}:username=${registry.organization}`,
1228
- `//${feedHost}:_password=\${PAT}`,
1229
- `//${feedHost}:email=npm@example.com`
1230
- );
1231
- } else {
1232
- lines.push(`//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}`);
1233
- }
1234
- lines.push("");
1235
- return lines.join("\n");
1178
+ function npmrcContent(_registry, _scope) {
1179
+ return `; Intentionally empty.
1180
+ ;
1181
+ ; Publish authentication is not configured yet. The generated CI exports a
1182
+ ; token for the release step (NODE_AUTH_TOKEN for public npm, PAT for Azure
1183
+ ; Artifacts), but nothing here consumes it, so 'npm publish' will not
1184
+ ; authenticate until this is wired up.
1185
+ ;
1186
+ ; See "Known gaps" in the @mnci/cli README.
1187
+ `;
1236
1188
  }
1237
1189
  function releaseConfig(ci) {
1238
1190
  const githubReleases = ci === "github";
@@ -1259,6 +1211,18 @@ function releaseConfig(ci) {
1259
1211
  };
1260
1212
  }
1261
1213
  var SYNC_CONFIG = { applyChanges: true };
1214
+ var ESLINT_PLUGIN_CONFIG = {
1215
+ plugin: "@nx/eslint/plugin",
1216
+ options: { targetName: "lint" }
1217
+ };
1218
+ function pluginName(entry) {
1219
+ return typeof entry === "string" ? entry : entry.plugin;
1220
+ }
1221
+ function withEslintPlugin(nxJson) {
1222
+ const plugins = nxJson.plugins ?? [];
1223
+ const registered = plugins.some((entry) => pluginName(entry) === ESLINT_PLUGIN_CONFIG.plugin);
1224
+ return registered ? { ...nxJson, plugins } : { ...nxJson, plugins: [...plugins, ESLINT_PLUGIN_CONFIG] };
1225
+ }
1262
1226
  function withReleaseConfig(nxJson, ci) {
1263
1227
  return { ...nxJson, defaultBase: "main", release: releaseConfig(ci) };
1264
1228
  }
@@ -1269,13 +1233,30 @@ var COMMIT_MSG_HOOK = `npx --no -- commitlint --edit "$1"
1269
1233
  var PRETTIER_CONFIG = `{
1270
1234
  "semi": false,
1271
1235
  "singleQuote": true,
1272
- "trailingComma": "es5",
1236
+ "trailingComma": "none",
1273
1237
  "arrowParens": "avoid",
1274
1238
  "printWidth": 100,
1275
1239
  "tabWidth": 2,
1276
1240
  "useTabs": false
1277
1241
  }
1278
1242
  `;
1243
+ var ESLINT_CONFIG_VERSION = "^0.1.0";
1244
+ function eslintConfigSpec() {
1245
+ return process.env.MNCI_ESLINT_CONFIG_SPEC ?? ESLINT_CONFIG_VERSION;
1246
+ }
1247
+ var ESLINT_VERSION = "^9.39.0";
1248
+ function eslintToolchainDependencies(nxVersion) {
1249
+ return {
1250
+ eslint: ESLINT_VERSION,
1251
+ "@nx/eslint": nxVersion,
1252
+ "@nx/eslint-plugin": nxVersion,
1253
+ "@mnci/eslint-config": eslintConfigSpec()
1254
+ };
1255
+ }
1256
+ var ESLINT_CONFIG = `import mnci from '@mnci/eslint-config'
1257
+
1258
+ export default mnci({ workspaceRoot: import.meta.dirname })
1259
+ `;
1279
1260
  var PRETTIER_IGNORE = `node_modules
1280
1261
  dist
1281
1262
  dist-dev
@@ -1352,7 +1333,13 @@ function rootScripts() {
1352
1333
  }
1353
1334
  function generatorDefaults(stack) {
1354
1335
  const shared = {
1355
- linter: "eslint",
1336
+ // `none`, not `eslint`, and the workspace is still fully linted. The root
1337
+ // config plus `@nx/eslint/plugin` ({@link ESLINT_PLUGIN_CONFIG}) give every
1338
+ // project its `lint` target; `eslint` here would only make the generator
1339
+ // scaffold a per-project config mnci deletes anyway — and drag in
1340
+ // `eslint-plugin-import@2.31.0`, which peer-caps at ESLint 9 and breaks the
1341
+ // install outright on this workspace's ESLint 10.
1342
+ linter: "none",
1356
1343
  unitTestRunner: stack.testRunner
1357
1344
  };
1358
1345
  return {
@@ -1797,6 +1784,21 @@ updates:
1797
1784
  schedule:
1798
1785
  interval: weekly
1799
1786
  `;
1787
+ var NX_SCAFFOLDING_TO_REMOVE = [".prettierrc", ".vscode"];
1788
+ function removeNxScaffolding(workspaceRoot) {
1789
+ for (const entry of NX_SCAFFOLDING_TO_REMOVE) {
1790
+ (0, import_node_fs6.rmSync)((0, import_node_path10.join)(workspaceRoot, entry), { recursive: true, force: true });
1791
+ }
1792
+ removeProjectEslintConfigs(workspaceRoot);
1793
+ }
1794
+ function removeProjectEslintConfigs(workspaceRoot) {
1795
+ const matches = (0, import_node_fs6.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
1796
+ cwd: workspaceRoot
1797
+ });
1798
+ for (const match of matches) {
1799
+ (0, import_node_fs6.rmSync)((0, import_node_path10.join)(workspaceRoot, match), { force: true });
1800
+ }
1801
+ }
1800
1802
  function applyOverlay(workspaceRoot, options) {
1801
1803
  const nxJsonPath = (0, import_node_path10.join)(workspaceRoot, "nx.json");
1802
1804
  const nxJson = readJson(nxJsonPath);
@@ -1806,19 +1808,20 @@ function applyOverlay(workspaceRoot, options) {
1806
1808
  };
1807
1809
  const sync = { ...nxJson.sync, ...SYNC_CONFIG };
1808
1810
  const mnci = { ...nxJson.mnci, ...mnciConfig(options) };
1809
- writeFileEnsured(
1810
- nxJsonPath,
1811
- toJson({ ...withReleaseConfig(nxJson, options.ci), generators, sync, mnci })
1812
- );
1811
+ const patched = withEslintPlugin(withReleaseConfig(nxJson, options.ci));
1812
+ writeFileEnsured(nxJsonPath, toJson({ ...patched, generators, sync, mnci }));
1813
1813
  const manifestPath = (0, import_node_path10.join)(workspaceRoot, "package.json");
1814
1814
  const manifest = readJson(manifestPath);
1815
1815
  const scripts = {
1816
1816
  ...manifest.scripts,
1817
1817
  ...rootScripts()
1818
1818
  };
1819
+ const existingDevDeps = manifest.devDependencies;
1819
1820
  const devDeps = {
1820
- ...manifest.devDependencies,
1821
+ ...existingDevDeps,
1821
1822
  ...TS_COMPILER_DEPENDENCIES,
1823
+ // The preset pins `nx` itself; the ESLint plugins must match it exactly.
1824
+ ...eslintToolchainDependencies(existingDevDeps?.nx ?? "latest"),
1822
1825
  prettier: PRETTIER_VERSION
1823
1826
  };
1824
1827
  writeFileEnsured(
@@ -1830,8 +1833,10 @@ function applyOverlay(workspaceRoot, options) {
1830
1833
  const hookPath = (0, import_node_path10.join)(workspaceRoot, ".husky/commit-msg");
1831
1834
  writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
1832
1835
  markExecutable(hookPath);
1836
+ writeFileEnsured((0, import_node_path10.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
1833
1837
  writeFileEnsured((0, import_node_path10.join)(workspaceRoot, ".prettierrc.json"), PRETTIER_CONFIG);
1834
1838
  writeFileEnsured((0, import_node_path10.join)(workspaceRoot, ".prettierignore"), PRETTIER_IGNORE);
1839
+ removeNxScaffolding(workspaceRoot);
1835
1840
  writeFileEnsured(
1836
1841
  (0, import_node_path10.join)(workspaceRoot, `${options.workspaceName}.code-workspace`),
1837
1842
  vscodeWorkspace(options.workspaceName)
@@ -1922,7 +1927,7 @@ async function runNew(name, options) {
1922
1927
  "Applying MoNecromanCI overlay (VS Code workspace, release config, .npmrc, commitlint, pipeline, stack)"
1923
1928
  );
1924
1929
  applyOverlay(workspaceRoot, { workspaceName, scope, registry, agent, variableGroup, ci, stack });
1925
- logger.step("Installing the toolchain (commitlint + husky)");
1930
+ logger.step("Installing the toolchain (commitlint + husky + linting/formatting)");
1926
1931
  const installStatus = runShell(
1927
1932
  "npm",
1928
1933
  ["install", "--save-dev", "husky", "@commitlint/cli", "@commitlint/config-conventional"],
@@ -1932,6 +1937,8 @@ async function runNew(name, options) {
1932
1937
  throw new Error(`npm install of the toolchain failed with exit code ${installStatus}`);
1933
1938
  }
1934
1939
  runShell("npx", ["husky"], workspaceRoot);
1940
+ logger.step("Formatting the workspace (Prettier, JavaScript Standard Style)");
1941
+ runPrettier(workspaceRoot);
1935
1942
  logger.success("Done. Next steps:");
1936
1943
  logger.info(` cd ${workspaceName}`);
1937
1944
  logger.info(
@@ -2039,7 +2046,7 @@ async function runInteractive() {
2039
2046
  }
2040
2047
 
2041
2048
  // src/util/versionChecker.ts
2042
- var import_node_fs6 = require("fs");
2049
+ var import_node_fs7 = require("fs");
2043
2050
  var import_node_path14 = require("path");
2044
2051
  var import_cross_spawn2 = __toESM(require("cross-spawn"));
2045
2052
  var PACKAGE_NAME = "@mnci/cli";
@@ -2097,7 +2104,7 @@ function releaseTriple(version) {
2097
2104
  function readCliVersion(packageDirectory) {
2098
2105
  try {
2099
2106
  const manifestPath = (0, import_node_path14.join)(packageDirectory, "..", "package.json");
2100
- const manifest = JSON.parse((0, import_node_fs6.readFileSync)(manifestPath, "utf8"));
2107
+ const manifest = JSON.parse((0, import_node_fs7.readFileSync)(manifestPath, "utf8"));
2101
2108
  return manifest.version ?? "0.0.0";
2102
2109
  } catch {
2103
2110
  return "0.0.0";