@mnci/cli 1.16.0 → 1.17.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
@@ -78,7 +78,7 @@ ever needed is noise that trains people to ignore the output.
78
78
  | Check | The failure it catches |
79
79
  | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
80
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.json`, so the whole formatting opinion is silently discarded while both files look fine |
81
+ | No stray `.prettierrc` | It outranks `.prettierrc.mjs`, so the whole formatting opinion is silently discarded while both files look fine |
82
82
  | `@nx/eslint/plugin` registered in `nx.json` | Without it `npm run lint` exits 0 while linting nothing |
83
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
84
  | `.npmrc` matches the recorded registry | The two registry kinds get different files; an Azure workspace also needs its scope routed |
@@ -152,9 +152,11 @@ that would just fail felt worse than being upfront that it doesn't exist yet.
152
152
  changing one marked only the root pseudo-project — which has no
153
153
  lint/typecheck/test/build target — and the affected-scoped verify step ran
154
154
  nothing at all while reporting green.
155
- 3. Writes `eslint.config.mjs` (three lines importing `@mnci/eslint-config` —
156
- the whole linting opinion, in one root config), `.prettierrc.json` +
157
- `.prettierignore`, `.npmrc` (publish auth — see **Publish auth** below),
155
+ 3. Writes `eslint.config.mjs` (one import from `@mnci/eslint-config` — the whole
156
+ linting opinion, in one root config — plus a commented inventory naming every
157
+ config block and how to override it), `.prettierrc.mjs` (which re-exports
158
+ `@mnci/eslint-config/prettier`, so lint and format ship as one package and
159
+ upgrade together) + `.prettierignore`, `.npmrc` (publish auth — see **Publish auth** below),
158
160
  `commitlint.config.mjs`, a husky `commit-msg` hook, the chosen CI provider's
159
161
  pipeline file(s)
160
162
  (`azure-pipelines.yml` and/or `.github/workflows/ci.yml`, `--ci`, default
@@ -177,8 +179,8 @@ same options `new` would have and calls the exact same `applyOverlay` `new`
177
179
  itself calls — the one function that does every bit of `mnci`-owned file
178
180
  writing (`nx.json`'s `release`/`sync`/`generators`/`namedInputs.sharedGlobals`/
179
181
  `mnci` blocks, `.npmrc`,
180
- `eslint.config.mjs`, `.prettierrc.json`, `commitlint.config.mjs`,
181
- `.husky/commit-msg`, the CI pipeline file(s), the
182
+ `eslint.config.mjs`, `.prettierrc.mjs`, `commitlint.config.mjs`,
183
+ `.husky/commit-msg`, the CI pipeline file(s), `.devcontainer/devcontainer.json`, the
182
184
  `<workspace-name>.code-workspace` file, and the curated root `package.json`
183
185
  scripts). Nothing else in the workspace — app/lib source, `project.json` targets
184
186
  from `mnci add` — is ever touched, and it finishes by running Prettier over the
@@ -194,9 +196,10 @@ project's build/qa/start entry on upgrade.
194
196
  has always done — one more reason to run `git diff` first, as the command's own
195
197
  output tells you to:
196
198
 
197
- - `create-nx-workspace`'s `.prettierrc` and `.vscode/`, both superseded (by
198
- `.prettierrc.json`, which Prettier would otherwise never reach, and by the
199
- `.code-workspace` file).
199
+ - `create-nx-workspace`'s `.prettierrc` and `.vscode/`, plus mnci's own former
200
+ `.prettierrc.json`. The two Prettier files are deleted for the same reason:
201
+ both outrank `.prettierrc.mjs`, which Prettier would otherwise never reach.
202
+ `.vscode/` is superseded by the `.code-workspace` file.
200
203
  - **every per-project `eslint.config.*` under `apps/`, `libs/` and
201
204
  `packages/`.** This is the migration path for a workspace generated before
202
205
  mnci owned linting: without it an upgrade would install the root config while
@@ -251,11 +254,26 @@ switch linting off workspace-wide.)
251
254
 
252
255
  ESLint handles code quality only; **Prettier owns all formatting**, configured
253
256
  for JavaScript Standard Style (no semicolons, single quotes, 2-space indents, no
254
- trailing commas). `mnci` runs Prettier itself at the end of `new` and every
257
+ trailing commas). Both opinions come from the same package: `.prettierrc.mjs`
258
+ re-exports `@mnci/eslint-config/prettier`, so a formatting fix arrives through
259
+ `npm update` exactly as a rule fix does, and the two cannot drift into
260
+ contradicting each other. `mnci` runs Prettier itself at the end of `new` and every
255
261
  `add`, so a generated workspace passes its own `format:check` immediately — Nx's
256
262
  generators emit semicolons and double quotes, and without that pass the first
257
263
  commit buries every real change under generator noise.
258
264
 
265
+ **Finding and overriding a rule.** One root config importing a package is easy to
266
+ own and hard to read: nothing in three lines says which of ~20 plugins reported
267
+ the thing you disagree with. Every block in `@mnci/eslint-config` therefore
268
+ carries a `name` — `mnci/base`, `mnci/react`, `mnci/type-aware`,
269
+ `mnci/prettier-compat` — and the generated `eslint.config.mjs` ships the full list
270
+ as a comment, next to the recipe for overriding one. `npx eslint --inspect-config`
271
+ lists them as ESLint actually resolves them, `npx eslint --print-config <file>`
272
+ shows the merged result for a single file, and an override is a named block
273
+ appended after the spread, since later blocks win. Don't edit the package inside
274
+ `node_modules`: it is a dependency, and `npm update` will replace it.
275
+ `packages/eslint-config/README.md` has the block-by-block table.
276
+
259
277
  One caveat worth knowing: `space-before-function-paren`, Standard's signature
260
278
  rule, is **not** enforced. Prettier rewrites `function f (a)` to `function f(a)`
261
279
  on every run and has closed the corresponding option permanently, so enabling it
package/dist/cli.js CHANGED
@@ -1350,21 +1350,55 @@ var COMMITLINT_CONFIG = `export default { extends: ['@commitlint/config-conventi
1350
1350
  `;
1351
1351
  var COMMIT_MSG_HOOK = `npx --no -- commitlint --edit "$1"
1352
1352
  `;
1353
- var PRETTIER_CONFIG = `{
1354
- "semi": false,
1355
- "singleQuote": true,
1356
- "trailingComma": "none",
1357
- "arrowParens": "avoid",
1358
- "printWidth": 100,
1359
- "tabWidth": 2,
1360
- "useTabs": false
1361
- }
1353
+ var PRETTIER_CONFIG = `// The formatting half of @mnci/eslint-config.
1354
+ //
1355
+ // Prettier's options live in the same package as the ESLint rules on purpose:
1356
+ // linting and formatting are one decision. \`eslint-config-prettier\` is composed
1357
+ // last inside that package so every formatting rule defers to these settings, and
1358
+ // splitting the two across packages means a version pair that can drift until
1359
+ // \`npm run lint\` and \`npm run format:check\` disagree with each other.
1360
+ //
1361
+ // Consuming it as a shareable config also means a formatting fix reaches this
1362
+ // workspace through \`npm update\`, the same way a rule change does, instead of
1363
+ // waiting for \`mnci upgrade\` to rewrite a file.
1364
+ //
1365
+ // TO OVERRIDE an option, spread it rather than re-exporting:
1366
+ //
1367
+ // import mnci from '@mnci/eslint-config/prettier'
1368
+ // export default { ...mnci, printWidth: 120 }
1369
+ //
1370
+ // TO SEE the resolved options for a file: npx prettier --find-config-path <file>
1371
+ export { default } from '@mnci/eslint-config/prettier'
1362
1372
  `;
1363
1373
  var ESLINT_CONFIG_VERSION = "^0.1.0";
1364
1374
  function eslintConfigSpec() {
1365
1375
  return process.env.MNCI_ESLINT_CONFIG_SPEC ?? ESLINT_CONFIG_VERSION;
1366
1376
  }
1367
1377
  var ESLINT_VERSION = "^10.8.0";
1378
+ function devcontainerJson(workspaceName) {
1379
+ return `${toJson({
1380
+ name: workspaceName,
1381
+ image: `mcr.microsoft.com/devcontainers/typescript-node:${NODE_VERSION}-bookworm`,
1382
+ features: {
1383
+ "ghcr.io/devcontainers/features/python:1": { version: "3.12" },
1384
+ "ghcr.io/devcontainers/features/go:1": { version: "latest" }
1385
+ },
1386
+ // `npm ci` first: every guard after it runs through the workspace's own
1387
+ // scripts and Nx, which do not exist until the install completes.
1388
+ postCreateCommand: [
1389
+ "npm ci",
1390
+ "npm run python:install",
1391
+ GOLANGCI_LINT_INSTALL_GUARD,
1392
+ FLUTTER_SDK_INSTALL_GUARD
1393
+ ].join(" && "),
1394
+ // The same recommendations the `.code-workspace` file carries, so opening
1395
+ // the folder in a container suggests the identical toolset.
1396
+ customizations: {
1397
+ vscode: { extensions: [...VSCODE_RECOMMENDED_EXTENSIONS] }
1398
+ }
1399
+ })}
1400
+ `;
1401
+ }
1368
1402
  var ROOT_LINT_TARGET = {
1369
1403
  executor: "nx:run-commands",
1370
1404
  cache: true,
@@ -1391,8 +1425,61 @@ function eslintToolchainDependencies(nxVersion) {
1391
1425
  "@mnci/eslint-config": eslintConfigSpec()
1392
1426
  };
1393
1427
  }
1428
+ var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block, by the \`name\` it carries.
1429
+ //
1430
+ // mnci/ignores paths never linted (dist, coverage, .venv, \u2026)
1431
+ // mnci/base JS/TS correctness \u2014 @eslint/js, eslint-plugin-unicorn,
1432
+ // -promise, -n, -unused-imports
1433
+ // typescript-eslint/* typescript-eslint's own recommended blocks
1434
+ // mnci/typescript* TS rules on top of them, no type information needed
1435
+ // mnci/type-aware* the rules that DO read types (no-floating-promises and
1436
+ // friends), scoped to {apps,libs,packages}/*/src
1437
+ // mnci/import-graph import cycles \u2014 eslint-plugin-import-x
1438
+ // mnci/react JSX/TSX \u2014 @eslint-react/eslint-plugin,
1439
+ // eslint-plugin-react-hooks, -react-refresh, -jsx-a11y
1440
+ // mnci/regexp* regex correctness \u2014 eslint-plugin-regexp
1441
+ // mnci/json mnci/jsonc mnci/json5
1442
+ // eslint-plugin-jsonc \u2014 comments are allowed in .jsonc
1443
+ // and tsconfig.json, forbidden in plain .json
1444
+ // mnci/yaml* eslint-plugin-yml \u2014 your CI pipeline files
1445
+ // mnci/toml/base* eslint-plugin-toml, PARSER ONLY: a malformed
1446
+ // pyproject.toml is a syntax error, nothing is styled
1447
+ // mnci/markdown @eslint/markdown
1448
+ // mnci/css @eslint/css
1449
+ // mnci/html @html-eslint/eslint-plugin
1450
+ // mnci/tests *.spec/*.test relaxations \u2014 eslint-plugin-jest
1451
+ // (Vitest's globals too; the two stacks share them)
1452
+ // mnci/nx-dependency-checks @nx/eslint-plugin, on publishable packages' manifests
1453
+ // mnci/prettier-compat eslint-config-prettier \u2014 switches off every rule
1454
+ // Prettier owns. Composed LAST, on purpose.
1455
+ // mnci/stylistic the 3 Standard rules Prettier does not touch
1456
+ //
1457
+ // To list them as ESLint actually resolves them: npx eslint --inspect-config
1458
+ `;
1394
1459
  var ESLINT_CONFIG = `import mnci from '@mnci/eslint-config'
1395
1460
 
1461
+ ${ESLINT_BLOCK_INVENTORY}//
1462
+ // TO OVERRIDE a rule, append a block AFTER the spread \u2014 later blocks win, so one
1463
+ // of your own beats anything above it. Give it a name, so the inspector shows
1464
+ // where the change came from:
1465
+ //
1466
+ // export default [
1467
+ // ...mnci({ workspaceRoot: import.meta.dirname }),
1468
+ // {
1469
+ // name: 'local/legacy-app-allows-any',
1470
+ // files: ['apps/legacy/**/*.ts'],
1471
+ // rules: { '@typescript-eslint/no-explicit-any': 'off' }
1472
+ // }
1473
+ // ]
1474
+ //
1475
+ // Do NOT edit @mnci/eslint-config inside node_modules, and do not fork it: it is
1476
+ // a dependency, so \`npm update\` brings rule fixes in the way it brings any
1477
+ // other. An override here survives that; an edit to the package does not.
1478
+ //
1479
+ // One override cannot work, and it is the one people reach for first:
1480
+ // \`space-before-function-paren\`. Prettier rewrites \`f (a)\` to \`f(a)\` on every
1481
+ // run, so switching that rule on makes \`npm run lint\` and \`npm run format:check\`
1482
+ // impossible to satisfy at the same time. Formatting lives in .prettierrc.mjs.
1396
1483
  export default mnci({ workspaceRoot: import.meta.dirname })
1397
1484
  `;
1398
1485
  var PRETTIER_IGNORE = `node_modules
@@ -1414,6 +1501,12 @@ __pycache__
1414
1501
  .ruff_cache
1415
1502
  .dart_tool
1416
1503
  `;
1504
+ var VSCODE_RECOMMENDED_EXTENSIONS = [
1505
+ "dbaeumer.vscode-eslint",
1506
+ "esbenp.prettier-vscode",
1507
+ "nrwl.angular-console",
1508
+ "firsttris.vscode-jest-runner"
1509
+ ];
1417
1510
  function vscodeWorkspace(workspaceName, existingTasks) {
1418
1511
  return JSON.stringify(
1419
1512
  {
@@ -1444,14 +1537,7 @@ function vscodeWorkspace(workspaceName, existingTasks) {
1444
1537
  "editor.defaultFormatter": "esbenp.prettier-vscode"
1445
1538
  }
1446
1539
  },
1447
- extensions: {
1448
- recommendations: [
1449
- "dbaeumer.vscode-eslint",
1450
- "esbenp.prettier-vscode",
1451
- "nrwl.angular-console",
1452
- "firsttris.vscode-jest-runner"
1453
- ]
1454
- },
1540
+ extensions: { recommendations: [...VSCODE_RECOMMENDED_EXTENSIONS] },
1455
1541
  tasks: {
1456
1542
  version: existingTasks?.version ?? "2.0.0",
1457
1543
  tasks: existingTasks?.tasks ?? []
@@ -1531,6 +1617,7 @@ var GO_TOOL_PATH_PRELUDE = `const fs=require('node:fs'),cp=require('node:child_p
1531
1617
  var GO_TOOL_PATH_AZURE = `node -e "${GO_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
1532
1618
  var GO_TOOL_PATH_GITHUB = `node -e "${GO_TOOL_PATH_PRELUDE}if(!process.env.GITHUB_PATH){console.log('Not running in GitHub Actions - skipping.');process.exit(0)}fs.appendFileSync(process.env.GITHUB_PATH,bin+${String.raw`'\n'`})"`;
1533
1619
  var FLUTTER_SDK_VERSION = "3.44.8";
1620
+ var NODE_VERSION = "24";
1534
1621
  var FLUTTER_SDK_DIRECTORY_EXPRESSION = `require('node:path').join(require('node:os').homedir(),'.mnci-flutter-${FLUTTER_SDK_VERSION}')`;
1535
1622
  var FLUTTER_SDK_INSTALL_GUARD = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}if(cp.spawnSync('flutter',['--version'],{stdio:'ignore',shell:process.platform==='win32'}).status===0){console.log('Flutter SDK already on PATH - skipping.');process.exit(0)}const sdk=${FLUTTER_SDK_DIRECTORY_EXPRESSION};if(fs.existsSync(sdk)){console.log('Flutter SDK already installed at '+sdk+' - skipping.');process.exit(0)}process.exit(cp.spawnSync('git',['clone','--depth','1','--branch','${FLUTTER_SDK_VERSION}','https://github.com/flutter/flutter.git',sdk],{stdio:'inherit'}).status ?? 1)"`;
1536
1623
  var FLUTTER_TOOL_PATH_PRELUDE = `const fs=require('node:fs'),path=require('node:path');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}const sdk=${FLUTTER_SDK_DIRECTORY_EXPRESSION};if(!fs.existsSync(sdk)){console.log('Flutter SDK was not installed by mnci (already on PATH) - skipping.');process.exit(0)}const bin=path.join(sdk,'bin');`;
@@ -1828,7 +1915,7 @@ jobs:
1828
1915
 
1829
1916
  - uses: actions/setup-node@v4
1830
1917
  with:
1831
- node-version: 24
1918
+ node-version: ${NODE_VERSION}
1832
1919
  # Caches ~/.npm keyed on package-lock.json, so \`npm ci\` restores from
1833
1920
  # the local cache instead of re-downloading every tarball on every run.
1834
1921
  # Nothing to invalidate by hand: the action keys on the lockfile, so a
@@ -2012,7 +2099,7 @@ updates:
2012
2099
  schedule:
2013
2100
  interval: weekly
2014
2101
  `;
2015
- var NX_SCAFFOLDING_TO_REMOVE = [".prettierrc", ".vscode"];
2102
+ var NX_SCAFFOLDING_TO_REMOVE = [".prettierrc", ".prettierrc.json", ".vscode"];
2016
2103
  function removeNxScaffolding(workspaceRoot) {
2017
2104
  for (const entry of NX_SCAFFOLDING_TO_REMOVE) {
2018
2105
  (0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, entry), { recursive: true, force: true });
@@ -2082,7 +2169,11 @@ function applyOverlay(workspaceRoot, options) {
2082
2169
  writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
2083
2170
  markExecutable(hookPath);
2084
2171
  writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
2085
- writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierrc.json"), PRETTIER_CONFIG);
2172
+ writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierrc.mjs"), PRETTIER_CONFIG);
2173
+ writeFileEnsured(
2174
+ (0, import_node_path11.join)(workspaceRoot, ".devcontainer/devcontainer.json"),
2175
+ devcontainerJson(options.workspaceName)
2176
+ );
2086
2177
  writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierignore"), PRETTIER_IGNORE);
2087
2178
  removeNxScaffolding(workspaceRoot);
2088
2179
  const codeWorkspacePath = (0, import_node_path11.join)(workspaceRoot, `${options.workspaceName}.code-workspace`);