@mnci/cli 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,6 +119,39 @@ accordingly, `start` marked `isBackground` since it runs a process that
119
119
  doesn't exit on its own. Re-running `add` for the same project name
120
120
  overwrites its own scripts/tasks rather than duplicating them.
121
121
 
122
+ #### Run and Debug: the `launch` section
123
+
124
+ Tasks are reachable **only** through Terminal → Run Task. The **Run and Debug**
125
+ panel reads a separate `launch` section, so a workspace with tasks alone offers
126
+ nothing in the dropdown people actually open. Every generated workspace therefore
127
+ also gets four launch configurations, one per verify target:
128
+
129
+ | configuration | runs |
130
+ | --- | --- |
131
+ | `mnci: build` | `npm run build` |
132
+ | `mnci: test` | `npm run test` |
133
+ | `mnci: lint` | `npm run lint` |
134
+ | `mnci: typecheck` | `npm run typecheck` |
135
+
136
+ Three details are load-bearing rather than incidental:
137
+
138
+ - **`type: node-terminal`, not `node`.** `nx run-many` executes every target in a
139
+ **child** process. A plain `node` launch attaches to the Nx parent alone, so a
140
+ breakpoint inside a spec never binds; `node-terminal` runs the command in VS
141
+ Code's JS Debug Terminal, which instruments children as they spawn. It also avoids
142
+ a second trap — a `node` launch defaults to `internalConsole`, which renders none
143
+ of Nx's progress output, so a build there looks like it has hung.
144
+ - **They drive `npm run <script>`, never a path into `node_modules`.** The obvious
145
+ `program: node_modules/nx/bin/nx.js` is wrong: Nx ships its bin at
146
+ `dist/bin/nx.js`, and that path moves between versions. Driving the root script
147
+ tracks whatever it does, so a change to the scripts reaches these for free.
148
+ - **`cwd` is `${workspaceFolder:<name>}`, scoped by folder name.** A bare
149
+ `${workspaceFolder}` is ambiguous the moment a second folder joins the workspace,
150
+ and VS Code then refuses to resolve it — breaking all four at once.
151
+
152
+ Your own configurations are safe: `mnci upgrade` replaces only the entries named
153
+ `mnci: *` and carries every other one through untouched.
154
+
122
155
  `:start` resolves differently per kind — an existing generator target where
123
156
  one already exists, a small `nx:run-commands` target mnci writes where none
124
157
  did:
@@ -191,6 +224,12 @@ back and carried through unchanged**. Those tasks are per-project state written
191
224
  `mnci add`, not overlay-owned, so regenerating them wholesale would wipe every
192
225
  project's build/qa/start entry on upgrade.
193
226
 
227
+ The `launch` array is handled differently again — **merged, not carried through**.
228
+ mnci owns the four `mnci: *` configurations and replaces them, while any
229
+ configuration you added yourself survives. The asymmetry is deliberate: tasks are
230
+ written by `mnci add` and the overlay has no idea which projects exist, whereas the
231
+ launch entries are entirely overlay-authored and should track an upgrade.
232
+
194
233
  `upgrade` also **deletes** things, which is stronger than the overwriting it
195
234
  has always done — one more reason to run `git diff` first, as the command's own
196
235
  output tells you to:
@@ -508,6 +547,95 @@ repo settings. Either way it's mapped as `env` on the npm steps and read by
508
547
  the root `.npmrc`'s `_password` block — the PAT value never lands in a file.
509
548
  No `npmAuthenticate@0` task (it would overwrite the hand-set password).
510
549
 
