Skip to content

Commit 4b6f2be

Browse files
fix(nx-dev): cross site link checks working as expected
1 parent 1a973ba commit 4b6f2be

8 files changed

Lines changed: 222 additions & 35 deletions

File tree

astro-docs/project.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,18 @@
8686
"command": "playwright show-report dist/astro-docs/playwright-report"
8787
},
8888
"validate-links": {
89-
"dependsOn": ["build"],
89+
"dependsOn": [
90+
"build",
91+
{
92+
"projects": ["nx-dev"],
93+
"target": "sitemap"
94+
}
95+
],
9096
"cache": true,
9197
"inputs": [
98+
"{projectRoot}/validate-links.ts",
99+
"{workspaceRoot}/docs/**/*.md",
100+
{ "dependentTasksOutputFiles": "**/sitemap*.xml" },
92101
"{projectRoot}/src/**/*",
93102
"{projectRoot}/astro.config.mjs",
94103
"{projectRoot}/sidebar.mts",

astro-docs/src/content/docs/concepts/executors-and-configurations.mdoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ If defining a new target that needs to run a single shell command, there is a sh
6969
}
7070
```
7171

72-
For more info, see the [run-commands documentation](/docs/reference/nx/executors/run-commands)
72+
For more info, see the [run-commands documentation](/docs/guides/tasks--caching/run-commands-executor)
7373

7474
## Build your own Executor
7575

astro-docs/src/content/docs/guides/Tips-n-Tricks/define-environment-variables.mdoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ If you want to load variables from `env` files other than the ones listed above:
130130

131131
1. Use the [env-cmd](https://www.npmjs.com/package/env-cmd) package: `env-cmd -f .qa.env nx serve`
132132
2. Use [dotenvx](https://github.com/dotenvx/dotenvx): `dotenvx run --env-file=.qa.env -- nx serve`
133-
3. Use the `envFile` option of the [run-commands](/docs/reference/nx/executors/run-commands#envfile) builder and execute your command inside of the builder
133+
3. Use the `envFile` option of the [run-commands](/docs/guides/tasks--caching/run-commands-executor#envfile) builder and execute your command inside of the builder
134134

135135
### Ad-hoc Variables
136136

astro-docs/src/content/docs/guides/Tips-n-Tricks/feature-based-testing.mdoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ When running tests from multiple features in parallel, be mindful of shared reso
152152

153153
- **Using unique test data**: Don't rely on specific database records or application state
154154
- **Managing ports**: Configure each test to use different ports, or let the test framework find free ports automatically
155-
- For Cypress, use the [`--port` flag](/nx-api/cypress/executors/cypress#port) to specify or auto-detect ports
155+
- For Cypress, use the [`--port` flag](/docs/technologies/test-tools/cypress/executors) to specify or auto-detect ports
156156
- For Playwright, the `webServerAddress` can be dynamically assigned
157157
- **Isolating state**: Use test-specific user accounts, temporary data, or cleanup between tests
158158

astro-docs/src/content/docs/technologies/angular/Guides/nx-and-angular.mdoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ Learn more about the [graph features here](/docs/features/explore-graph).
398398

399399
### Extensible and Customizable: Make it fit your own needs
400400

401-
Nx is [built to be extensible](/docs/getting-started/intro#start-small-extend-as-you-grow). Just like the [packages published by the Nx core team](/docs/plugin-registry) you can create your own Nx plugins by [extending Nx](/docs/extending-nx/intro). This can be as simple as using [run-commands](/docs/reference/nx/executors/run-commands) to integrate custom commands into the project configuration or as complex as [creating your own local executor](/docs/extending-nx/local-executors).
401+
Nx is [built to be extensible](/docs/getting-started/intro#start-small-extend-as-you-grow). Just like the [packages published by the Nx core team](/docs/plugin-registry) you can create your own Nx plugins by [extending Nx](/docs/extending-nx/intro). This can be as simple as using [run-commands](/docs/guides/tasks--caching/run-commands-executor) to integrate custom commands into the project configuration or as complex as [creating your own local executor](/docs/extending-nx/local-executors).
402402

403403
And if you ever need to expand beyond Angular or diversify your stack, you can still keep using Nx, which is [battle-tested with many different technologies](/docs/technologies).
404404

astro-docs/src/content/docs/technologies/module-federation/concepts/nx-module-federation-technical-overview.mdoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ filter: 'type:Concepts'
55
---
66

77
Nx's Module Federation support is provided through a mixture of `executors` and the `withModuleFederation()` util that is used in your `webpack.config` or `rspack.config` file. Understanding what is happening under the hood can help when developing applications that use Module Federation as well as debugging any potential issues you run into.
8-
With Rspack, Module Federation support can also be provided through the [`NxModuleFederationPlugin`](nx-api/module-federation/documents/nx-module-federation-plugin) and [`NxModuleFederationDevServerPlugin`](nx-api/module-federation/documents/nx-module-federation-dev-server-plugin) plugins that can be used in the `rspack.config` file when utilizing [Inferred Tasks](/docs/concepts/inferred-tasks).
8+
With Rspack, Module Federation support can also be provided through the [`NxModuleFederationPlugin`](/docs/technologies/module-federation/guides/nx-module-federation-plugin) and [`NxModuleFederationDevServerPlugin`](/docs/technologies/module-federation/guides/nx-module-federation-dev-server-plugin) plugins that can be used in the `rspack.config` file when utilizing [Inferred Tasks](/docs/concepts/inferred-tasks).
99

1010
## What happens when you serve your host?
1111

astro-docs/validate-links.ts

Lines changed: 205 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33
import { workspaceRoot } from '@nx/devkit';
4+
import glob from 'glob';
45

5-
const ignoredLinks = [
6-
// '/reference/devkit',
7-
// these are typically source from the plugin specific examples and can't change until we're pushing to canary
8-
'/nx-api/',
9-
'/reference/core-api',
10-
// TODO: caleb make this nx api reference page
11-
'/docs/reference/nx/executors',
12-
'NxPowerpack-Trial-v1.1.pdf',
13-
'/blog/',
14-
'/contact',
15-
];
6+
// Links to pages hosted outside of both astro-docs and nx-dev sites
7+
const ignoredLinks = ['/contact'];
168

179
// These are more so until we cut over and can modify production file links
1810
const filesToIgnore = [
@@ -23,7 +15,16 @@ const filesToIgnore = [
2315
];
2416

2517
const distDir = path.join(workspaceRoot, 'astro-docs', 'dist');
26-
const sitemapPath = path.join(distDir, 'sitemap-0.xml');
18+
const sitemapIndexPath = path.join(distDir, 'sitemap-index.xml');
19+
const sitemapFallbackPath = path.join(distDir, 'sitemap-0.xml');
20+
const nxDevSitemapPath = path.join(
21+
workspaceRoot,
22+
'dist',
23+
'nx-dev',
24+
'nx-dev',
25+
'public',
26+
'sitemap-0.xml'
27+
);
2728

2829
if (!fs.existsSync(distDir)) {
2930
console.error(
@@ -32,9 +33,9 @@ if (!fs.existsSync(distDir)) {
3233
process.exit(1);
3334
}
3435

35-
if (!fs.existsSync(sitemapPath)) {
36+
if (!fs.existsSync(sitemapIndexPath) && !fs.existsSync(sitemapFallbackPath)) {
3637
console.error(
37-
`Sitemap does not exist at path. Have you ran the build?: ${sitemapPath}`
38+
`No sitemap found at ${sitemapIndexPath} or ${sitemapFallbackPath}. Have you ran the build?`
3839
);
3940
process.exit(1);
4041
}
@@ -112,11 +113,169 @@ function parseSitemap(sitemapContent: string) {
112113
return routes;
113114
}
114115

116+
/**
117+
* Parses sitemap-index.xml to discover all sitemap files,
118+
* then merges routes from all of them.
119+
* Falls back to sitemap-0.xml if sitemap-index.xml doesn't exist.
120+
*/
121+
function loadAllSitemapRoutes(): Set<string> {
122+
const allRoutes = new Set<string>();
123+
124+
// Load astro-docs sitemap routes
125+
if (fs.existsSync(sitemapIndexPath)) {
126+
const indexContent = fs.readFileSync(sitemapIndexPath, 'utf-8');
127+
const locRegex = /<loc>([^<]+)<\/loc>/gi;
128+
const sitemapUrls: string[] = [];
129+
let match;
130+
131+
while ((match = locRegex.exec(indexContent)) !== null) {
132+
sitemapUrls.push(match[1]);
133+
}
134+
135+
for (const sitemapUrl of sitemapUrls) {
136+
const parsedUrl = new URL(sitemapUrl);
137+
const filename = path.basename(parsedUrl.pathname);
138+
const sitemapFilePath = path.join(distDir, filename);
139+
140+
if (!fs.existsSync(sitemapFilePath)) {
141+
console.warn(
142+
`Sitemap file referenced in index not found: ${sitemapFilePath}`
143+
);
144+
continue;
145+
}
146+
147+
const sitemapContent = fs.readFileSync(sitemapFilePath, 'utf-8');
148+
const routes = parseSitemap(sitemapContent);
149+
for (const route of routes) {
150+
allRoutes.add(route);
151+
}
152+
}
153+
} else {
154+
// Fallback to sitemap-0.xml
155+
const sitemapContent = fs.readFileSync(sitemapFallbackPath, 'utf-8');
156+
for (const route of parseSitemap(sitemapContent)) {
157+
allRoutes.add(route);
158+
}
159+
}
160+
161+
// Load nx-dev (Next.js) sitemap routes for cross-site validation
162+
if (fs.existsSync(nxDevSitemapPath)) {
163+
const nxDevSitemapContent = fs.readFileSync(nxDevSitemapPath, 'utf-8');
164+
const nxDevRoutes = parseSitemap(nxDevSitemapContent);
165+
console.log(
166+
`Found ${nxDevRoutes.size} routes in nx-dev sitemap (${nxDevSitemapPath})`
167+
);
168+
for (const route of nxDevRoutes) {
169+
allRoutes.add(route);
170+
}
171+
} else {
172+
console.warn(
173+
`nx-dev sitemap not found at ${nxDevSitemapPath}. Run "nx build nx-dev" to enable cross-site link validation against nx-dev routes.`
174+
);
175+
}
176+
177+
return allRoutes;
178+
}
179+
115180
function toFriendlyName(file: string) {
116181
// TODO: resolve to the actual markdown file if possile?
117182
return path.relative(workspaceRoot, file);
118183
}
119184

185+
/**
186+
* Extracts links pointing to /docs/ paths from markdown file content.
187+
* Handles both markdown links like [text](/docs/...) and card url attributes like url="/docs/..."
188+
*/
189+
function extractDocsLinksFromMarkdown(content: string): Set<string> {
190+
const links = new Set<string>();
191+
192+
// Match markdown links: [text](/docs/...)
193+
const markdownLinkRegex = /\]\(\/docs\/[^)]*\)/g;
194+
let match;
195+
while ((match = markdownLinkRegex.exec(content)) !== null) {
196+
// Extract the path from ](/docs/...)
197+
const linkPath = match[0].slice(2, -1);
198+
// Strip anchors and query params
199+
const clean = linkPath.split('#')[0].split('?')[0];
200+
links.add(clean);
201+
}
202+
203+
// Match card url attributes: url="/docs/..."
204+
const urlAttrRegex = /url="(\/docs\/[^"]*)"/g;
205+
while ((match = urlAttrRegex.exec(content)) !== null) {
206+
const linkPath = match[1];
207+
const clean = linkPath.split('#')[0].split('?')[0];
208+
links.add(clean);
209+
}
210+
211+
// Match reference-style links: [ref]: /docs/...
212+
const refStyleRegex = /^\s*\[[^\]]+\]:\s*(\/docs\/\S*)/gm;
213+
while ((match = refStyleRegex.exec(content)) !== null) {
214+
const linkPath = match[1];
215+
const clean = linkPath.split('#')[0].split('?')[0];
216+
links.add(clean);
217+
}
218+
219+
// Match card url attributes without leading slash: url="docs/..."
220+
const urlAttrNoSlashRegex = /url="(docs\/[^"]*)"/g;
221+
while ((match = urlAttrNoSlashRegex.exec(content)) !== null) {
222+
const linkPath = '/' + match[1]; // normalize by prepending /
223+
const clean = linkPath.split('#')[0].split('?')[0];
224+
links.add(clean);
225+
}
226+
227+
return links;
228+
}
229+
230+
/**
231+
* Scans docs/ markdown files for links to /docs/ paths (astro-docs pages)
232+
* and validates them against the astro sitemap.
233+
* Returns broken links grouped by source file.
234+
*/
235+
function validateCrossSiteLinks(
236+
availableInternalRoutes: Set<string>
237+
): Map<string, string[]> {
238+
const docsDir = path.join(workspaceRoot, 'docs');
239+
const crossSiteBrokenLinks = new Map<string, string[]>();
240+
241+
if (!fs.existsSync(docsDir)) {
242+
console.warn(
243+
`docs/ directory not found at ${docsDir}, skipping cross-site link check`
244+
);
245+
return crossSiteBrokenLinks;
246+
}
247+
248+
const mdFiles = glob.sync('**/*.md', { cwd: docsDir });
249+
console.log(`Found ${mdFiles.length} markdown files in docs/\n`);
250+
251+
let totalLinks = 0;
252+
253+
for (const relPath of mdFiles) {
254+
const fullPath = path.join(docsDir, relPath);
255+
const content = fs.readFileSync(fullPath, 'utf-8');
256+
const docsLinks = extractDocsLinksFromMarkdown(content);
257+
258+
for (const link of docsLinks) {
259+
totalLinks++;
260+
261+
if (!availableInternalRoutes.has(link)) {
262+
const existing = crossSiteBrokenLinks.get(relPath);
263+
if (existing) {
264+
existing.push(link);
265+
} else {
266+
crossSiteBrokenLinks.set(relPath, [link]);
267+
}
268+
}
269+
}
270+
}
271+
272+
console.log(
273+
`Checked ${totalLinks} cross-site links from docs/ to /docs/ astro pages\n`
274+
);
275+
276+
return crossSiteBrokenLinks;
277+
}
278+
120279
function validateLinks() {
121280
const linksToFiles = new Map<string, string[]>();
122281

@@ -151,16 +310,9 @@ function validateLinks() {
151310
}
152311
}
153312

154-
const filteredLinks = Array.from(linksToFiles.keys()).filter((href) => {
155-
const includesIgnoredLink = ignoredLinks.some((il) => href.includes(il));
156-
157-
if (includesIgnoredLink) {
158-
console.warn(`Skipping link since matching manual ignore list: ${href}`);
159-
}
160-
161-
// filter out any manually ingored links
162-
return !includesIgnoredLink;
163-
});
313+
const filteredLinks = Array.from(linksToFiles.keys()).filter(
314+
(href) => !ignoredLinks.some((il) => href.includes(il))
315+
);
164316

165317
const actualLinksUsed = new Set(filteredLinks);
166318
console.log(
@@ -169,20 +321,22 @@ function validateLinks() {
169321

170322
console.log('📍 Parsing sitemap for valid routes...');
171323

172-
const sitemapContent = fs.readFileSync(sitemapPath, 'utf-8');
173-
const availableInternalRoutes = parseSitemap(sitemapContent);
324+
const availableInternalRoutes = loadAllSitemapRoutes();
174325
console.log(
175326
`Found ${availableInternalRoutes.size} unique routes in sitemap\n`
176327
);
177328

178-
console.log('✅ Validating links...\n');
329+
console.log('✅ Validating astro internal links...\n');
179330

180331
// Find links that exist in actualLinksUsed but not in availableInternalRoutes
181332
const brokenLinks: Set<string> = new Set(
182333
[...actualLinksUsed].filter((link) => !availableInternalRoutes.has(link))
183334
);
184335

336+
let hasBrokenLinks = false;
337+
185338
if (brokenLinks.size > 0) {
339+
hasBrokenLinks = true;
186340
console.log(`Found ${brokenLinks.size} broken links:\n`);
187341

188342
const filesWithErrors = new Map<string, string[]>();
@@ -217,7 +371,31 @@ function validateLinks() {
217371
console.log(
218372
`\n🔎 Check the above output to resolve the ${brokenLinks.size} broken links in each respecitve source (.mdoc, .astro, and/or content collection generation`
219373
);
374+
}
375+
376+
// Cross-site validation: check docs/ markdown links to /docs/ astro pages
377+
console.log(
378+
'\n🔗 Validating cross-site links from docs/ markdown to astro pages...\n'
379+
);
380+
const crossSiteBrokenLinks = validateCrossSiteLinks(availableInternalRoutes);
381+
382+
if (crossSiteBrokenLinks.size > 0) {
383+
hasBrokenLinks = true;
384+
let totalCrossSiteBroken = 0;
385+
for (const [file, badLinks] of crossSiteBrokenLinks) {
386+
totalCrossSiteBroken += badLinks.length;
387+
console.log(
388+
`\n❌ docs/${file} has ${badLinks.length} broken cross-site links:`
389+
);
390+
badLinks.forEach((link) => console.log(`\t- ${link}`));
391+
}
392+
393+
console.log(
394+
`\n🔎 Found ${totalCrossSiteBroken} broken cross-site links from docs/ markdown pointing to non-existent astro pages`
395+
);
396+
}
220397

398+
if (hasBrokenLinks) {
221399
process.exit(1);
222400
}
223401

nx-dev/nx-dev/project.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@
3636
},
3737
"inputs": [
3838
"{workspaceRoot}/docs/**/*",
39-
"{workspaceRoot}/dist/nx-dev/nx-dev/public/sitemap*.xml",
40-
"{workspaceRoot}/astro-docs/dist/sitemap*.xml",
39+
{ "dependentTasksOutputFiles": "**/sitemap*.xml" },
4140
"{workspaceRoot}/scripts/tsconfig.scripts.json",
4241
"{workspaceRoot}/scripts/documentation/internal-link-checker.ts"
4342
]
@@ -46,6 +45,7 @@
4645
"dependsOn": ["check-links"]
4746
},
4847
"sitemap": {
48+
"dependsOn": ["build-base"],
4949
"executor": "nx:run-commands",
5050
"inputs": [
5151
"{workspaceRoot}/docs/**/*",

0 commit comments

Comments
 (0)