Skip to content

Commit 15a6856

Browse files
barbados-clemensFrozenPandaz
authored andcommitted
fix(nx-dev): correct interpolate sub command for cli reference (#34585)
also adding e2e for command hierarchy <img width="823" height="362" alt="image" src="https://github.com/user-attachments/assets/3db98945-4221-4bf3-8b92-9d5b25eb2444" /> <img width="778" height="349" alt="image" src="https://github.com/user-attachments/assets/432ebd61-c34e-40f5-b95a-f8d73c682da4" /> ![wm_2026-02-24T14-11-07@2x](https://github.com/user-attachments/assets/a3d635a3-b928-4ff2-a147-28f0873d26c2) (cherry picked from commit 700c98f)
1 parent f01bb98 commit 15a6856

3 files changed

Lines changed: 148 additions & 6 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('CLI sub-command formatting', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/docs/reference/nx-commands');
6+
await expect(
7+
page.getByRole('heading', { name: 'Nx Commands' })
8+
).toBeVisible();
9+
});
10+
11+
test('parent commands render as h2 and sub-commands as h3', async ({
12+
page,
13+
}) => {
14+
const mainContent = page.getByTestId('main-pane');
15+
16+
// "nx show" should be an h2 (top-level parent command)
17+
const showHeading = mainContent.getByRole('heading', {
18+
name: 'nx show',
19+
level: 2,
20+
exact: true,
21+
});
22+
await expect(showHeading).toBeVisible();
23+
24+
// "nx show projects" should be an h3 (sub-command nested under parent)
25+
const showProjectsHeading = mainContent.getByRole('heading', {
26+
name: 'nx show projects',
27+
level: 3,
28+
exact: true,
29+
});
30+
await expect(showProjectsHeading).toBeVisible();
31+
32+
// "nx show project" should also be an h3
33+
const showProjectHeading = mainContent.getByRole('heading', {
34+
name: 'nx show project',
35+
level: 3,
36+
exact: true,
37+
});
38+
await expect(showProjectHeading).toBeVisible();
39+
});
40+
41+
test('sub-command usage blocks show the full command name', async ({
42+
page,
43+
}) => {
44+
const mainContent = page.getByTestId('main-pane');
45+
46+
// Find the "nx show projects" section and verify its usage block
47+
// The usage code block should contain "nx show projects", not "nx projects"
48+
const showProjectsHeading = mainContent.getByRole('heading', {
49+
name: 'nx show projects',
50+
level: 3,
51+
exact: true,
52+
});
53+
await expect(showProjectsHeading).toBeVisible();
54+
55+
// Get the section between "nx show projects" heading and the next heading.
56+
// We look for a code block containing the correct usage pattern.
57+
const codeBlocks = mainContent.locator('pre code');
58+
const allCodeTexts = await codeBlocks.allTextContents();
59+
60+
// There should be a usage block with "nx show projects" (full sub-command name)
61+
expect(allCodeTexts.some((text) => text.includes('nx show projects'))).toBe(
62+
true
63+
);
64+
65+
// There should NOT be a usage block with just "nx projects" (missing parent)
66+
expect(
67+
allCodeTexts.some(
68+
(text) => text.match(/^nx projects/) || text.match(/\nnx projects/)
69+
)
70+
).toBe(false);
71+
});
72+
73+
test('release sub-commands use correct heading and usage format', async ({
74+
page,
75+
}) => {
76+
const mainContent = page.getByTestId('main-pane');
77+
78+
// "nx release" should be h2
79+
const releaseHeading = mainContent.getByRole('heading', {
80+
name: 'nx release',
81+
level: 2,
82+
exact: true,
83+
});
84+
await expect(releaseHeading).toBeVisible();
85+
86+
// "nx release version" should be h3
87+
const releaseVersionHeading = mainContent.getByRole('heading', {
88+
name: 'nx release version',
89+
level: 3,
90+
exact: true,
91+
});
92+
await expect(releaseVersionHeading).toBeVisible();
93+
94+
// Verify usage block includes the full command
95+
const codeBlocks = mainContent.locator('pre code');
96+
const allCodeTexts = await codeBlocks.allTextContents();
97+
98+
expect(
99+
allCodeTexts.some((text) => text.includes('nx release version'))
100+
).toBe(true);
101+
});
102+
103+
test('options and examples are h4 headings excluded from the TOC', async ({
104+
page,
105+
}) => {
106+
const mainContent = page.getByTestId('main-pane');
107+
108+
// Options/Examples should be h4 headings — linkable but below the TOC threshold (h2-h3)
109+
const sharedOptionsHeading = mainContent.getByRole('heading', {
110+
name: 'Shared Options',
111+
level: 4,
112+
});
113+
await expect(sharedOptionsHeading.first()).toBeVisible();
114+
115+
const optionsHeading = mainContent.getByRole('heading', {
116+
name: 'Options',
117+
level: 4,
118+
});
119+
await expect(optionsHeading.first()).toBeVisible();
120+
121+
// They should NOT appear as h2 or h3 (which would put them in the TOC)
122+
await expect(
123+
mainContent.getByRole('heading', { name: 'Options', level: 2 })
124+
).toHaveCount(0);
125+
await expect(
126+
mainContent.getByRole('heading', { name: 'Options', level: 3 })
127+
).toHaveCount(0);
128+
});
129+
});

astro-docs/src/pages/reference/nx-commands.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const { Content, headings } = await render(nxCli);
1515
...nxCli.data,
1616
title: 'Nx Commands',
1717
description: 'Complete reference for Nx CLI',
18+
tableOfContents: { minHeadingLevel: 2, maxHeadingLevel: 3 },
1819
}}
1920
headings={headings || []}
2021
>

astro-docs/src/plugins/utils/nx-cli-generation.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,12 @@ The Nx command line has various subcommands and options to help you manage your
213213
Below is a complete reference for all available commands and their options.
214214
You can run nx --help to view all available options.
215215
216-
## Available Commands
217-
218216
${flattenedCommands
219217
.map(({ fullName, cmd, parentOptions }) => {
220-
let section = `### \`nx ${fullName}\`\n`;
218+
const isSubCommand = parentOptions !== undefined;
219+
const headingLevel = isSubCommand ? '###' : '##';
220+
221+
let section = `${headingLevel} \`nx ${fullName}\`\n`;
221222
222223
section += cmd.description || 'No description available';
223224
@@ -228,9 +229,20 @@ ${flattenedCommands
228229
}
229230
230231
// Build the usage command string
231-
const usageCmd = cmd.command
232-
? cmd.command.replace('$0', fullName)
233-
: fullName;
232+
let usageCmd: string;
233+
if (cmd.command && cmd.command.includes('$0')) {
234+
// Has $0 placeholder - replace with full name
235+
usageCmd = cmd.command.replace('$0', fullName);
236+
} else if (cmd.command && parentOptions !== undefined) {
237+
// Sub-command without $0: use fullName, append positional args from cmd.command
238+
const firstSpaceIdx = cmd.command.indexOf(' ');
239+
usageCmd =
240+
firstSpaceIdx !== -1
241+
? fullName + cmd.command.substring(firstSpaceIdx)
242+
: fullName;
243+
} else {
244+
usageCmd = cmd.command || fullName;
245+
}
234246
235247
section += `\n\n**Usage:**
236248
\`\`\`bash

0 commit comments

Comments
 (0)