550
+ #### Why `_password` and never `_authToken` — read this before "fixing" it
551
+
552
+ Basic auth here is not a legacy choice. Azure Artifacts answers an unauthenticated
553
+ `PUT` to its npm publish endpoint with:
554
+
555
+ ```
556
+ www-authenticate: Bearer authorization_uri=https://login.windows.net/<tenant>,
557
+ Basic realm="...", TFS-Federated
558
+ ```
559
+
560
+ `authorization_uri` pointing at `login.windows.net` means its **Bearer scheme expects
561
+ an Entra ID access token**. A PAT is not one. npm sends `_authToken` *verbatim* as a
562
+ Bearer header (it base64-decodes only `_password`), so a PAT placed there is rejected
563
+ with:
564
+
565
+ ```
566
+ npm publish error:
567
+ Unable to authenticate, your authentication token seems to be invalid.
568
+ ```
569
+
570
+ **A PAT can only authenticate through the Basic scheme**, which is
571
+ `username` + base64 `_password`. That is what this file emits.
572
+
573
+ The trap worth knowing: a PAT *is* accepted as a Bearer token by the Packaging REST
574
+ API (`https://feeds.dev.azure.com/<org>/<proj>/_apis/packaging/feeds` returns 200),
575
+ so testing there looks like proof and is not. **Measure the endpoint the code
576
+ actually calls.** An `_authToken` change was shipped and reverted on exactly this.
577
+
578
+ Both feed path forms are keyed (`…/npm/registry/` and `…/npm/`) because npm matches
579
+ credentials by URL prefix and walks only *up* a path, so an entry on
580
+ `/npm/registry/` is never found for a request to `/npm/`.
581
+
582
+ #### The alternative on Azure: build-identity auth, no PAT at all
583
+
584
+ mnci does not generate this, but it is the better setup for an Azure Artifacts feed
585
+ in the **same organisation** as the pipeline, and it is what a real workspace uses
586
+ today. `npmAuthenticate@0` injects the build service identity's token — which *is*
587
+ Entra-issued, so it satisfies the Bearer scheme the feed advertises — and there is no
588
+ secret to store, encode, rotate or let expire.
589
+
590
+ To switch a generated workspace over by hand:
591
+
592
+ 1. Delete the credential lines from the root `.npmrc`, keeping only the
593
+ `@scope:registry=` routing line. (`mnci upgrade` will rewrite them back — see the
594
+ caveat below.)
595
+ 2. Add this step **before** `npm ci` in `azure-pipelines.yml`:
596
+
597
+ ```yaml
598
+ - task: npmAuthenticate@0
599
+ displayName: Authenticate npm against the feed (build identity, no PAT)
600
+ inputs:
601
+ workingFile: .npmrc
602
+ ```
603
+
604
+ 3. Grant the build identity **Feed Publisher (Contributor)** on the feed (Artifacts →
605
+ Feed Settings → Permissions). Which identity depends on the job authorization
606
+ scope: `<Project> Build Service (<org>)` when scoped to the project (the default),
607
+ or `Project Collection Build Service (<org>)` when not. A **403** rather than a
608
+ 401 is the tell that the wrong identity is in play.
609
+
610
+ Caveats, stated rather than glossed. This is **Azure-only** — GitHub Actions has no
611
+ equivalent task, so a `--ci=github` or `--ci=both` workspace still needs the PAT.
612
+ Local development then needs `npx vsts-npm-auth -config .npmrc` (Windows) or a
613
+ hand-added credential, since developers no longer inherit one from the file.
614
+ And `mnci upgrade` rewrites `.npmrc`, so it will restore the PAT block — `git diff`
615
+ before committing an upgrade, which the upgrade docs already tell you to do.
616
+
617
+ #### Diagnosing an auth failure
618
+
619
+ The error text distinguishes the two schemes, and that is the fastest signal:
620
+
621
+ | message | meaning |
622
+ | --- | --- |
623
+ | `E401 Incorrect or missing password` | Basic auth was used and rejected — check the base64 `_password` value |
624
+ | `Unable to authenticate, your authentication token seems to be invalid` | a token went out as Bearer and was rejected — a PAT cannot go here |
625
+ | `E403` | the credential is valid but lacks publish rights on the feed |
626
+
627
+ Two things are easy to get wrong when chasing this:
628
+
629
+ - **`nx release` publishes from the workspace root, not from each project.**
630
+ `@nx/js`'s `runPublish` uses `cwd: context.root` and passes `packageRoot` only as
631
+ the directory argument, so a missing per-package `.npmrc` is never the cause.
632
+ - **npm resolves `_authToken` before `username`/`_password` for a given registry
633
+ key** (`hasAuth` in `npm-registry-fetch/lib/auth.js`), and *key* precedence is
634
+ decided before *file* precedence. So a stale `_authToken` in a persistent build
635
+ agent's user-level `.npmrc` outranks the project-level `_password` and the
636
+ workspace's own credential never reaches the wire. Check
637
+ `npm config get userconfig` on the agent before blaming the secret.
638
+
511
639
  On Azure, two one-time grants are required (project admin): **Contribute** on
