@mnci/cli 1.17.2 → 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 +37 -9
- package/dist/cli.js +243 -51
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
@@ -141,7 +141,20 @@ async function promptStack() {
|
|
|
141
141
|
{ name: "Vitest", value: "vitest" }
|
|
142
142
|
]
|
|
143
143
|
});
|
|
144
|
-
|
|
144
|
+
const linter = await (0, import_prompts.select)({
|
|
145
|
+
message: "Linter and formatter",
|
|
146
|
+
choices: [
|
|
147
|
+
{
|
|
148
|
+
name: "ESLint + Prettier \u2014 every file type, the widest rule coverage",
|
|
149
|
+
value: "eslint"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "oxlint + oxfmt for JS/TS, ESLint for the rest \u2014 much faster, same style",
|
|
153
|
+
value: "oxlint"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
});
|
|
157
|
+
return { testRunner, linter };
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
// src/util/fsx.ts
|
|
@@ -1241,7 +1254,7 @@ var import_node_path12 = require("path");
|
|
|
1241
1254
|
// src/overlay.ts
|
|
1242
1255
|
var import_node_fs6 = require("fs");
|
|
1243
1256
|
var import_node_path11 = require("path");
|
|
1244
|
-
var DEFAULT_STACK = { testRunner: "jest" };
|
|
1257
|
+
var DEFAULT_STACK = { testRunner: "jest", linter: "eslint" };
|
|
1245
1258
|
var PRETTIER_VERSION = "^3.8.1";
|
|
1246
1259
|
var TS_COMPILER_DEPENDENCIES = {
|
|
1247
1260
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
@@ -1375,7 +1388,7 @@ function eslintConfigSpec() {
|
|
|
1375
1388
|
return process.env.MNCI_ESLINT_CONFIG_SPEC ?? ESLINT_CONFIG_VERSION;
|
|
1376
1389
|
}
|
|
1377
1390
|
var ESLINT_VERSION = "^10.8.0";
|
|
1378
|
-
function devcontainerJson(workspaceName) {
|
|
1391
|
+
function devcontainerJson(workspaceName, linter) {
|
|
1379
1392
|
return `${toJson({
|
|
1380
1393
|
name: workspaceName,
|
|
1381
1394
|
image: `mcr.microsoft.com/devcontainers/typescript-node:${NODE_VERSION}-bookworm`,
|
|
@@ -1394,7 +1407,7 @@ function devcontainerJson(workspaceName) {
|
|
|
1394
1407
|
// The same recommendations the `.code-workspace` file carries, so opening
|
|
1395
1408
|
// the folder in a container suggests the identical toolset.
|
|
1396
1409
|
customizations: {
|
|
1397
|
-
vscode: { extensions:
|
|
1410
|
+
vscode: { extensions: vscodeExtensions(linter) }
|
|
1398
1411
|
}
|
|
1399
1412
|
})}
|
|
1400
1413
|
`;
|
|
@@ -1422,9 +1435,41 @@ function eslintToolchainDependencies(nxVersion) {
|
|
|
1422
1435
|
eslint: ESLINT_VERSION,
|
|
1423
1436
|
"@nx/eslint": nxVersion,
|
|
1424
1437
|
"@nx/eslint-plugin": nxVersion,
|
|
1425
|
-
"@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
|
+
}
|
|
1426
1458
|
};
|
|
1427
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
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
var OXLINT_CONFIG_VERSION = "^0.1.0";
|
|
1470
|
+
function oxlintConfigSpec() {
|
|
1471
|
+
return process.env.MNCI_OXLINT_CONFIG_SPEC ?? OXLINT_CONFIG_VERSION;
|
|
1472
|
+
}
|
|
1428
1473
|
var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block, by the \`name\` it carries.
|
|
1429
1474
|
//
|
|
1430
1475
|
// mnci/ignores paths never linted (dist, coverage, .venv, \u2026)
|
|
@@ -1482,6 +1527,53 @@ ${ESLINT_BLOCK_INVENTORY}//
|
|
|
1482
1527
|
// impossible to satisfy at the same time. Formatting lives in .prettierrc.mjs.
|
|
1483
1528
|
export default mnci({ workspaceRoot: import.meta.dirname })
|
|
1484
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
|
+
`;
|
|
1485
1577
|
var PRETTIER_IGNORE = `node_modules
|
|
1486
1578
|
dist
|
|
1487
1579
|
dist-dev
|
|
@@ -1501,43 +1593,75 @@ __pycache__
|
|
|
1501
1593
|
.ruff_cache
|
|
1502
1594
|
.dart_tool
|
|
1503
1595
|
`;
|
|
1504
|
-
var
|
|
1505
|
-
"dbaeumer.vscode-eslint",
|
|
1506
|
-
"
|
|
1596
|
+
var LINTER_EXTENSIONS = {
|
|
1597
|
+
eslint: ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"],
|
|
1598
|
+
oxlint: ["oxc.oxc-vscode", "dbaeumer.vscode-eslint"]
|
|
1599
|
+
};
|
|
1600
|
+
var SHARED_VSCODE_EXTENSIONS = [
|
|
1507
1601
|
"nrwl.angular-console",
|
|
1508
1602
|
"firsttris.vscode-jest-runner"
|
|
1509
1603
|
];
|
|
1510
|
-
|
|
1604
|
+
var FORMATTED_LANGUAGES = [
|
|
1605
|
+
"javascript",
|
|
1606
|
+
"javascriptreact",
|
|
1607
|
+
"typescript",
|
|
1608
|
+
"typescriptreact",
|
|
1609
|
+
"json",
|
|
1610
|
+
"jsonc",
|
|
1611
|
+
"yaml",
|
|
1612
|
+
"markdown",
|
|
1613
|
+
"css"
|
|
1614
|
+
];
|
|
1615
|
+
function vscodeExtensions(linter) {
|
|
1616
|
+
return [...LINTER_EXTENSIONS[linter], ...SHARED_VSCODE_EXTENSIONS];
|
|
1617
|
+
}
|
|
1618
|
+
function vscodeSettings(linter) {
|
|
1619
|
+
const formatter = linter === "oxlint" ? "oxc.oxc-vscode" : "esbenp.prettier-vscode";
|
|
1620
|
+
const eslintLanguages = linter === "oxlint" ? ["json", "jsonc", "markdown", "yaml", "html", "css"] : [
|
|
1621
|
+
"javascript",
|
|
1622
|
+
"javascriptreact",
|
|
1623
|
+
"typescript",
|
|
1624
|
+
"typescriptreact",
|
|
1625
|
+
"json",
|
|
1626
|
+
"jsonc",
|
|
1627
|
+
"markdown",
|
|
1628
|
+
"yaml"
|
|
1629
|
+
];
|
|
1630
|
+
return {
|
|
1631
|
+
"eslint.validate": eslintLanguages,
|
|
1632
|
+
"editor.codeActionsOnSave": {
|
|
1633
|
+
"source.fixAll.eslint": "explicit"
|
|
1634
|
+
},
|
|
1635
|
+
"editor.defaultFormatter": formatter,
|
|
1636
|
+
"editor.formatOnSave": true,
|
|
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
|
+
)
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function vscodeWorkspace(workspaceName, linter, existingTasks) {
|
|
1511
1660
|
return JSON.stringify(
|
|
1512
1661
|
{
|
|
1513
1662
|
folders: [{ path: ".", name: workspaceName }],
|
|
1514
|
-
settings:
|
|
1515
|
-
|
|
1516
|
-
"javascript",
|
|
1517
|
-
"javascriptreact",
|
|
1518
|
-
"typescript",
|
|
1519
|
-
"typescriptreact",
|
|
1520
|
-
"json",
|
|
1521
|
-
"jsonc",
|
|
1522
|
-
"markdown",
|
|
1523
|
-
"yaml"
|
|
1524
|
-
],
|
|
1525
|
-
"editor.codeActionsOnSave": {
|
|
1526
|
-
"source.fixAll.eslint": "explicit"
|
|
1527
|
-
},
|
|
1528
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
1529
|
-
"editor.formatOnSave": true,
|
|
1530
|
-
"[json]": {
|
|
1531
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
1532
|
-
},
|
|
1533
|
-
"[jsonc]": {
|
|
1534
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
1535
|
-
},
|
|
1536
|
-
"[yaml]": {
|
|
1537
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
1538
|
-
}
|
|
1539
|
-
},
|
|
1540
|
-
extensions: { recommendations: [...VSCODE_RECOMMENDED_EXTENSIONS] },
|
|
1663
|
+
settings: vscodeSettings(linter),
|
|
1664
|
+
extensions: { recommendations: vscodeExtensions(linter) },
|
|
1541
1665
|
tasks: {
|
|
1542
1666
|
version: existingTasks?.version ?? "2.0.0",
|
|
1543
1667
|
tasks: existingTasks?.tasks ?? []
|
|
@@ -1557,11 +1681,12 @@ var ROOT_SCRIPTS = {
|
|
|
1557
1681
|
"release:preview": "nx release --dry-run",
|
|
1558
1682
|
prepare: "husky"
|
|
1559
1683
|
};
|
|
1560
|
-
function rootScripts() {
|
|
1684
|
+
function rootScripts(linter = "eslint") {
|
|
1685
|
+
const formatter = linter === "oxlint" ? "oxfmt" : "prettier";
|
|
1561
1686
|
return {
|
|
1562
1687
|
...ROOT_SCRIPTS,
|
|
1563
|
-
format: "prettier --write .",
|
|
1564
|
-
"format:check":
|
|
1688
|
+
format: linter === "oxlint" ? "oxfmt ." : "prettier --write .",
|
|
1689
|
+
"format:check": `${formatter} --check .`,
|
|
1565
1690
|
"python:install": `${PYTHON_INSTALL_GUARD} && ${PYTHON_WORKSPACE_INSTALL_GUARD}`
|
|
1566
1691
|
};
|
|
1567
1692
|
}
|
|
@@ -1595,7 +1720,7 @@ function mnciConfig(options) {
|
|
|
1595
1720
|
agent: options.agent,
|
|
1596
1721
|
variableGroup: options.variableGroup,
|
|
1597
1722
|
ci: options.ci,
|
|
1598
|
-
stack: { testRunner: options.stack.testRunner }
|
|
1723
|
+
stack: { testRunner: options.stack.testRunner, linter: options.stack.linter }
|
|
1599
1724
|
};
|
|
1600
1725
|
}
|
|
1601
1726
|
function readMnciConfig(workspaceRoot) {
|
|
@@ -2106,6 +2231,9 @@ function removeNxScaffolding(workspaceRoot) {
|
|
|
2106
2231
|
}
|
|
2107
2232
|
removeProjectEslintConfigs(workspaceRoot);
|
|
2108
2233
|
}
|
|
2234
|
+
function removeIfPresent(path) {
|
|
2235
|
+
(0, import_node_fs6.rmSync)(path, { recursive: true, force: true });
|
|
2236
|
+
}
|
|
2109
2237
|
function removeProjectEslintConfigs(workspaceRoot) {
|
|
2110
2238
|
const matches = (0, import_node_fs6.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
|
|
2111
2239
|
cwd: workspaceRoot
|
|
@@ -2129,7 +2257,7 @@ function applyOverlay(workspaceRoot, options) {
|
|
|
2129
2257
|
const manifest = readJson(manifestPath);
|
|
2130
2258
|
const scripts = {
|
|
2131
2259
|
...manifest.scripts,
|
|
2132
|
-
...rootScripts()
|
|
2260
|
+
...rootScripts(options.stack.linter)
|
|
2133
2261
|
};
|
|
2134
2262
|
const existingDevDeps = manifest.devDependencies;
|
|
2135
2263
|
const devDeps = {
|
|
@@ -2137,6 +2265,9 @@ function applyOverlay(workspaceRoot, options) {
|
|
|
2137
2265
|
...TS_COMPILER_DEPENDENCIES,
|
|
2138
2266
|
// The preset pins `nx` itself; the ESLint plugins must match it exactly.
|
|
2139
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(),
|
|
2140
2271
|
prettier: PRETTIER_VERSION
|
|
2141
2272
|
};
|
|
2142
2273
|
const overrides = {
|
|
@@ -2149,7 +2280,7 @@ function applyOverlay(workspaceRoot, options) {
|
|
|
2149
2280
|
includedScripts: existingNx?.includedScripts ?? [],
|
|
2150
2281
|
targets: {
|
|
2151
2282
|
...existingNx?.targets,
|
|
2152
|
-
lint:
|
|
2283
|
+
lint: rootLintTarget(options.stack.linter)
|
|
2153
2284
|
}
|
|
2154
2285
|
};
|
|
2155
2286
|
writeFileEnsured(
|
|
@@ -2168,17 +2299,29 @@ function applyOverlay(workspaceRoot, options) {
|
|
|
2168
2299
|
const hookPath = (0, import_node_path11.join)(workspaceRoot, ".husky/commit-msg");
|
|
2169
2300
|
writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
|
|
2170
2301
|
markExecutable(hookPath);
|
|
2171
|
-
|
|
2172
|
-
|
|
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
|
+
}
|
|
2173
2313
|
writeFileEnsured(
|
|
2174
2314
|
(0, import_node_path11.join)(workspaceRoot, ".devcontainer/devcontainer.json"),
|
|
2175
|
-
devcontainerJson(options.workspaceName)
|
|
2315
|
+
devcontainerJson(options.workspaceName, options.stack.linter)
|
|
2176
2316
|
);
|
|
2177
2317
|
writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".prettierignore"), PRETTIER_IGNORE);
|
|
2178
2318
|
removeNxScaffolding(workspaceRoot);
|
|
2179
2319
|
const codeWorkspacePath = (0, import_node_path11.join)(workspaceRoot, `${options.workspaceName}.code-workspace`);
|
|
2180
2320
|
const existing = readCodeWorkspace(codeWorkspacePath);
|
|
2181
|
-
writeFileEnsured(
|
|
2321
|
+
writeFileEnsured(
|
|
2322
|
+
codeWorkspacePath,
|
|
2323
|
+
vscodeWorkspace(options.workspaceName, options.stack.linter, existing?.tasks)
|
|
2324
|
+
);
|
|
2182
2325
|
if (options.workspaceName !== "undefined") {
|
|
2183
2326
|
(0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, "undefined.code-workspace"), { force: true });
|
|
2184
2327
|
}
|
|
@@ -2229,6 +2372,44 @@ function checkPrettierConfig(workspaceRoot) {
|
|
|
2229
2372
|
remedy: "delete .prettierrc (or run `mnci upgrade`, which deletes it)"
|
|
2230
2373
|
};
|
|
2231
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
|
+
}
|
|
2232
2413
|
function checkEslintPlugin(nxJson) {
|
|
2233
2414
|
const plugins = nxJson.plugins ?? [];
|
|
2234
2415
|
const registered = plugins.some(
|
|
@@ -2323,6 +2504,8 @@ function collectFindings(workspaceRoot) {
|
|
|
2323
2504
|
return [
|
|
2324
2505
|
...checkEslintConfigs(workspaceRoot),
|
|
2325
2506
|
checkPrettierConfig(workspaceRoot),
|
|
2507
|
+
checkLinterModeIsConsistent(workspaceRoot),
|
|
2508
|
+
checkOxlintToolchainDeclared(workspaceRoot),
|
|
2326
2509
|
checkEslintPlugin(nxJson),
|
|
2327
2510
|
checkResolvedEslint(workspaceRoot),
|
|
2328
2511
|
checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
|
|
@@ -2361,14 +2544,16 @@ function nxCloudProviderValue(ci) {
|
|
|
2361
2544
|
return ci === "azure" ? "azure" : "github";
|
|
2362
2545
|
}
|
|
2363
2546
|
async function resolveStack(options) {
|
|
2364
|
-
if (options.testRunner || options.yes) {
|
|
2547
|
+
if (options.testRunner || options.linter || options.yes) {
|
|
2365
2548
|
return {
|
|
2366
|
-
testRunner: options.testRunner ?? DEFAULT_STACK.testRunner
|
|
2549
|
+
testRunner: options.testRunner ?? DEFAULT_STACK.testRunner,
|
|
2550
|
+
linter: options.linter ?? DEFAULT_STACK.linter
|
|
2367
2551
|
};
|
|
2368
2552
|
}
|
|
2369
2553
|
const prompted = await promptStack();
|
|
2370
2554
|
return {
|
|
2371
|
-
testRunner: prompted.testRunner
|
|
2555
|
+
testRunner: prompted.testRunner,
|
|
2556
|
+
linter: prompted.linter
|
|
2372
2557
|
};
|
|
2373
2558
|
}
|
|
2374
2559
|
var CI_PROVIDERS = /* @__PURE__ */ new Set(["azure", "github", "both"]);
|
|
@@ -2514,6 +2699,7 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
|
2514
2699
|
);
|
|
2515
2700
|
}
|
|
2516
2701
|
const variableGroup = options.variableGroup ?? persisted.variableGroup ?? "Build";
|
|
2702
|
+
const linter = options.linter ?? persisted.stack?.linter ?? "eslint";
|
|
2517
2703
|
const testRunner = options.testRunner ?? persisted.stack?.testRunner;
|
|
2518
2704
|
if (!testRunner) {
|
|
2519
2705
|
throw new Error(
|
|
@@ -2527,7 +2713,7 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
|
2527
2713
|
agent,
|
|
2528
2714
|
variableGroup,
|
|
2529
2715
|
ci,
|
|
2530
|
-
stack: { testRunner }
|
|
2716
|
+
stack: { testRunner, linter }
|
|
2531
2717
|
};
|
|
2532
2718
|
}
|
|
2533
2719
|
function runUpgrade(workspaceRoot, options) {
|
|
@@ -2641,7 +2827,10 @@ function buildProgram(cliVersion) {
|
|
|
2641
2827
|
program.command("new").argument("[name]", "workspace name").description("Create a new monorepo (Nx TS preset + release/pipeline overlay)").option("-y, --yes", "accept defaults for anything not passed as a flag").option("--scope <scope>", "npm scope for publishable packages (e.g. @demo)").option("--registry <kind>", "azure-artifacts | npm").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option(
|
|
2642
2828
|
"--agent <pool>",
|
|
2643
2829
|
"CI build agent: a vmImage (e.g. ubuntu-latest) or self-hosted pool name"
|
|
2644
|
-
).option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both").option("--test-runner <runner>", "unit-test runner: jest | vitest").option(
|
|
2830
|
+
).option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both").option("--test-runner <runner>", "unit-test runner: jest | vitest").option(
|
|
2831
|
+
"--linter <linter>",
|
|
2832
|
+
"linter + formatter: eslint (ESLint + Prettier) | oxlint (oxlint + oxfmt for JS/TS, ESLint for the rest)"
|
|
2833
|
+
).option("--nx-cloud", "connect the workspace to Nx Cloud (remote caching + CI insights)").action(async (name, options) => {
|
|
2645
2834
|
await runNew(name, options);
|
|
2646
2835
|
});
|
|
2647
2836
|
program.command("upgrade").description(
|
|
@@ -2649,6 +2838,9 @@ function buildProgram(cliVersion) {
|
|
|
2649
2838
|
).option("--scope <scope>", "npm scope for publishable packages (overrides the persisted value)").option("--registry <kind>", "azure-artifacts | npm (overrides the persisted value)").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent (overrides the persisted value)").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option("--ci <provider>", "CI provider: azure | github | both (overrides the persisted value)").option(
|
|
2650
2839
|
"--test-runner <runner>",
|
|
2651
2840
|
"unit-test runner: jest | vitest (overrides the persisted value)"
|
|
2841
|
+
).option(
|
|
2842
|
+
"--linter <linter>",
|
|
2843
|
+
"linter + formatter: eslint | oxlint (overrides the persisted value)"
|
|
2652
2844
|
).action((options) => {
|
|
2653
2845
|
runUpgrade(process.cwd(), options);
|
|
2654
2846
|
});
|