512
640
  the repo for the _Project Collection Build Service_ account (tag push), and
513
641
  **publish** rights on the feed for the PAT's owner. On GitHub, the workflow's
package/dist/cli.js CHANGED
@@ -207,6 +207,23 @@ function markPublic(manifestPath) {
207
207
  const manifest = readJson(manifestPath);
208
208
  writeFileEnsured(manifestPath, toJson({ ...manifest, publishConfig: { access: "public" } }));
209
209
  }
210
+ var WRONG_TYPES_PATH = "./dist/index.esm.d.ts";
211
+ var ACTUAL_TYPES_PATH = "./dist/index.d.ts";
212
+ var DECLARATION_MAP_EXCLUSION = "!**/*.d.ts.map";
213
+ function repairPublishableManifest(manifestPath) {
214
+ const manifest = readJson(manifestPath);
215
+ if (manifest.types === WRONG_TYPES_PATH) {
216
+ manifest.types = ACTUAL_TYPES_PATH;
217
+ }
218
+ const dot = manifest.exports?.["."];
219
+ if (typeof dot === "object" && dot.types === WRONG_TYPES_PATH) {
220
+ dot.types = ACTUAL_TYPES_PATH;
221
+ }
222
+ if (manifest.files && !manifest.files.includes(DECLARATION_MAP_EXCLUSION)) {
223
+ manifest.files.push(DECLARATION_MAP_EXCLUSION);
224
+ }
225
+ writeFileEnsured(manifestPath, toJson(manifest));
226
+ }
210
227
  function ensureAdmZip(workspaceRoot) {
211
228
  if (hasPlugin(workspaceRoot, "adm-zip")) {
212
229
  return;
@@ -714,7 +731,9 @@ async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
714
731
  ],
715
732
  workspaceRoot
716
733
  );
717
- markPublic((0, import_node_path6.join)(workspaceRoot, "packages", name, "package.json"));
734
+ const manifestPath = (0, import_node_path6.join)(workspaceRoot, "packages", name, "package.json");
735
+ markPublic(manifestPath);
736
+ repairPublishableManifest(manifestPath);
718
737
  removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
719
738
  registerProjectCommands(workspaceRoot, name, { build: true });
720
739
  }
@@ -1021,19 +1040,6 @@ function addReactApp(workspaceRoot, name, stack) {
1021
1040
 
1022
1041
  // src/commands/add/reactLib.ts
1023
1042
  var import_node_path9 = require("path");
1024
- var WRONG_TYPES_PATH = "./dist/index.esm.d.ts";
1025
- var ACTUAL_TYPES_PATH = "./dist/index.d.ts";
1026
- function repairTypesPath(manifestPath) {
1027
- const manifest = readJson(manifestPath);
1028
- if (manifest.types === WRONG_TYPES_PATH) {
1029
- manifest.types = ACTUAL_TYPES_PATH;
1030
- }
1031
- const dot = manifest.exports?.["."];
1032
- if (typeof dot === "object" && dot.types === WRONG_TYPES_PATH) {
1033
- dot.types = ACTUAL_TYPES_PATH;
1034
- }
1035
- writeFileEnsured(manifestPath, toJson(manifest));
1036
- }
1037
1043
  var REACT_LIB_BUNDLER = "rollup";
1038
1044
  async function addReactLib(workspaceRoot, name, options, kindProvided, stack) {
1039
1045
  ensurePlugin(workspaceRoot, "@nx/react");
@@ -1055,7 +1061,7 @@ async function addReactLib(workspaceRoot, name, options, kindProvided, stack) {
1055
1061
  );
1056
1062
  const publishableManifest = (0, import_node_path9.join)(workspaceRoot, "packages", name, "package.json");
1057
1063
  markPublic(publishableManifest);
1058
- repairTypesPath(publishableManifest);
1064
+ repairPublishableManifest(publishableManifest);
1059
1065
  removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
1060
1066
  registerProjectCommands(workspaceRoot, name, { build: true });
1061
1067
  }
@@ -1076,7 +1082,7 @@ function addReactInternalLib(workspaceRoot, name, stack) {
1076
1082
  );
1077
1083
  const privateManifest = (0, import_node_path9.join)(workspaceRoot, "libs", name, "package.json");
1078
1084
  markPrivate(privateManifest);
1079
- repairTypesPath(privateManifest);
1085
+ repairPublishableManifest(privateManifest);
1080
1086
  removeGeneratedEslintConfig(workspaceRoot, `libs/${name}`);
1081
1087
  registerProjectCommands(workspaceRoot, name, { build: true });
1082
1088
  }
@@ -1275,6 +1281,7 @@ function npmrcContent(registry, scope) {
1275
1281
  }
1276
1282
  const feedUrl = registryUrl(registry);
1277
1283
  const feedKey = feedUrl.replace(/^https:/, "");
1284
+ const feedShortKey = feedKey.replace("npm/registry/", "npm/");
1278
1285
  return `; Publish + resolution routing for this workspace's own scope.
1279
1286
  ;
1280
1287
  ; '${scope}:registry' sends BOTH resolution and 'npm publish' of ${scope}/* to the
@@ -1290,9 +1297,24 @@ ${scope}:registry=${feedUrl}
1290
1297
  ; instructions give you, which is exactly what _password expects, so it is used
1291
1298
  ; as-is. (twine wants the RAW token; the CI release step decodes it there.)
1292
1299
  ; Azure ignores the username, and npm requires an email it never uses.
1300
+ ;
1301
+ ; NOT _authToken, and that is measured rather than assumed. The feed answers an
1302
+ ; unauthenticated PUT to the publish endpoint with:
1303
+ ; www-authenticate: Bearer authorization_uri=https://login.windows.net/<tenant>,
1304
+ ; Basic realm="...", TFS-Federated
1305
+ ; so its Bearer scheme wants an Entra ID access token, NOT a PAT. npm sends
1306
+ ; _authToken verbatim as a Bearer header, so a PAT there is rejected with
1307
+ ; "Unable to authenticate, your authentication token seems to be invalid". A PAT
1308
+ ; authenticates through the Basic scheme, which is username/_password. Note the
1309
+ ; Packaging REST API DOES accept a PAT as Bearer - do not generalise from it.
1310
+ ;
1311
+ ; Both path forms are keyed because npm walks only UP a URL when matching.
1293
1312
  ${feedKey}:username=AzureArtifacts
1294
1313
  ${feedKey}:_password=\${PAT}
1295
1314
  ${feedKey}:email=npm-requires-this-and-never-uses-it
1315
+ ${feedShortKey}:username=AzureArtifacts
1316
+ ${feedShortKey}:_password=\${PAT}
1317
+ ${feedShortKey}:email=npm-requires-this-and-never-uses-it
1296
1318
  `;
1297
1319
  }
1298
1320
  function releaseConfig(ci) {
@@ -1433,6 +1455,9 @@ var ESLINT_PEER_OVERRIDES = {
1433
1455
  // even a trade, just a version nobody had bumped yet.
1434
1456
  postcss: { nanoid: "^3.3.18" }
1435
1457
  };
1458
+ var SECURITY_OVERRIDES = {
1459
+ "brace-expansion@4.0.0 - 5.0.8": "^5.0.9"
1460
+ };
1436
1461
  function eslintToolchainDependencies(nxVersion) {
1437
1462
  return {
1438
1463
  eslint: ESLINT_VERSION,
@@ -1588,7 +1613,27 @@ function vscodeSettings() {
1588
1613
  )
1589
1614
  };
1590
1615
  }
1591
- function vscodeWorkspace(workspaceName, existingTasks) {
1616
+ var LAUNCH_CONFIG_PREFIX = "mnci: ";
1617
+ var LAUNCH_CONFIGURATIONS = ["build", "test", "lint", "typecheck"];
1618
+ function launchConfigurations(workspaceName) {
1619
+ return LAUNCH_CONFIGURATIONS.map((script, index) => ({
1620
+ type: "node-terminal",
1621
+ request: "launch",
1622
+ name: `${LAUNCH_CONFIG_PREFIX}${script}`,
1623
+ command: `npm run ${script}`,
1624
+ // Scoped by folder name rather than a bare ${workspaceFolder}: that variable is
1625
+ // ambiguous once a second folder is added to the workspace, and VS Code then
1626
+ // refuses to resolve it.
1627
+ cwd: `\${workspaceFolder:${workspaceName}}`,
1628
+ // One shared group keeps the four together in the dropdown; a per-entry group
1629
+ // would make four groups of one. `order` holds them in verify order.
1630
+ presentation: { group: "mnci", order: index + 1 }
1631
+ }));
1632
+ }
1633
+ function vscodeWorkspace(workspaceName, existingTasks, existingLaunch) {
1634
+ const userConfigurations = (existingLaunch?.configurations ?? []).filter(
1635
+ (configuration) => !String(configuration.name ?? "").startsWith(LAUNCH_CONFIG_PREFIX)
1636
+ );
1592
1637
  return JSON.stringify(
1593
1638
  {
1594
1639
  folders: [{ path: ".", name: workspaceName }],
@@ -1597,6 +1642,10 @@ function vscodeWorkspace(workspaceName, existingTasks) {
1597
1642
  tasks: {
1598
1643
  version: existingTasks?.version ?? "2.0.0",
1599
1644
  tasks: existingTasks?.tasks ?? []
1645
+ },
1646
+ launch: {
1647
+ version: existingLaunch?.version ?? "0.2.0",
1648
+ configurations: [...launchConfigurations(workspaceName), ...userConfigurations]
1600
1649
  }
1601
1650
  },
1602
1651
  null,
@@ -1720,15 +1769,40 @@ trigger:
1720
1769
  # reason: two concurrent 'nx release' runs would race to create the same tag.
1721
1770
  batch: true
1722
1771
  branches:
1723
- include: [main]
1724
-
1725
- # Note: cancelling a superseded PR validation run is NOT a YAML setting on Azure
1726
- # \u2014 it lives in the branch policy ("Build validation -> automatically cancel").
1727
- # Nothing here can express it, so it is left to whoever configures the policy
1728
- # rather than faked with a batch setting that would only delay PR runs.
1729
- pr:
1730
- branches:
1731
- include: [main]
1772
+ include:
1773
+ - main
1774
+ # EVERY other branch too, and that breadth is a correction rather than
1775
+ # generosity. On Azure Repos Git a YAML 'pr:' block does nothing at all
1776
+ # (see the note below), so a CI trigger is the only pre-merge
1777
+ # verification this file can switch on by itself. Listing main alone \u2014
1778
+ # which is what this pipeline used to do, next to a 'pr:' block that
1779
+ # looked like it covered the rest \u2014 means a workspace verifies NOTHING
1780
+ # until a change has already reached the branch it was supposed to
1781
+ # protect.
1782
+ #
1783
+ # Release stays gated on main (the 'condition:' on every release step
1784
+ # below), so a topic-branch run installs, audits, syncs, formats,
1785
+ # verifies and packs, and publishes nothing.
1786
+ - '*'
1787
+
1788
+ # PR validation is deliberately NOT configured here, because on Azure Repos Git
1789
+ # it CANNOT be: "For an Azure Repos Git repo, you cannot configure a PR trigger
1790
+ # in the YAML file. You need to use branch policies." A 'pr:' block here is not
1791
+ # an error \u2014 it is silently ignored, which is worse, because the file then
1792
+ # documents a gate that does not exist. (The same block IS honoured for GitHub
1793
+ # and Bitbucket repos, which is why .github/workflows/ci.yml keeps its
1794
+ # 'pull_request:' trigger.)
1795
+ #
1796
+ # To get PR validation, configure it once in the UI: Project Settings ->
1797
+ # Repositories -> <repo> -> Policies -> <branch> -> Build Validation, pointing
1798
+ # at this pipeline. That is also the only thing that sets
1799
+ # System.PullRequest.TargetBranch, which the verify step below reads to scope
1800
+ # itself to affected projects \u2014 without a policy that variable is never set and
1801
+ # the step verifies every project instead. Correct either way, just slower.
1802
+ #
1803
+ # Cancelling a superseded PR run lives in that same policy ("automatically
1804
+ # cancel"), and likewise has no YAML expression.
1805
+ pr: none
1732
1806
 
1733
1807
  pool:
1734
1808
  ${poolBlock(agent)}
@@ -2186,7 +2260,8 @@ function applyOverlay(workspaceRoot, options) {
2186
2260
  });
2187
2261
  const overrides = {
2188
2262
  ...manifest.overrides,
2189
- ...ESLINT_PEER_OVERRIDES
2263
+ ...ESLINT_PEER_OVERRIDES,
2264
+ ...SECURITY_OVERRIDES
2190
2265
  };
2191
2266
  const existingNx = manifest.nx;
2192
2267
  const nx = {
@@ -2226,7 +2301,7 @@ function applyOverlay(workspaceRoot, options) {
2226
2301
  const existing = readCodeWorkspace(codeWorkspacePath);
2227
2302
  writeFileEnsured(
2228
2303
  codeWorkspacePath,
2229
- vscodeWorkspace(options.workspaceName, existing?.tasks)
2304
+ vscodeWorkspace(options.workspaceName, existing?.tasks, existing?.launch)
2230
2305
  );
2231
2306
  if (options.workspaceName !== "undefined") {
2232
2307
  (0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, "undefined.code-workspace"), { force: true });
@@ -2249,11 +2324,14 @@ function applyOverlay(workspaceRoot, options) {
2249
2324
 
2250
2325
  // src/commands/doctor.ts
2251
2326
  var SUPPORTED_ESLINT_MAJOR = ESLINT_VERSION.replace(/^\D*/, "").split(".", 1)[0];
2327
+ function toPosix(path) {
2328
+ return path.replaceAll("\\", "/");
2329
+ }
2252
2330
  function checkEslintConfigs(workspaceRoot) {
2253
2331
  const rootConfigs = (0, import_node_fs7.globSync)("eslint.config.{js,mjs,cjs,ts,mts,cts}", { cwd: workspaceRoot });
2254
2332
  const projectConfigs = (0, import_node_fs7.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
2255
2333
  cwd: workspaceRoot
2256
- });
2334
+ }).map((config) => toPosix(config));
2257
2335
  return [
2258
2336
  {
2259
2337
  check: "root ESLint config",
@@ -2348,7 +2426,7 @@ function checkVersionActions(workspaceRoot) {
2348
2426
  const candidates = [
2349
2427
  ...(0, import_node_fs7.globSync)("packages/*/pubspec.yaml", { cwd: workspaceRoot }),
2350
2428
  ...(0, import_node_fs7.globSync)("python-packages/*/pyproject.toml", { cwd: workspaceRoot })
2351
- ];
2429
+ ].map((manifest) => toPosix(manifest));
2352
2430
  return candidates.flatMap((manifest) => {
2353
2431
  const projectRoot = manifest.slice(0, manifest.lastIndexOf("/"));
2354
2432
  const projectJsonPath = (0, import_node_path12.join)(workspaceRoot, projectRoot, "project.json");
@@ -2368,6 +2446,63 @@ function checkVersionActions(workspaceRoot) {
2368
2446
  ];
2369
2447
  });
2370
2448
  }
2449
+ var TARGET_FILE_OPTIONS = ["main", "tsConfig", "packageJson"];
2450
+ function declaredTargets(workspaceRoot, projectRoot) {
2451
+ const sources = [];
2452
+ for (const [file, extract] of [
2453
+ ["project.json", (json) => json["targets"]],
2454
+ [
2455
+ "package.json",
2456
+ (json) => json["nx"]?.["targets"]
2457
+ ]
2458
+ ]) {
2459
+ try {
2460
+ const targets = extract(
2461
+ readJson((0, import_node_path12.join)(workspaceRoot, projectRoot, file))
2462
+ );
2463
+ if (targets && typeof targets === "object") {
2464
+ sources.push({
2465
+ source: `${projectRoot}/${file}`,
2466
+ targets
2467
+ });
2468
+ }
2469
+ } catch {
2470
+ }
2471
+ }
2472
+ return sources;
2473
+ }
2474
+ function checkTargetFilesExist(workspaceRoot) {
2475
+ const projectRoots = new Set(
2476
+ [
2477
+ ...(0, import_node_fs7.globSync)("{apps,libs,packages}/*/project.json", { cwd: workspaceRoot }),
2478
+ ...(0, import_node_fs7.globSync)("{apps,libs,packages}/*/package.json", { cwd: workspaceRoot })
2479
+ ].map((manifest) => toPosix(manifest).split("/").slice(0, 2).join("/"))
2480
+ );
2481
+ return [...projectRoots].flatMap(
2482
+ (projectRoot) => declaredTargets(workspaceRoot, projectRoot).flatMap(
2483
+ ({ source, targets }) => Object.entries(targets).flatMap(
2484
+ ([targetName, target]) => checkOneTargetsFiles(workspaceRoot, source, targetName, target)
2485
+ )
2486
+ )
2487
+ );
2488
+ }
2489
+ function checkOneTargetsFiles(workspaceRoot, source, targetName, target) {
2490
+ return TARGET_FILE_OPTIONS.flatMap((option) => {
2491
+ const value = target?.options?.[option];
2492
+ const checkable = typeof value === "string" && !value.includes("{");
2493
+ if (!checkable || fileExists((0, import_node_path12.join)(workspaceRoot, value))) {
2494
+ return [];
2495
+ }
2496
+ return [
2497
+ {
2498
+ check: `${source} \u2192 ${targetName}.${option} points at a real file`,
2499
+ ok: false,
2500
+ detail: `${value} does not exist \u2014 the target fails as if the compiler were misconfigured`,
2501
+ remedy: `create ${value}, or correct ${option} in ${source}`
2502
+ }
2503
+ ];
2504
+ });
2505
+ }
2371
2506
  function checkSync(workspaceRoot) {
2372
2507
  return {
2373
2508
  check: "TypeScript project references synced",
@@ -2391,6 +2526,7 @@ function collectFindings(workspaceRoot) {
2391
2526
  checkResolvedEslint(workspaceRoot),
2392
2527
  checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
2393
2528
  ...checkVersionActions(workspaceRoot),
2529
+ ...checkTargetFilesExist(workspaceRoot),
2394
2530
  checkSync(workspaceRoot)
2395
2531
  ].filter((finding) => finding !== void 0);
2396
2532
  }