Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43a34d0b95 | |||
| 8add883499 | |||
| 79319472b6 | |||
| 0848679fd9 | |||
| 2a93513d9a | |||
| d0f7a77f8f | |||
| 5ebd5acca8 | |||
| 06629a0df1 | |||
| 9c6c9a1a0c | |||
| 4f504372d2 | |||
| df9dc4290c | |||
| 50c0038358 | |||
| 9066622acb | |||
| c687cc6600 | |||
| 795463d93f | |||
| fea8528dc1 | |||
| a43b86316e | |||
| f2142a9d64 | |||
| 9073f40e1d | |||
| 1c1388dbfd | |||
| 20da4f23eb | |||
| 754cb1271c | |||
| 02b1c49497 | |||
| cd97ff7a13 | |||
| 075b6542b8 | |||
| afc27c8ff6 | |||
| 089db8762a | |||
| cba9c88d9b | |||
| 52088dd13a | |||
| cf42933bea | |||
| 8ad00a791e | |||
| dfa878ec14 | |||
| 5f8458469e | |||
| 55fe0419e9 | |||
| 86fbeac4ef | |||
| a40d06ed23 | |||
| 758cc7b7c8 | |||
| b6cb5ccaca | |||
| 1dad304994 | |||
| eb239f3a91 | |||
| c1c1a64daa | |||
| ca38b53eb4 | |||
| 30abc3fcc6 | |||
| f872ebdad7 | |||
| 0e37963f67 | |||
| a475a43ac0 | |||
| dc2e9d7087 | |||
| 71039e8ffd | |||
| 126710a3ed | |||
| acb1b8e55c | |||
| 9b8a9b31f0 |
@@ -18,6 +18,7 @@ jobs:
|
||||
main-linux:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_BATCH_MODE: 'true'
|
||||
NX_E2E_CI_CACHE_KEY: e2e-github-linux
|
||||
NX_DAEMON: 'true'
|
||||
NX_PERF_LOGGING: 'false'
|
||||
@@ -47,7 +48,7 @@ jobs:
|
||||
main-branch-name: 'master'
|
||||
|
||||
- name: Start CI Run
|
||||
run: npx nx-cloud@next start-ci-run --auto-apply-fixes="*format:check*,*sync:check*,*conformance:check*,*format-native*,*lint-native*,*lint*,*astro-docs:validate-links*" --distribute-on="./.nx/workflows/dynamic-changesets.yaml" --stop-agents-after="e2e"
|
||||
run: npx nx-cloud@next start-ci-run --distribute-on="./.nx/workflows/dynamic-changesets.yaml" --stop-agents-after="e2e"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
distribute-on:
|
||||
default: auto linux-large, 3 linux-extra-large
|
||||
extra-small-changeset: 6 linux-large, 3 linux-extra-large
|
||||
small-changeset: 6 linux-large, 4 linux-extra-large
|
||||
medium-changeset: 6 linux-large, 5 linux-extra-large
|
||||
large-changeset: 6 linux-large, 6 linux-extra-large
|
||||
extra-large-changeset: 8 linux-large, 8 linux-extra-large
|
||||
assignment-rules:
|
||||
- projects:
|
||||
- e2e-gradle
|
||||
- e2e-next
|
||||
- e2e-plugin
|
||||
targets:
|
||||
- e2e-ci**
|
||||
run-on:
|
||||
- agent: linux-extra-large
|
||||
parallelism: 2
|
||||
- projects:
|
||||
- e2e-angular
|
||||
- e2e-node
|
||||
- e2e-react
|
||||
targets:
|
||||
- e2e-ci**
|
||||
run-on:
|
||||
@@ -25,15 +40,14 @@ assignment-rules:
|
||||
|
||||
- projects:
|
||||
- e2e-release
|
||||
- e2e-angular
|
||||
- e2e-react
|
||||
- e2e-next
|
||||
- e2e-nuxt
|
||||
- e2e-web
|
||||
- e2e-eslint
|
||||
- e2e-remix
|
||||
- e2e-cypress
|
||||
- e2e-docker
|
||||
- e2e-js
|
||||
- e2e-nx
|
||||
- e2e-nx-init
|
||||
- e2e-dotnet
|
||||
- e2e-workspace-create
|
||||
@@ -44,7 +58,7 @@ assignment-rules:
|
||||
- agent: linux-large
|
||||
parallelism: 1
|
||||
- agent: linux-extra-large
|
||||
parallelism: 1
|
||||
parallelism: 2
|
||||
|
||||
# All other e2e tests can run in parallel
|
||||
- targets:
|
||||
|
||||
@@ -86,6 +86,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi-escape-sequences"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6afb74bb006f83a46d689f70e46ce3a9830db762134b3b585986fa9eab21f75d"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.6"
|
||||
@@ -295,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.6",
|
||||
"regex-automata 0.4.13",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -790,6 +799,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||
|
||||
[[package]]
|
||||
name = "east-asian-width"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef8c47de0fcde8bd09c98fc22e4ea10ed14b9c05bf02c309e1e743aee7d0d9f6"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.10.0"
|
||||
@@ -1382,8 +1397,8 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.6",
|
||||
"regex-syntax 0.8.2",
|
||||
"regex-automata 0.4.13",
|
||||
"regex-syntax 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1587,7 +1602,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.57.0",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1625,7 +1640,7 @@ dependencies = [
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.6",
|
||||
"regex-automata 0.4.13",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
@@ -1892,7 +1907,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
@@ -1916,7 +1931,7 @@ dependencies = [
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tower",
|
||||
"url",
|
||||
@@ -1944,7 +1959,7 @@ dependencies = [
|
||||
"http",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2072,9 +2087,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.1"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
@@ -2456,6 +2471,7 @@ dependencies = [
|
||||
"watchexec-filterer-ignore",
|
||||
"watchexec-signals",
|
||||
"winapi",
|
||||
"wrap-ansi",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
@@ -3040,14 +3056,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.3"
|
||||
version = "1.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.6",
|
||||
"regex-syntax 0.8.2",
|
||||
"regex-automata 0.4.13",
|
||||
"regex-syntax 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3061,13 +3077,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.6"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.2",
|
||||
"regex-syntax 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3078,9 +3094,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
@@ -3616,6 +3632,18 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "string-width"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f00d94d660cfa7bf3669352ebe9e06cc0351e0c9e696a31d051a54fa4bbb2b5"
|
||||
dependencies = [
|
||||
"east-asian-width",
|
||||
"regex",
|
||||
"strip-ansi-escapes",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.7"
|
||||
@@ -3654,6 +3682,15 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strip-ansi-escapes"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
|
||||
dependencies = [
|
||||
"vte 0.14.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -3962,11 +3999,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.12",
|
||||
"thiserror-impl 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3982,9 +4019,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4459,7 +4496,7 @@ dependencies = [
|
||||
"ratatui",
|
||||
"tui-term 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.2.0",
|
||||
"vte",
|
||||
"vte 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4473,6 +4510,15 @@ dependencies = [
|
||||
"vte_generate_state_changes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte_generate_state_changes"
|
||||
version = "0.1.2"
|
||||
@@ -4827,7 +4873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.2",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-numerics",
|
||||
@@ -4839,19 +4885,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement 0.57.0",
|
||||
"windows-interface 0.57.0",
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4860,10 +4894,10 @@ version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result 0.3.4",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
@@ -4873,22 +4907,11 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
@@ -4900,17 +4923,6 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
@@ -4934,19 +4946,10 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -5253,6 +5256,18 @@ dependencies = [
|
||||
"wayland-protocols-wlr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrap-ansi"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "848bc403c11fc8f4d6597f2bad2ce43111fbf6757c7dd7d62590199e8dade5fd"
|
||||
dependencies = [
|
||||
"ansi-escape-sequences",
|
||||
"regex",
|
||||
"string-width",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<p style="text-align: center;">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./images/nx-dark.svg">
|
||||
<img alt="Nx - Smart Repos · Fast Builds" src="./images/nx-light.svg" width="100%">
|
||||
<img alt="Nx - Smart Monorepos · Fast Builds" src="./images/nx-light.svg" width="100%">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<hr>
|
||||
|
||||
# Smart Repos · Fast Builds
|
||||
# Smart Monorepos · Fast Builds
|
||||
|
||||
Get to green PRs in half the time. Nx optimizes your builds, scales your CI, and fixes failed PRs. Built for developers and AI agents.
|
||||
|
||||
@@ -58,7 +58,7 @@ Learn more in the [Nx CI docs »](https://nx.dev/ci/getting-started/intro?u
|
||||
- [Our Twitter/X](https://x.com/nxdevtools)
|
||||
|
||||
<p style="text-align: center;"><a href="https://www.youtube.com/@nxdevtools/videos" target="_blank" rel="noreferrer"><img src="./images/nx-courses-and-videos.svg"
|
||||
width="100%" alt="Nx - Smart Repos · Fast Builds"></a></p>
|
||||
width="100%" alt="Nx - Smart Monorepos · Fast Builds"></a></p>
|
||||
|
||||
## Want to help?
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import react from '@astrojs/react';
|
||||
import markdoc from '@astrojs/markdoc';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sidebar } from './sidebar.mts';
|
||||
import rehypeTableOptionLinks from './src/plugins/utils/rehype-table-option-links.ts';
|
||||
|
||||
const BASE = '/docs';
|
||||
|
||||
@@ -33,6 +34,9 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
rehypePlugins: [rehypeTableOptionLinks],
|
||||
},
|
||||
trailingSlash: 'never',
|
||||
// This adapter doesn't support local previews, so only load it on Netlify.
|
||||
adapter: process.env['NETLIFY'] ? netlify() : undefined,
|
||||
@@ -94,10 +98,7 @@ export default defineConfig({
|
||||
'./src/plugins/canonical.middleware.ts',
|
||||
],
|
||||
markdown: {
|
||||
// this breaks the renderMarkdown function in the plugin loader due to starlight path normalization
|
||||
// as to _why_ it has to normalize a path?
|
||||
// idk just working around the issue for now but we'll want to have linked headers so will need to fix
|
||||
headingLinks: false,
|
||||
headingLinks: true,
|
||||
},
|
||||
social: [
|
||||
{ icon: 'github', label: 'GitHub', href: 'https://github.com/nrwl/nx' },
|
||||
|
||||
@@ -11,7 +11,7 @@ test('links in descriptions of properties should correctly link to the same page
|
||||
|
||||
await page
|
||||
.getByTestId('main-pane')
|
||||
.getByRole('link', { name: 'nxCloudAccessToken' })
|
||||
.getByRole('link', { name: 'nxCloudAccessToken', exact: true })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -4,9 +4,15 @@ import {
|
||||
Markdoc,
|
||||
} from '@astrojs/markdoc/config';
|
||||
import starlightMarkdoc from '@astrojs/starlight-markdoc';
|
||||
import { transformOptionsTable } from './src/utils/markdoc-table-option-links';
|
||||
|
||||
export default defineMarkdocConfig({
|
||||
extends: [starlightMarkdoc()],
|
||||
nodes: {
|
||||
table: {
|
||||
transform: transformOptionsTable,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
call_to_action: {
|
||||
render: component('./src/components/markdoc/CallToAction.astro'),
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
NX_GRADLE_DISABLE = "true"
|
||||
NX_MAVEN_DISABLE = "true"
|
||||
|
||||
# Edge functions are auto-discovered from netlify/edge-functions/
|
||||
# Path configuration is in each function's inline `config` export
|
||||
|
||||
# Permanent redirects (301 by default)
|
||||
|
||||
# Storybook docs consolidation
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { Context } from 'https://edge.netlify.com';
|
||||
|
||||
/**
|
||||
* Content negotiation for LLM-friendly docs access.
|
||||
* See: https://llmstxt.org/
|
||||
*/
|
||||
export default async function handler(
|
||||
request: Request,
|
||||
context: Context
|
||||
): Promise<Response | URL> {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
const acceptHeader = request.headers.get('accept') || '';
|
||||
|
||||
// Serve markdown for LLM tools that explicitly request it
|
||||
// Or if there are no accept headers passed (e.g. Cursor)
|
||||
if (!acceptHeader || acceptHeader.includes('text/markdown')) {
|
||||
const mdPath = pathname.replace(/\/?$/, '.md');
|
||||
return new URL(mdPath, request.url);
|
||||
}
|
||||
|
||||
const response = await context.next();
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (!contentType.includes('text/html')) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const mdPath = pathname.replace(/\/?$/, '.md');
|
||||
|
||||
const linkHeader = [
|
||||
`<${mdPath}>; rel="alternate"; type="text/markdown"`,
|
||||
`</docs/llms.txt>; rel="alternate"; type="text/markdown"; title="LLM Index"`,
|
||||
`</docs/llms-full.txt>; rel="alternate"; type="text/markdown"; title="Full Documentation"`,
|
||||
].join(', ');
|
||||
|
||||
// Netlify responses are immutable
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set('Link', linkHeader);
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
export const config = {
|
||||
path: ['/docs/*'],
|
||||
excludedPath: [
|
||||
'/docs/*.md',
|
||||
'/docs/*.js',
|
||||
'/docs/*.txt',
|
||||
'/docs/images/*',
|
||||
// _astro and other asset paths
|
||||
'/docs/_*',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { Context } from 'https://edge.netlify.com';
|
||||
|
||||
// Configuration - set these in Netlify environment variables
|
||||
const GA_MEASUREMENT_ID =
|
||||
Netlify.env.get('GA_MEASUREMENT_ID') || 'G-XXXXXXXXXX';
|
||||
const GA_API_SECRET = Netlify.env.get('GA_API_SECRET') || '';
|
||||
|
||||
function getClientId(request: Request): string {
|
||||
// Try to extract existing GA client ID from cookie
|
||||
const cookies = request.headers.get('cookie') || '';
|
||||
const gaMatch = cookies.match(/_ga=GA\d+\.\d+\.(\d+\.\d+)/);
|
||||
if (gaMatch) {
|
||||
return gaMatch[1];
|
||||
}
|
||||
|
||||
// Generate a new client ID for this request
|
||||
// For non-browser clients (AI tools), this creates a session-based ID
|
||||
const timestamp = Date.now();
|
||||
const random = Math.floor(Math.random() * 1000000000);
|
||||
return `${random}.${timestamp}`;
|
||||
}
|
||||
|
||||
async function sendToGA4(
|
||||
request: Request,
|
||||
context: Context,
|
||||
pathname: string
|
||||
): Promise<void> {
|
||||
if (!GA_API_SECRET) {
|
||||
console.warn('GA_API_SECRET not configured, skipping analytics');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = getClientId(request);
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown';
|
||||
|
||||
// Detect AI tools from user agent
|
||||
const isAITool =
|
||||
/bot|crawler|spider|gpt|claude|anthropic|openai|perplexity|cohere/i.test(
|
||||
userAgent
|
||||
);
|
||||
|
||||
const payload = {
|
||||
client_id: clientId,
|
||||
events: [
|
||||
{
|
||||
name: 'server_page_view',
|
||||
params: {
|
||||
page_location: request.url,
|
||||
page_title: pathname,
|
||||
page_path: pathname,
|
||||
// Custom parameters for filtering
|
||||
content_type: pathname.endsWith('.txt')
|
||||
? 'text/plain'
|
||||
: 'text/markdown',
|
||||
file_extension: pathname.substring(pathname.lastIndexOf('.')),
|
||||
user_agent: userAgent,
|
||||
is_ai_tool: isAITool ? 'true' : 'false',
|
||||
country: context.geo?.country?.code || 'unknown',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
console.log(`Tracked asset path: ${pathname}`);
|
||||
|
||||
const endpoint = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
||||
|
||||
try {
|
||||
await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch (error) {
|
||||
// Log but don't fail the request
|
||||
console.error('Failed to send to GA4:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
request: Request,
|
||||
context: Context
|
||||
): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Send analytics in background (non-blocking)
|
||||
context.waitUntil(sendToGA4(request, context, pathname));
|
||||
|
||||
// Continue to serve the actual file
|
||||
const response = await context.next();
|
||||
|
||||
// Netlify Edge Function responses are immutable, so create a new Response
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set('x-nx-edge-function', 'track-asset-requests');
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
export const config = {
|
||||
path: ['/**/*.txt', '/**/*.md'],
|
||||
// Something is adding .png.md to get image paths, exclude those.
|
||||
excludedPath: ['/docs/og/*'],
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { Context } from 'https://edge.netlify.com';
|
||||
|
||||
const GA_MEASUREMENT_ID =
|
||||
Netlify.env.get('GA_MEASUREMENT_ID') || 'G-XXXXXXXXXX';
|
||||
const GA_API_SECRET = Netlify.env.get('GA_API_SECRET') || '';
|
||||
|
||||
function shouldTrack(request: Request): boolean {
|
||||
const accept = request.headers.get('accept') || '';
|
||||
|
||||
// Track if:
|
||||
// - No Accept header (some LLM tools)
|
||||
// - Accept contains text/html (browsers)
|
||||
// - Accept contains */* (curl, browser default)
|
||||
if (!accept || accept.includes('text/html') || accept.includes('*/*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip image/css/js/font requests
|
||||
return false;
|
||||
}
|
||||
|
||||
function getClientId(request: Request): string {
|
||||
const cookies = request.headers.get('cookie') || '';
|
||||
const gaMatch = cookies.match(/_ga=GA\d+\.\d+\.(\d+\.\d+)/);
|
||||
if (gaMatch) return gaMatch[1];
|
||||
|
||||
const timestamp = Date.now();
|
||||
const random = Math.floor(Math.random() * 1000000000);
|
||||
return `${random}.${timestamp}`;
|
||||
}
|
||||
|
||||
async function sendToGA4(
|
||||
request: Request,
|
||||
context: Context,
|
||||
pathname: string
|
||||
): Promise<void> {
|
||||
if (!GA_API_SECRET) {
|
||||
console.warn('GA_API_SECRET not configured, skipping analytics');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = getClientId(request);
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown';
|
||||
const isAITool =
|
||||
/bot|crawler|spider|gpt|claude|anthropic|openai|perplexity|cohere/i.test(
|
||||
userAgent
|
||||
);
|
||||
|
||||
const payload = {
|
||||
client_id: clientId,
|
||||
events: [
|
||||
{
|
||||
name: 'server_page_view',
|
||||
params: {
|
||||
page_location: request.url,
|
||||
page_title: pathname,
|
||||
page_path: pathname,
|
||||
content_type: 'text/html',
|
||||
file_extension: '.html',
|
||||
user_agent: userAgent,
|
||||
is_ai_tool: isAITool ? 'true' : 'false',
|
||||
country: context.geo?.country?.code || 'unknown',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
console.log(`Tracked HTML page: ${pathname}`);
|
||||
|
||||
const endpoint = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
||||
|
||||
try {
|
||||
await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to send to GA4:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
request: Request,
|
||||
context: Context
|
||||
): Promise<Response> {
|
||||
const pathname = new URL(request.url).pathname;
|
||||
|
||||
if (shouldTrack(request)) {
|
||||
context.waitUntil(sendToGA4(request, context, pathname));
|
||||
}
|
||||
|
||||
const response = await context.next();
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set('x-nx-edge-function', 'track-page-requests');
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
export const config = {
|
||||
path: ['/docs/*'],
|
||||
excludedPath: [
|
||||
// Text/code files (handled by track-asset-requests or not tracked)
|
||||
'/docs/*.md',
|
||||
'/docs/*.js',
|
||||
'/docs/*.txt',
|
||||
// Images
|
||||
'/docs/*.svg',
|
||||
'/docs/*.png',
|
||||
'/docs/*.jpg',
|
||||
'/docs/*.jpeg',
|
||||
'/docs/*.gif',
|
||||
'/docs/*.webp',
|
||||
'/docs/*.ico',
|
||||
'/docs/images/*',
|
||||
'/docs/og/*',
|
||||
// Fonts
|
||||
'/docs/fonts/*',
|
||||
'/docs/*.woff',
|
||||
'/docs/*.woff2',
|
||||
// Search index (pagefind)
|
||||
'/docs/pagefind/*',
|
||||
// Astro build assets
|
||||
'/docs/_*',
|
||||
],
|
||||
};
|
||||
@@ -16,6 +16,7 @@
|
||||
"@nx/nx-dev-ui-icons": "workspace:*",
|
||||
"@nx/nx-dev-ui-markdoc": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/hast": "^3.0.4",
|
||||
"astro": "^5.10.1",
|
||||
"astro-og-canvas": "^0.7.0",
|
||||
"canvaskit-wasm": "^0.40.0",
|
||||
|
||||
@@ -144,6 +144,78 @@
|
||||
window.twq('config', 'obtp4');
|
||||
};
|
||||
|
||||
// Scroll depth tracking
|
||||
const SCROLL_THRESHOLDS = [10, 25, 50, 75, 90];
|
||||
let firedThresholds = new Set();
|
||||
let scrollTrackingEnabled = false;
|
||||
let scrollRafId = null;
|
||||
|
||||
function getScrollPercentage() {
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
const scrollHeight = document.documentElement.scrollHeight;
|
||||
const clientHeight = window.innerHeight;
|
||||
return (scrollTop + clientHeight) / scrollHeight;
|
||||
}
|
||||
|
||||
function handleScrollTracking() {
|
||||
if (!scrollTrackingEnabled) return;
|
||||
|
||||
const scrollPercentage = getScrollPercentage() * 100;
|
||||
|
||||
// Fire events for all thresholds we've passed but haven't fired yet
|
||||
for (const threshold of SCROLL_THRESHOLDS) {
|
||||
if (scrollPercentage >= threshold && !firedThresholds.has(threshold)) {
|
||||
firedThresholds.add(threshold);
|
||||
sendSearchEvent(`scroll_${threshold}`, {
|
||||
event_category: 'scroll',
|
||||
event_label: window.location.pathname,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function throttledScrollHandler() {
|
||||
if (scrollRafId !== null) return;
|
||||
|
||||
scrollRafId = requestAnimationFrame(() => {
|
||||
handleScrollTracking();
|
||||
scrollRafId = null;
|
||||
});
|
||||
}
|
||||
|
||||
function attachScrollListener() {
|
||||
window.addEventListener('scroll', throttledScrollHandler, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
function setupScrollTracking() {
|
||||
// Reset scroll depth on navigation (for SPA-like behavior via View Transitions)
|
||||
firedThresholds = new Set();
|
||||
scrollTrackingEnabled = false;
|
||||
|
||||
// Delay tracking start to avoid false triggers during navigation
|
||||
setTimeout(() => {
|
||||
scrollTrackingEnabled = true;
|
||||
// Immediately check current scroll position to capture thresholds
|
||||
// that may have been passed during the delay
|
||||
handleScrollTracking();
|
||||
}, 500);
|
||||
|
||||
attachScrollListener();
|
||||
|
||||
// Handle Astro View Transitions - reset on navigation
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
firedThresholds = new Set();
|
||||
scrollTrackingEnabled = false;
|
||||
setTimeout(() => {
|
||||
scrollTrackingEnabled = true;
|
||||
// Immediately check current scroll position after navigation
|
||||
handleScrollTracking();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 1000;
|
||||
let searchDebounceTimer;
|
||||
let lastSearchQuery = '';
|
||||
@@ -209,12 +281,14 @@
|
||||
loadGTM();
|
||||
loadHubSpot();
|
||||
setupSearchTracking();
|
||||
setupScrollTracking();
|
||||
} else if (window.Cookiebot && window.Cookiebot.consent) {
|
||||
// Statistics cookies (Google Analytics, GTM, Search Tracking)
|
||||
// Statistics cookies (Google Analytics, GTM, Search, Scroll Tracking)
|
||||
if (window.Cookiebot.consent.statistics) {
|
||||
loadGoogleAnalytics();
|
||||
loadGTM();
|
||||
setupSearchTracking();
|
||||
setupScrollTracking();
|
||||
}
|
||||
|
||||
// Marketing cookies (HubSpot, Apollo, Hotjar, Twitter)
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 409 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 297 KiB |
@@ -122,6 +122,33 @@ steps:
|
||||
|
||||
{% /tabitem %}
|
||||
|
||||
{% tabitem label="Bitbucket Pipelines" %}
|
||||
|
||||
```yaml
|
||||
# bitbucket-pipelines.yml
|
||||
image: node:22
|
||||
|
||||
pipelines:
|
||||
pull-requests:
|
||||
'**':
|
||||
- step:
|
||||
name: CI
|
||||
script:
|
||||
# Your existing steps which start-ci-run, install
|
||||
# dependencies, etc.
|
||||
# These are just illustrative examples...
|
||||
- npx nx-cloud start-ci-run
|
||||
- npm ci
|
||||
- npx nx affected -t lint test build
|
||||
after-script:
|
||||
# NEW: Add this section at the end of your step
|
||||
# IMPORTANT: after-script runs regardless of step success/failure
|
||||
# so it's like if: always() on GitHub
|
||||
- npx nx fix-ci
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
|
||||
{% /tabs %}
|
||||
|
||||
> NOTE: If all tasks succeed then the `fix-ci` command becomes a no-op automatically, so that is why "always" is recommended.
|
||||
@@ -182,31 +209,62 @@ Tasks matching these patterns will also have high-confidence, verified code chan
|
||||
|
||||
Tasks matching these patterns will **never** have code changes auto-applied, even if they match the include patterns or presets specified above. For example: `*e2e*`.
|
||||
|
||||
## Customization with CLAUDE.md
|
||||
## Configuration with SELF_HEALING.md
|
||||
|
||||
Create a `CLAUDE.md` file in your repository root to provide additional context to the AI agent:
|
||||
Create a `.nx/SELF_HEALING.md` file in your repository to provide project-specific instructions to the Self-Healing CI agent. This file contains freeform markdown that the AI agent reads and interprets naturally.
|
||||
|
||||
### Failure Classification Rules
|
||||
{% aside title="Why a dedicated file?" type="note" %}
|
||||
Using `.nx/SELF_HEALING.md` instead of `AGENTS.md` (or equivalent) separates CI-specific instructions from local development context. The file lives in the `.nx` directory alongside other Nx Cloud configuration.
|
||||
{% /aside %}
|
||||
|
||||
Override how the AI categorizes failures:
|
||||
### Example SELF_HEALING.md
|
||||
|
||||
```markdown
|
||||
## Failure Classification
|
||||
# Self-Healing Configuration
|
||||
|
||||
- Failures in `**/migrations/**` should be classified as `environment_state`
|
||||
- Test timeouts in e2e tests are usually `flaky_task`
|
||||
## Confidence Rules
|
||||
|
||||
- Fixes involving "test" targets should require high confidence
|
||||
- Formatting fixes can be applied with medium confidence
|
||||
|
||||
## Off-Limits Areas
|
||||
|
||||
- `/src/generated/` - auto-generated, do not modify
|
||||
- `/legacy/` - requires manual review
|
||||
|
||||
## Fix Preferences
|
||||
|
||||
- Prefer updating ESLint rules over adding disable comments
|
||||
- For type errors, prefer explicit types over `any`
|
||||
|
||||
## Context
|
||||
|
||||
See ARCHITECTURE.md for module boundaries.
|
||||
```
|
||||
|
||||
### Predefined Fixes
|
||||
### What to Include
|
||||
|
||||
Specify deterministic solutions for common failures:
|
||||
| Section | Purpose | Example |
|
||||
| -------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| **Confidence Rules** | Override how the AI categorizes failure severity | "Failures in `**/migrations/**` should be classified as `environment_state`" |
|
||||
| **Off-Limits Areas** | Directories or files the agent should never modify | "`/src/generated/` - auto-generated code" |
|
||||
| **Fix Preferences** | Guide the agent's approach to common issues | "Prefer updating ESLint rules over adding disable comments" |
|
||||
| **Predefined Fixes** | Specify deterministic solutions for known failures | "For lint failures, always try running `nx lint --fix` first" |
|
||||
| **Context** | Reference other documentation the agent should read | "See ARCHITECTURE.md for module boundaries" |
|
||||
|
||||
```markdown
|
||||
## Predefined Fixes
|
||||
### Using CLAUDE.md
|
||||
|
||||
- For lint failures, always try running `nx lint --fix` first
|
||||
- Format failures should use `nx format:write`
|
||||
```
|
||||
If your repository already has a `CLAUDE.md` file at the root, the Self-Healing CI agent will read it for additional context. When both files exist:
|
||||
|
||||
- **SELF_HEALING.md takes precedence** for any conflicting instructions
|
||||
- Both files are read, so general context in `CLAUDE.md` is still available
|
||||
- CI-specific instructions should go in `SELF_HEALING.md`
|
||||
|
||||
This allows teams to maintain `CLAUDE.md` for local development workflows while using `SELF_HEALING.md` for CI-specific behavior.
|
||||
|
||||
### Viewing Configuration Status
|
||||
|
||||
After a CI run, navigate to the pipeline execution in Nx Cloud and check the **Configurations** tab to see whether `SELF_HEALING.md` was detected and applied.
|
||||
|
||||
## Receiving Fix Notifications
|
||||
|
||||
|
||||
@@ -1,180 +1,82 @@
|
||||
---
|
||||
title: 'Enhance Your LLM'
|
||||
description: 'Learn how Nx enhances your AI assistant by providing rich workspace metadata, architectural insights, and project relationships to make your LLM smarter and more context-aware.'
|
||||
title: 'Enhance Your AI Coding Agent'
|
||||
description: 'Learn how Nx enhances your AI assistant by providing rich workspace metadata, architectural insights, and CI integration for autonomous workflows.'
|
||||
sidebar:
|
||||
order: 3
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
{% youtube src="https://youtu.be/dRQq_B1HSLA" title="We Just Shipped the Monorepo MCP for Copilot" /%}
|
||||
AI agents are moving beyond autocomplete. They can now operate independently across projects. But most setups hit a wall: agents lack workspace context (seeing files, not architecture), generate inconsistent code, and have a hard time to interact with CI.
|
||||
|
||||
Monorepos [provide an ideal foundation for AI-powered development](https://nx.dev/blog/nx-and-ai-why-they-work-together), enabling cross-project reasoning and code generation. However, without proper context, **LLMs struggle to understand your workspace architecture**, seeing only individual files rather than the complete picture.
|
||||
Nx monorepos solve this by enabling cross-project reasoning and by providing the structured metadata and CI integration that agents need to work autonomously:
|
||||
|
||||
Nx transforms your AI assistant by providing rich workspace metadata that enables it to:
|
||||
- Deep **workspace architecture** understanding and project relationships
|
||||
- **Code generators** for fast, predictable scaffolding
|
||||
- **CI pipeline integration** to fix failures autonomously
|
||||
- The ability to **iterate until CI is green** without human intervention
|
||||
|
||||
- Understand your **workspace architecture** and project relationships
|
||||
- Identify **project owners** and team responsibilities
|
||||
- Access **Nx documentation** for accurate guidance
|
||||
- Leverage **code generators** for consistent scaffolding
|
||||
- Connect to your **CI pipeline** to help fix failures
|
||||
## Setup
|
||||
|
||||
The goal is to transform your AI assistant from a generic code helper into an architecturally-aware collaborator that understands your specific workspace structure and can make intelligent, context-aware decisions.
|
||||
|
||||
## How Nx MCP Enhances Your LLM
|
||||
|
||||
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI models to interact with your development environment through a standardized interface. Nx implements an MCP server via the [Nx Console](/docs/getting-started/editor-setup) that exposes workspace metadata to compatible AI assistants like GitHub Copilot, Claude, and others.
|
||||
|
||||
With the Nx MCP server, your AI assistant gains a "map" of your entire system being able to go from just reasoning at the file level to seeing the higher-level picture. This allows the LLM to move between different abstraction levels - from high-level architecture down to specific implementation details:
|
||||
|
||||

|
||||
|
||||
The Nx MCP server exposes tools for workspace analysis, code generation, documentation lookup, and CI/CD analytics. For a complete list of available tools and their descriptions, see the [Nx MCP Server Reference](/docs/reference/nx-mcp#available-tools).
|
||||
|
||||
## Setting Up Nx MCP
|
||||
|
||||
To configure Nx for AI agents and AI-Assistants, run the following command:
|
||||
To configure your Nx workspace for AI agents, run:
|
||||
|
||||
```shell
|
||||
npx nx configure-ai-agents
|
||||
```
|
||||
|
||||
This configures Nx Console which automatically configures and serves the Nx MCP server for you if you're using VSCode or Cursor. It also sets up the corresponding AI agent configuration files (e.g. `CLAUDE.md`, `AGENTS.md`,...).
|
||||
This sets up:
|
||||
|
||||
### IDE Setup
|
||||
- **Agent configuration files**: `CLAUDE.md`, `AGENTS.md` with workspace-specific guidelines
|
||||
- **Agent skills**: Specialized capabilities for monorepo workflows, including CI monitoring and self-healing integration
|
||||
- **Nx MCP server**: Provides tools that combine local workspace metadata with CI context, enabling seamless local-to-CI workflows that unlock true agent autonomy
|
||||
|
||||
For VS Code, Cursor, and JetBrains IDE users:
|
||||
## What This Enables
|
||||
|
||||
1. Install [Nx Console](/docs/getting-started/editor-setup) from the marketplace
|
||||
2. You'll receive a notification to "Improve Copilot/AI agent with Nx-specific context"
|
||||
3. Click "Yes" to configure the MCP server
|
||||
### Self-Healing CI Integration
|
||||
|
||||

|
||||
Nx Cloud provides AI-powered [Self-Healing CI](/docs/features/ci-features/self-healing-ci) that analyzes failed runs and proposes verified fixes. With `configure-ai-agents`, your local agent connects to this CI counterpart via skills and the Nx MCP, gaining full context about run information, failures, and suggested fixes.
|
||||
|
||||
If you miss the notification, run the `nx.configureMcpServer` (`Nx: Setup MCP Server` in JetBrains) command from the command palette (Cursor: `Ctrl/Cmd + Shift + P`, JetBrains IDEs: `Ctrl/Cmd + Shift + A`).
|
||||
|
||||
### Other MCP-Compatible Clients
|
||||
|
||||
For other MCP-compatible clients like Claude Desktop, Claude Code, or Warp, you can configure the Nx MCP server manually. See the [Nx MCP Server Reference](/docs/reference/nx-mcp#client-specific-setup) for detailed setup instructions for each client.
|
||||
|
||||
Quick example for Claude Code:
|
||||
|
||||
```shell
|
||||
claude mcp add nx-mcp npx nx-mcp@latest
|
||||
```
|
||||
|
||||
## Powerful Use Cases
|
||||
|
||||
### Understanding Your Workspace Architecture
|
||||
|
||||
{% youtube src="https://youtu.be/RNilYmJJzdk" title="Nx Just Made Your LLM Way Smarter" /%}
|
||||
|
||||
Ask your AI assistant about your workspace structure and get detailed, accurate responses about projects, their types, and relationships:
|
||||
Your agent can autonomously iterate until CI passes:
|
||||
|
||||
```text
|
||||
What is the structure of this workspace?
|
||||
How are the projects organized?
|
||||
Commit this work, create a PR, and monitor CI until it's green.
|
||||
```
|
||||
|
||||
With Nx MCP, your AI assistant can:
|
||||
The workflow:
|
||||
|
||||
- Identify applications and libraries in your workspace
|
||||
- Understand project categorization through tags
|
||||
- Recognize technology types (feature, UI, data-access)
|
||||
- Determine project ownership and team responsibilities
|
||||
1. Agent pushes changes and creates PR
|
||||
2. Monitors CI pipeline
|
||||
3. Receives failure context from Nx Cloud and Self-Healing CI
|
||||
4. Accepts proposed fix or pulls context locally and manually applies it
|
||||
5. Repeats until CI is green
|
||||
|
||||

|
||||
This reduces context-switching—you review the final PR rather than intervening at each failure.
|
||||
|
||||
You can also get informed suggestions about where to implement new functionality:
|
||||
### Workspace Architecture Understanding
|
||||
|
||||
```text
|
||||
Where should I implement a feature for adding products to cart?
|
||||
```
|
||||
Nx exposes the project graph and relevant metadata to AI agents. This helps them move faster and more precisely:
|
||||
|
||||

|
||||
- Identify all applications and libraries in the workspace
|
||||
- Understand project relationships and dependencies
|
||||
- Recognize project types and ownership via tags
|
||||
- Determine which projects are affected by changes
|
||||
- Suggest where to implement new functionality based on existing structure
|
||||
|
||||
Learn more about workspace architecture understanding in our blog post [Nx Just Made Your LLM Way Smarter](https://nx.dev/blog/nx-just-made-your-llm-smarter).
|
||||
This architectural awareness is critical for agents operating in large monorepos where understanding project relationships determines the quality of generated code.
|
||||
|
||||
### Instant CI Failure Resolution
|
||||
### Predictable, Fast Code Generation
|
||||
|
||||
{% youtube src="https://youtu.be/fPqPh4h8RJg" title="Connect Your Editor, CI and LLMs" /%}
|
||||
AI-generated code is token-intensive, slow, and not guaranteed to align with patterns in other projects. Nx generators solve this by providing predictable scaffolding that agents can invoke and then adapt.
|
||||
|
||||
When a CI build fails, Nx Console can notify you directly in your editor:
|
||||
Your AI agent can:
|
||||
|
||||

|
||||
1. Find generators from [Nx plugins](/docs/plugin-registry) or custom [local workspace generators](/docs/extending-nx/local-generators)
|
||||
2. Run the generator with correct options
|
||||
3. Make small adjustments based on the specific situation
|
||||
|
||||
Your AI assistant can then:
|
||||
This approach is faster, produces consistent code across projects, and reduces hallucinations.
|
||||
|
||||
1. Access detailed information from Nx Cloud about the failed build
|
||||
2. Analyze your git history to understand what changed in your PR
|
||||
3. Understand the error context and affected files
|
||||
4. Help implement the fix right in your editor
|
||||
## Learn More
|
||||
|
||||
This integration dramatically improves the development velocity because you get immediately notified when an error occurs, you don't even have to leave your editor to understand what broke, and the LLM can help you implement or suggest a possible fix.
|
||||
|
||||
Learn more about CI integration in our blog post [Save Time: Connecting Your Editor, CI and LLMs](https://nx.dev/blog/nx-editor-ci-llm-integration).
|
||||
|
||||
### Smart Code Generation with AI-Enhanced Generators
|
||||
|
||||
{% youtube src="https://youtu.be/PXNjedYhZDs" title="Enhancing Nx Generators with AI" /%}
|
||||
|
||||
Nx generators provide predictable code scaffolding, while AI adds intelligence and contextual understanding. Instead of having the AI generate everything from scratch, you get the best of both worlds:
|
||||
|
||||
```text
|
||||
Create a new React library into the packages/orders/feat-cancel-orders folder
|
||||
and call the library with the same name of the folder structure. Afterwards,
|
||||
also connect it to the main shop application.
|
||||
```
|
||||
|
||||
Your AI assistant will:
|
||||
|
||||
1. Identify the appropriate generator and its parameters
|
||||
2. Open the Nx Console Generate UI with preset values
|
||||
3. Let you review and customize the options
|
||||
4. Execute the generator and help integrate the new code with your existing projects
|
||||
|
||||

|
||||
|
||||
This approach ensures consistent code that follows your organization's best practices while still being tailored to your specific needs. Learn more about AI-enhanced generators in our blog post [Enhancing Nx Generators with AI](https://nx.dev/blog/nx-generators-ai-integration).
|
||||
|
||||
### Documentation-Aware Configuration
|
||||
|
||||
{% youtube src="https://youtu.be/V2W94Sq_v6A?si=aBA-eppEw0fHrh5O&t=388" title="Making Cursor Smarter with an MCP Server" /%}
|
||||
|
||||
Get accurate guidance on Nx configuration without worrying about hallucinations or outdated information:
|
||||
|
||||
```text
|
||||
Can you configure Nx release for the packages of this workspace?
|
||||
Update nx.json with the necessary configuration using conventional commits
|
||||
as the versioning strategy.
|
||||
```
|
||||
|
||||
The AI assistant will:
|
||||
|
||||
1. Query the Nx docs for the latest information on release configuration
|
||||
2. Understand your workspace structure to identify packages
|
||||
3. Generate the correct configuration based on your specific needs
|
||||
4. Apply the changes to your nx.json file
|
||||
|
||||
Learn more about documentation-aware configuration in our blog post [Making Cursor Smarter with an MCP Server For Nx Monorepos](https://nx.dev/blog/nx-made-cursor-smarter).
|
||||
|
||||
### Cross-Project Dependency Analysis
|
||||
|
||||
{% youtube src="https://youtu.be/dRQq_B1HSLA?si=lhHsjRvwgijC1IL8&t=186" title="Nx MCP Now Available for VS Code Copilot" /%}
|
||||
|
||||
Understand the impact of changes across your monorepo with questions like:
|
||||
|
||||
```text
|
||||
If I change the public API of feat-product-detail, which other projects
|
||||
might be affected by that change?
|
||||
```
|
||||
|
||||
Your AI assistant can:
|
||||
|
||||
- Analyze the project graph to identify direct and indirect dependencies
|
||||
- Visualize affected projects using the `nx_visualize_graph` tool
|
||||
- Suggest strategies for refactoring that minimize impact
|
||||
- Identify which teams would need to be consulted for major changes
|
||||
|
||||
This architectural awareness is particularly powerful in larger monorepos where understanding project relationships is crucial for making informed development decisions.
|
||||
|
||||
Learn more about dependency analysis in our blog post [Nx MCP Now Available for VS Code Copilot](https://nx.dev/blog/nx-mcp-vscode-copilot).
|
||||
- [Autonomous AI Agents at Scale](https://nx.dev/blog/ai-agents-and-continuity): Infrastructure requirements for AI agent workflows
|
||||
- [Why Nx and AI Work So Well Together](https://nx.dev/blog/nx-and-ai-why-they-work-together): The foundation for AI-powered development
|
||||
- [Nx MCP Server Reference](/docs/reference/nx-mcp): Complete tool reference and setup instructions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'Building and Testing Angular Apps in Nx'
|
||||
description: In this tutorial you'll create a frontend-focused workspace with Nx.
|
||||
description: In this tutorial you'll create a frontend-focused monorepo with Nx.
|
||||
sidebar:
|
||||
label: 'Angular Monorepo'
|
||||
filter: 'type:Guides'
|
||||
|
||||
@@ -106,7 +106,7 @@ Root project 'gradle-tutorial'
|
||||
|
||||
## Add Nx
|
||||
|
||||
Nx is a build system with built in tooling and advanced CI capabilities. It helps you maintain and scale monorepos,
|
||||
Nx is a monorepo platform with built in tooling and advanced CI capabilities. It helps you maintain and scale monorepos,
|
||||
both locally and on CI. We will explore the features of Nx in this tutorial by adding it to the Gradle workspace above.
|
||||
|
||||
To add Nx, run
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: 'Building and Testing React Apps in Nx'
|
||||
sidebar:
|
||||
label: 'React Monorepo'
|
||||
description: In this tutorial you'll create a frontend-focused workspace with Nx.
|
||||
description: In this tutorial you'll create a frontend-focused monorepo with Nx.
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ sidebar:
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
Nx provides deep integration with AI coding assistants through the **Nx Model Context Protocol (MCP) server**, giving your AI assistant comprehensive understanding of your monorepo structure, running processes, and development workflows. When you create a new Nx workspace, it includes AI agent configuration files (`CLAUDE.md` and `AGENTS.md`) that provide guidelines for working with Nx and the Nx MCP server.
|
||||
This guide shows how to configure your Nx workspace for AI coding assistants. The setup gives your agent workspace context and CI integration, making it smarter when working in an Nx monorepo and more autonomous when iterating on CI failures.
|
||||
|
||||
## Configure Nx AI Integration
|
||||
|
||||
@@ -18,48 +18,33 @@ title="Set Up AI Agents in Nx"
|
||||
|
||||
### Automatic AI Setup
|
||||
|
||||
To automatically configure your Nx workspace to work best with AI agents and assistants, run the following command:
|
||||
To automatically configure your Nx monorepo to work best with AI agents and assistants, run the following command:
|
||||
|
||||
```shell
|
||||
npx nx configure-ai-agents
|
||||
```
|
||||
|
||||
This will prompt you for which AI agents/assistants to configure and make sure you are properly set up with both the [Nx MCP](/docs/features/enhance-ai) as well as corresponding AI agent configuration files (e.g. `AGENTS.md`, `CLAUDE.md` etc).
|
||||
This will prompt you for which AI agents/assistants to configure and set up the [Nx MCP server](/docs/features/enhance-ai), AI agent configuration files (`AGENTS.md`, `CLAUDE.md`, etc.), and agent skills. For Claude Code, skills are installed via a plugin; for other agents, they're copied into your workspace.
|
||||
|
||||
Watch [our Youtube video](https://youtu.be/8gdvIz2r_QM) for a full walkthrough.
|
||||
|
||||
### Manual AI Setup
|
||||
|
||||
You can of course manually configure the Nx MCP for MCP-compatible clients using the following configuration:
|
||||
|
||||
```json
|
||||
// mcp.json
|
||||
{
|
||||
"servers": {
|
||||
"nx-mcp": {
|
||||
"command": "npx",
|
||||
"args": ["nx-mcp@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Claude Code:
|
||||
Alternatively, you can install just the skills via:
|
||||
|
||||
```shell
|
||||
claude mcp add nx-mcp npx nx-mcp@latest
|
||||
npx skills add nrwl/nx-ai-agents-config
|
||||
```
|
||||
|
||||
This copies the skills into your workspace but does not install the Claude Code plugin.
|
||||
|
||||
Watch [our Youtube video](https://youtu.be/8gdvIz2r_QM) for a full walkthrough.
|
||||
|
||||
### What This Integration Enables
|
||||
|
||||
The Nx AI integration provides your coding assistant with powerful capabilities:
|
||||
|
||||
- **[Workspace Structure Understanding](https://nx.dev/blog/nx-mcp-vscode-copilot)** - Deep architectural awareness of your monorepo, project relationships, and dependencies
|
||||
- **[Real-time Terminal Integration](https://nx.dev/blog/nx-terminal-integration-ai)** - AI can read your terminal output, running processes, and error messages without copy-pasting
|
||||
- **[CI Pipeline Context](https://nx.dev/blog/nx-editor-ci-llm-integration)** - Access to build failures, test results, and deployment status from your CI/CD processes
|
||||
- **[Enhanced Code Generation](https://nx.dev/blog/nx-generators-ai-integration)** - AI-powered generator suggestions and custom scaffolding with intelligent defaults
|
||||
- **Cross-project Impact Analysis** - Understanding the implications of changes across your entire monorepo
|
||||
- **Autonomous Error Debugging** - AI independently accesses context to help fix development issues
|
||||
- **Workspace Understanding** - Graph-aware exploration of project dependencies and relationships. AI gets structured data instead of grepping through files.
|
||||
- **[Real-time Terminal Integration](https://nx.dev/blog/nx-terminal-integration-ai)** - AI can read your terminal output, running processes, and error messages without copy-pasting.
|
||||
- **Reliable Code Generation** - AI invokes Nx generators for predictable scaffolding, then adapts the result to your workspace. Faster, standardized, fewer hallucinations.
|
||||
- **Autonomous CI Workflows** - The CI monitor skill bridges your local agent with Nx Cloud. Push, monitor, get failures, fix, repeat until CI is green. Enables "Ralph Wiggum loop" patterns where you review the final PR, not every CI hiccup.
|
||||
- **Cross-project Impact Analysis** - Understanding the implications of changes across your entire monorepo.
|
||||
|
||||
## Configure CI to Leverage AI Capabilities
|
||||
|
||||
@@ -69,4 +54,5 @@ Read more on the [Self-Healing CI](/docs/features/ci-features/self-healing-ci) d
|
||||
|
||||
## Learn More about Nx and AI
|
||||
|
||||
Learn more about [why Nx and AI work so well together](https://nx.dev/blog/nx-and-ai-why-they-work-together).
|
||||
- [Autonomous AI Agents at Scale](https://nx.dev/blog/ai-agents-and-continuity) - Infrastructure requirements for scaling AI agent workflows
|
||||
- [Why Nx and AI Work So Well Together](https://nx.dev/blog/nx-and-ai-why-they-work-together)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: What is Nx?
|
||||
description: 'Nx is an AI-first build platform that connects everything from your editor to CI Helping you deliver fast, without breaking things.'
|
||||
description: 'Nx is an AI-first monorepo platform that connects everything from your editor to CI. Helping you deliver fast, without breaking things.'
|
||||
sidebar:
|
||||
order: 1
|
||||
label: Introduction
|
||||
filter: 'type:Features'
|
||||
---
|
||||
|
||||
Nx is a powerful, open source, technology-agnostic build platform designed to efficiently manage codebases of any scale. From small single projects to large enterprise monorepos, Nx provides the platform to **efficiently get from starting a feature in your editor to a green PR**.
|
||||
Nx is a powerful, open-source, technology-agnostic **monorepo platform** designed to efficiently manage codebases of any scale. From small workspaces to large enterprise monorepos, Nx provides the tools to **efficiently get from starting a feature in your editor to a green PR**.
|
||||
|
||||
As teams and codebases grow, productivity bottlenecks multiply: build times increase, CI becomes flaky, and code sharing becomes complex. **Nx reduces friction across your entire development cycle.**
|
||||
|
||||
@@ -17,6 +17,12 @@ As teams and codebases grow, productivity bottlenecks multiply: build times incr
|
||||
|
||||
Nx is built in a modular fashion, allowing you to adopt as little or as much as you'd like at any moment in your development lifecycle. You can **start with just the core and add additional capabilities incrementally** as your needs grow and complexity increases.
|
||||
|
||||
{% callout type="deepdive" title="Can I add Nx to a single-project repo?" %}
|
||||
Yes, Nx provides value even for single-project repositories. You get fast task caching, intelligent task orchestration, and access to Nx plugins for your specific technology stack. As your project grows into a monorepo, the foundation is already in place.
|
||||
|
||||
Nx can also connect multiple repositories into a synthetic monorepo, letting you orchestrate large changes across all connected repos.
|
||||
{% /callout %}
|
||||
|
||||
At the **foundation is Nx Core**, a Rust-based, technology-agnostic task runner. Nx Core creates a knowledge graph of your workspace, understanding project relationships and dependencies. This enables highly optimized and fast task execution regardless of technology stack. It runs `package.json` scripts in [TypeScript monorepos](/docs/technologies/typescript/introduction) or Gradle tasks in [Java projects](/docs/technologies/java/introduction) or [can be extended](/docs/extending-nx/intro) to meet your project's specific needs.
|
||||
|
||||
{% callout type="deepdive" title="What do you mean by \"running NPM scripts\"?" %}
|
||||
@@ -60,7 +66,7 @@ From there, you can gradually enhance your setup by adding features like [task c
|
||||
{% /callout %}
|
||||
|
||||
Nx Core provides everything you need to get started and works perfectly on its own.
|
||||
**When you're ready for more, the Nx platform offers additional capabilities you can adopt incrementally**.
|
||||
**When you're ready for more, the Nx monorepo platform offers additional capabilities you can adopt incrementally**.
|
||||
Extend your setup with [**Nx Cloud**](/docs/getting-started/nx-cloud) for remote caching, distributed task execution, and [**AI-powered self-healing CI**](/docs/features/ci-features/self-healing-ci) that automatically detects, analyzes, and fixes CI failures.
|
||||
Integrate [**Nx Console**](/docs/getting-started/editor-setup) with your editor for powerful autocomplete, project graph visualization, CI notifications, and an MCP to [make your AI coding assistant smarter](/docs/features/enhance-ai).
|
||||
Add [**Nx Plugins**](/docs/technologies) for technology-specific automation and DX improvements, or build custom platform capabilities using [Nx Devkit](/docs/extending-nx/intro).
|
||||
|
||||
@@ -62,7 +62,7 @@ Choose a preset that matches your technology stack. This gives you a fully confi
|
||||
|
||||
You can also choose **an empty workspace preset** (`--preset=ts`) which sets up the bare minimum configuration for TypeScript and Nx. This allows you to add technologies and features incrementally over time as you need them.
|
||||
|
||||
## Option 3: Get the complete Nx platform experience
|
||||
## Option 3: Get the complete Nx monorepo platform experience
|
||||
|
||||
[](https://cloud.nx.app/get-started?utm_source=nx-docs&utm_medium=nx-cloud-onboarding&utm_campaign=start-new-project)
|
||||
|
||||
@@ -74,4 +74,4 @@ This means you benefit from intelligent automation right from day one, without h
|
||||
|
||||
As your project grows and scales, you'll have access to additional features like **remote caching** to speed up builds across your team, **distributed task execution with Nx Agents** to parallelize work across multiple machines, and **automatic test splitting** to optimize your CI pipeline performance.
|
||||
|
||||
[Get started with the complete Nx platform experience →](https://cloud.nx.app/get-started?utm_source=nx-docs&utm_medium=nx-cloud-onboarding&utm_campaign=start-new-project)
|
||||
[Get started with the complete Nx monorepo platform experience →](https://cloud.nx.app/get-started?utm_source=nx-docs&utm_medium=nx-cloud-onboarding&utm_campaign=start-new-project)
|
||||
|
||||
@@ -77,7 +77,7 @@ You can implement your server in any programming language or framework, as long
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Nx custom remote cache specification.",
|
||||
"description": "Nx is an AI-first build platform that connects everything from your editor to CI. Helping you deliver fast, without breaking things.",
|
||||
"description": "Nx is an AI-first monorepo platform that connects everything from your editor to CI. Helping you deliver fast, without breaking things.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Quickstart with Nx
|
||||
description: Get up and running with Nx in minutes - install Nx, set up your editor, configure AI assistance, and choose your development path.
|
||||
description: Get up and running with the Nx monorepo platform in minutes - install Nx, set up your editor, configure AI assistance, and choose your development path.
|
||||
tableOfContents: false
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
@@ -9,22 +9,39 @@ filter: 'type:References'
|
||||
|
||||
The Nx MCP server is a [Model Context Protocol](https://modelcontextprotocol.io/introduction) implementation that gives LLMs deep access to your monorepo's structure: project relationships, file mappings, runnable tasks, ownership info, tech stacks, Nx generators, and Nx documentation.
|
||||
|
||||
{% aside type="tip" title="Quick Setup" %}
|
||||
To configure your Nx workspace for AI agents (MCP, skills, and more), run `npx nx configure-ai-agents`. See the [AI Integration guide](/docs/getting-started/ai-setup) for details.
|
||||
{% /aside %}
|
||||
|
||||
For an overview of how Nx enhances AI assistants and powerful use cases, see the [Enhance Your LLM](/docs/features/enhance-ai) guide.
|
||||
|
||||
## Installation
|
||||
|
||||
There are a few ways to setup the Nx MCP server
|
||||
There are a few ways to setup the Nx MCP server.
|
||||
|
||||
{% aside type="note" title="Top level command" %}
|
||||
Starting with Nx v21.4.0, you can run the MCP server directly using the `nx mcp` command.
|
||||
You can still use the standalone `nx-mcp` package if you prefer.
|
||||
All args are passed through to the underlying MCP server.
|
||||
{% /aside %}
|
||||
### via .mcp.json
|
||||
|
||||
### via mcp.json
|
||||
{% tabs syncKey="nx-version" %}
|
||||
{% tabitem label="Nx >= 21.4" %}
|
||||
|
||||
```json
|
||||
// mcp.json
|
||||
// .mcp.json
|
||||
{
|
||||
"servers": {
|
||||
"nx-mcp": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["nx", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% tabitem label="Nx < 21.4" %}
|
||||
|
||||
```json
|
||||
// .mcp.json
|
||||
{
|
||||
"servers": {
|
||||
"nx-mcp": {
|
||||
@@ -36,26 +53,55 @@ All args are passed through to the underlying MCP server.
|
||||
}
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
### Via Nx Console Extension
|
||||
|
||||
If you're using Cursor, VS Code, or JetBrains IDEs, install the [Nx Console extension](/docs/getting-started/editor-setup) which automatically manages the MCP server for you.
|
||||
If you're using Cursor or VS Code, install the [Nx Console extension](/docs/getting-started/editor-setup) which automatically manages the MCP server for you.
|
||||
|
||||
## Client-Specific Setup
|
||||
|
||||
> If your preferred client is not listed, then configure your client to run the `npx nx-mcp` command to start up the Nx MCP server.
|
||||
> If your preferred client is not listed, configure your client to run `npx nx mcp` (Nx >= 21.4) or `npx nx-mcp@latest` (older versions) to start the Nx MCP server.
|
||||
|
||||
### Claude Code
|
||||
|
||||
{% tabs syncKey="nx-version" %}
|
||||
{% tabitem label="Nx >= 21.4" %}
|
||||
|
||||
```shell
|
||||
claude mcp add nx-mcp npx nx mcp
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% tabitem label="Nx < 21.4" %}
|
||||
|
||||
```shell
|
||||
claude mcp add nx-mcp npx nx-mcp@latest
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
### VS Code
|
||||
|
||||
{% tabs syncKey="nx-version" %}
|
||||
{% tabitem label="Nx >= 21.4" %}
|
||||
|
||||
```shell
|
||||
code --add-mcp '{"name":"nx-mcp","command":"npx","args":["nx","mcp"]}'
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% tabitem label="Nx < 21.4" %}
|
||||
|
||||
```shell
|
||||
code --add-mcp '{"name":"nx-mcp","command":"npx","args":["nx-mcp"]}'
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
Alternatively, configure it in your VS Code settings or use the Nx Console extension for automatic setup.
|
||||
|
||||
### Cursor
|
||||
@@ -76,7 +122,7 @@ If you miss the notification, run the `Nx: Setup MCP Server` command from the co
|
||||
|
||||
## Command-Line Options
|
||||
|
||||
The `nx-mcp` command accepts the following options:
|
||||
The `nx mcp` command (or `npx nx-mcp` for older versions) accepts the following options:
|
||||
|
||||
| Option | Alias | Description |
|
||||
| ----------------------- | ----- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -92,16 +138,49 @@ The `nx-mcp` command accepts the following options:
|
||||
|
||||
If you want to host the server instead of communicating via `stdio`, use the `--transport` and `--port` flags:
|
||||
|
||||
{% tabs syncKey="nx-version" %}
|
||||
{% tabitem label="Nx >= 21.4" %}
|
||||
|
||||
```shell
|
||||
npx nx mcp --transport sse --port 9921
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% tabitem label="Nx < 21.4" %}
|
||||
|
||||
```shell
|
||||
npx nx-mcp@latest --transport sse --port 9921
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
The HTTP transport supports multiple concurrent connections, allowing different clients to connect simultaneously with independent sessions.
|
||||
|
||||
### Filtering Tools
|
||||
|
||||
You can limit which tools are available using the `--tools` option with glob patterns:
|
||||
|
||||
{% tabs syncKey="nx-version" %}
|
||||
{% tabitem label="Nx >= 21.4" %}
|
||||
|
||||
```shell
|
||||
# Enable all tools (default)
|
||||
npx nx mcp --tools "*"
|
||||
|
||||
# Disable specific tools
|
||||
npx nx mcp --tools "*" "!nx_docs"
|
||||
|
||||
# Enable only cloud analytics tools
|
||||
npx nx mcp --tools "cloud_*"
|
||||
|
||||
# Enable workspace tools only
|
||||
npx nx mcp --tools "nx_workspace" "nx_project_details" "nx_docs"
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% tabitem label="Nx < 21.4" %}
|
||||
|
||||
```shell
|
||||
# Enable all tools (default)
|
||||
npx nx-mcp@latest --tools "*"
|
||||
@@ -116,6 +195,9 @@ npx nx-mcp@latest --tools "cloud_*"
|
||||
npx nx-mcp@latest --tools "nx_workspace" "nx_project_details" "nx_docs"
|
||||
```
|
||||
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
This is useful when you want to restrict the LLM's capabilities or reduce noise in the tool list.
|
||||
|
||||
## Available Tools
|
||||
@@ -151,9 +233,18 @@ This is useful when you want to restrict the LLM's capabilities or reduce noise
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `nx_visualize_graph` | Opens interactive project or task graph visualizations (requires a running IDE instance with Nx Console) |
|
||||
|
||||
### Nx Cloud CI Tools
|
||||
|
||||
These tools enable AI agents to interact with CI pipelines and self-healing capabilities:
|
||||
|
||||
| Tool | Description |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ci_information` | Retrieves CI pipeline execution information from Nx Cloud for the current branch. Returns pipeline status, failed tasks, and self-healing status. |
|
||||
| `update_self_healing_fix` | Apply or reject a self-healing CI fix from Nx Cloud. Records the decision on the suggested fix. |
|
||||
|
||||
### Nx Cloud Analytics Tools
|
||||
|
||||
These tools are only available when connected to an Nx Cloud-enabled workspace. They provide analytics and insights into your CI/CD data:
|
||||
These tools provide analytics and insights into your CI/CD data:
|
||||
|
||||
| Tool | Description |
|
||||
| -------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
@@ -164,8 +255,8 @@ These tools are only available when connected to an Nx Cloud-enabled workspace.
|
||||
| `cloud_analytics_tasks_search` | Analyzes aggregated task performance statistics including success rates and cache hit rates |
|
||||
| `cloud_analytics_task_executions_search` | Analyzes individual task execution data to investigate performance trends over time |
|
||||
|
||||
{% aside type="note" title="Limited Tools Without Workspace" %}
|
||||
When no workspace path is specified, only the `nx_docs` and `nx_available_plugins` tools will be available.
|
||||
{% aside type="note" title="Tool Availability" %}
|
||||
When no workspace path is specified, only the `nx_docs` and `nx_available_plugins` tools will be available. Nx Cloud tools require a workspace connected to Nx Cloud.
|
||||
{% /aside %}
|
||||
|
||||
## Available Resources
|
||||
@@ -246,3 +337,18 @@ Using the Nx Cloud tools, the AI can:
|
||||
1. Access detailed information about the failed build
|
||||
2. Retrieve terminal output from failed tasks
|
||||
3. Understand what changed and suggest fixes
|
||||
|
||||
### Autonomous CI Monitoring
|
||||
|
||||
With the `ci_information` and `update_self_healing_fix` tools, AI agents can monitor CI pipelines and interact with self-healing. For a ready-to-use implementation, see the [CI monitoring skill](/docs/features/enhance-ai#self-healing-ci-integration) in Claude Code:
|
||||
|
||||
```text
|
||||
Monitor CI and fix any failures
|
||||
```
|
||||
|
||||
The AI agent can:
|
||||
|
||||
1. Poll CI pipeline status via `ci_information`
|
||||
2. Receive failure context and self-healing suggestions
|
||||
3. Apply verified fixes via `update_self_healing_fix`
|
||||
4. Continue iterating until CI passes
|
||||
|
||||
@@ -21,7 +21,7 @@ All .NET build targets generated by the plugin include a `dependsOn` configurati
|
||||
"targets": {
|
||||
"build": {
|
||||
"command": "dotnet build",
|
||||
"dependsOn": ["restore", "^build"]
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ All .NET build targets generated by the plugin include a `dependsOn` configurati
|
||||
|
||||
The `^build` dependency tells Nx to build all upstream dependencies before building the current project. This replaces the .NET CLI's built-in dependency resolution with Nx's more sophisticated task orchestration.
|
||||
|
||||
{% aside type="note" title="Why restore is not in dependsOn" %}
|
||||
The `restore` target is not included in the `dependsOn` array because Nx requires NuGet package restoration to be completed before running any tasks. Running `restore` through Nx's task graph would create a circular dependency since Nx needs restoration completed to analyze project structure and dependencies.
|
||||
{% /aside %}
|
||||
|
||||
## Avoiding duplicate builds
|
||||
|
||||
Consider a workspace with the following structure:
|
||||
|
||||
@@ -6,7 +6,7 @@ sidebar:
|
||||
filter: 'type:Guides'
|
||||
---
|
||||
|
||||
Nx is a general-purpose build system and a general-purpose CLI. It works with JavaScript, TypeScript, Java, C#, Go, etc.. The core plugins Nx comes with do work best with JavaScript or TypeScript.
|
||||
Nx is a general-purpose monorepo platform and CLI. It works with JavaScript, TypeScript, Java, C#, Go, etc.. The core plugins Nx comes with do work best with JavaScript or TypeScript.
|
||||
|
||||
TypeScript is a great choice for many teams, but not for everyone. If you want to use Nx with JavaScript, simply pass `--js` to all generate commands, as follows:
|
||||
|
||||
|
||||
@@ -23,10 +23,23 @@ Problem: A task is being executed when you expect it to be replayed from the cac
|
||||
|
||||
{% youtube src="https://youtu.be/zJmhW1iIxpc" title="Debug remote cache misses with Nx Cloud" /%}
|
||||
|
||||
- Make sure your repo is [connected to Nx Cloud](/docs/features/ci-features/remote-cache)
|
||||
- Click on the run details link that is printed in the terminal after you run a task
|
||||
- Click on the task with cache miss that you want to investigate
|
||||
- Click the "Compare to similar tasks" link in the top right corner of the task details
|
||||
- Select one of the similar tasks from the list in the "Compare to" section (or paste a URL of another run)
|
||||
- Nx Cloud will compare the input details of both tasks and will highlight all the differences
|
||||
- Note: Nx Cloud cannot access your source code, so it can only tell you which inputs are different based on their saved content hash, but not the exact git diff of the source code.
|
||||
1. Make sure your repo is [connected to Nx Cloud](/docs/features/ci-features/remote-cache)
|
||||
2. Click on the run details link that is printed in the terminal after you run a task. You can search for and filter tasks by cache status to find the task with the cache miss you want to investigate.
|
||||
|
||||

|
||||
|
||||
3. Click on the task with the cache miss to open the task details panel, then click the "Compare to similar tasks" button.
|
||||
|
||||

|
||||
|
||||
4. Select one of the similar tasks from the list in the "Compared to" section, or paste a run URL to compare against a specific run.
|
||||
|
||||

|
||||
|
||||
5. Nx Cloud will compare the hash inputs of both tasks and highlight all the differences, making it easy to identify which inputs changed.
|
||||
|
||||

|
||||
|
||||
{% aside type="note" title="Note" %}
|
||||
Nx Cloud cannot access your source code, so it can only tell you which inputs are different based on their saved content hash, but not the exact git diff of the source code.
|
||||
{% /aside %}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const contentDocsDir = join(__dirname, '../content/docs');
|
||||
|
||||
interface DocEntry {
|
||||
slug: string;
|
||||
title: string;
|
||||
content: string;
|
||||
section: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips YAML frontmatter from markdown content.
|
||||
* Frontmatter is delimited by --- at the start of the file.
|
||||
*/
|
||||
function stripFrontmatter(content: string): string {
|
||||
// Match frontmatter: starts with ---, ends with ---
|
||||
const frontmatterRegex = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
||||
return content.replace(frontmatterRegex, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates llms-full.txt - a concatenated file of all Nx documentation.
|
||||
* This file contains the full content of all docs for LLM consumption.
|
||||
* See: https://llmstxt.org/
|
||||
*/
|
||||
export const GET: APIRoute = async ({ site }) => {
|
||||
const siteUrl = site?.origin ?? 'https://nx.dev';
|
||||
const entries: DocEntry[] = [];
|
||||
|
||||
// Preferred section order (same as llms.txt)
|
||||
const sectionOrder = [
|
||||
'quickstart',
|
||||
'getting-started',
|
||||
'concepts',
|
||||
'features',
|
||||
'guides',
|
||||
'extending-nx',
|
||||
'technologies',
|
||||
'reference',
|
||||
'enterprise',
|
||||
'troubleshooting',
|
||||
];
|
||||
|
||||
// Section display names
|
||||
const sectionNames: Record<string, string> = {
|
||||
'getting-started': 'Getting Started',
|
||||
concepts: 'Core Concepts',
|
||||
features: 'Features',
|
||||
guides: 'Guides',
|
||||
'extending-nx': 'Extending Nx',
|
||||
technologies: 'Technologies',
|
||||
reference: 'Reference',
|
||||
enterprise: 'Enterprise',
|
||||
troubleshooting: 'Troubleshooting',
|
||||
quickstart: 'Quickstart',
|
||||
};
|
||||
|
||||
// Get all docs from the content collection (static .mdoc/.mdx files)
|
||||
const docs = await getCollection('docs');
|
||||
|
||||
for (const doc of docs) {
|
||||
const slug = doc.id;
|
||||
const title = doc.data.title || slug.split('/').pop() || slug;
|
||||
const section = slug.split('/')[0] || 'other';
|
||||
|
||||
// Try to get file path from doc.filePath, or try common extensions
|
||||
let filePath = doc.filePath;
|
||||
if (!filePath) {
|
||||
const extensions = ['.mdoc', '.mdx', '.md'];
|
||||
for (const ext of extensions) {
|
||||
const testPath = join(contentDocsDir, doc.id + ext);
|
||||
if (existsSync(testPath)) {
|
||||
filePath = testPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if no file path found or not readable
|
||||
if (!filePath || !existsSync(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const rawContent = readFileSync(filePath, 'utf-8');
|
||||
const content = stripFrontmatter(rawContent);
|
||||
entries.push({ slug, title, content, section });
|
||||
} catch {
|
||||
// Skip files that can't be read
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get plugin docs (executors, generators for each plugin)
|
||||
try {
|
||||
const pluginDocs = await getCollection('plugin-docs');
|
||||
for (const doc of pluginDocs) {
|
||||
if (doc.data.slug && doc.body) {
|
||||
const slug = doc.data.slug;
|
||||
const section = slug.split('/')[0] || 'technologies';
|
||||
entries.push({
|
||||
slug,
|
||||
title: doc.data.title || slug,
|
||||
content: stripFrontmatter(doc.body),
|
||||
section,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// plugin-docs collection might not exist
|
||||
}
|
||||
|
||||
// Note: Excluding nx-reference-packages (devkit API docs) for now
|
||||
// to keep the file size under 2MB. These are highly technical
|
||||
// and can be accessed individually via .md endpoints.
|
||||
|
||||
// Sort entries by section order, then by slug within section
|
||||
entries.sort((a, b) => {
|
||||
const sectionIndexA = sectionOrder.indexOf(a.section);
|
||||
const sectionIndexB = sectionOrder.indexOf(b.section);
|
||||
const orderA = sectionIndexA === -1 ? 999 : sectionIndexA;
|
||||
const orderB = sectionIndexB === -1 ? 999 : sectionIndexB;
|
||||
|
||||
if (orderA !== orderB) {
|
||||
return orderA - orderB;
|
||||
}
|
||||
return a.slug.localeCompare(b.slug);
|
||||
});
|
||||
|
||||
// Generate the llms-full.txt content
|
||||
const lines: string[] = [
|
||||
'# Nx Documentation',
|
||||
'',
|
||||
'> Complete Nx documentation compiled into a single file for LLM consumption.',
|
||||
'',
|
||||
'Nx is a powerful, open-source, technology-agnostic monorepo platform designed to efficiently manage codebases of any scale. From small workspaces to large enterprise monorepos, Nx provides intelligent task execution, caching, and CI optimization.',
|
||||
'',
|
||||
`This file was generated from ${entries.length} documentation pages.`,
|
||||
`Individual pages are available at: ${siteUrl}/docs/{slug}.md`,
|
||||
'',
|
||||
];
|
||||
|
||||
// Track current section for headers
|
||||
let currentSection = '';
|
||||
|
||||
for (const entry of entries) {
|
||||
// Add section header when section changes
|
||||
if (entry.section !== currentSection) {
|
||||
currentSection = entry.section;
|
||||
const sectionName = sectionNames[currentSection] || currentSection;
|
||||
lines.push('');
|
||||
lines.push(`# ${sectionName}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// URL encode spaces in the slug
|
||||
const encodedSlug = entry.slug.replace(/ /g, '%20');
|
||||
const sourceUrl = `${siteUrl}/docs/${encodedSlug}.md`;
|
||||
|
||||
lines.push('---');
|
||||
lines.push(`<!-- source: ${sourceUrl} -->`);
|
||||
lines.push(`## ${entry.title}`);
|
||||
lines.push('');
|
||||
lines.push(entry.content.trim());
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const content = lines.join('\n');
|
||||
|
||||
return new Response(content, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -79,9 +79,9 @@ export const GET: APIRoute = async ({ site }) => {
|
||||
const lines: string[] = [
|
||||
'# Nx',
|
||||
'',
|
||||
'> Nx is an AI-first build platform that connects your editor to CI. It helps you deliver fast without breaking things by optimizing builds, scaling CI, and fixing failed PRs.',
|
||||
'> Nx is an AI-first monorepo platform that connects your editor to CI. It helps you deliver fast without breaking things by optimizing builds, scaling CI, and fixing failed PRs.',
|
||||
'',
|
||||
'Nx is a powerful, open source, technology-agnostic build platform designed to efficiently manage codebases of any scale. From small single projects to large enterprise monorepos, Nx provides intelligent task execution, caching, and CI optimization.',
|
||||
'Nx is a powerful, open-source, technology-agnostic monorepo platform designed to efficiently manage codebases of any scale. From small workspaces to large enterprise monorepos, Nx provides intelligent task execution, caching, and CI optimization.',
|
||||
'',
|
||||
'Note: All documentation pages are available as raw Markdown by appending `.md` to the URL.',
|
||||
`For example: ${siteUrl}/docs/getting-started/intro.md returns the raw Markdown content.`,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/** SVG path data for the chain-link icon used in anchor links (matches Starlight heading links). */
|
||||
export const LINK_ICON_PATH =
|
||||
'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z';
|
||||
|
||||
export const TABLE_HEADERS_TO_MATCH = [
|
||||
'option',
|
||||
'options',
|
||||
'properties',
|
||||
'property',
|
||||
];
|
||||
/**
|
||||
* Converts an option name to an anchor slug.
|
||||
*
|
||||
* Strips leading `--` or `-` prefixes, takes only the first (canonical) name
|
||||
* if aliases are present (comma-separated), lowercases, and replaces
|
||||
* non-alphanumeric characters with `-`.
|
||||
*
|
||||
* Examples:
|
||||
* `--distribute-on` → `distribute-on`
|
||||
* `nxCloudUrl` → `nxcloudurl`
|
||||
* `--output, -o` → `output`
|
||||
*/
|
||||
export function optionSlug(raw: string): string {
|
||||
// Take only the first name if comma-separated aliases exist
|
||||
let name = raw.split(',')[0].trim();
|
||||
// Strip leading dashes
|
||||
name = name.replace(/^-{1,2}/, '');
|
||||
// Lowercase and replace non-alphanumeric with hyphens
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Rehype plugin that adds anchor links to option names in documentation tables.
|
||||
*
|
||||
* Targets tables whose first header cell contains "Option". For each data row,
|
||||
* it extracts the option name from the first cell's `<code>` element, generates
|
||||
* an anchor slug, sets an `id` on the `<tr>`, and wraps the `<code>` in an
|
||||
* `<a>` link with the Starlight-style anchor icon.
|
||||
*/
|
||||
|
||||
import type { Element, ElementContent, Root, Text } from 'hast';
|
||||
import {
|
||||
LINK_ICON_PATH,
|
||||
optionSlug,
|
||||
TABLE_HEADERS_TO_MATCH,
|
||||
} from './option-slug';
|
||||
|
||||
function getTextContent(node: ElementContent | Root): string {
|
||||
if (node.type === 'text') return (node as Text).value;
|
||||
if ('children' in node) {
|
||||
return (node.children as ElementContent[]).map(getTextContent).join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function isElement(node: ElementContent, tag?: string): node is Element {
|
||||
return node.type === 'element' && (!tag || node.tagName === tag);
|
||||
}
|
||||
|
||||
function findElement(
|
||||
children: ElementContent[],
|
||||
tag: string
|
||||
): Element | undefined {
|
||||
for (const child of children) {
|
||||
if (isElement(child)) {
|
||||
if (child.tagName === tag) return child;
|
||||
const found = findElement(child.children, tag);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findAllElements(children: ElementContent[], tag: string): Element[] {
|
||||
const results: Element[] = [];
|
||||
for (const child of children) {
|
||||
if (isElement(child, tag)) results.push(child);
|
||||
if (isElement(child)) {
|
||||
results.push(...findAllElements(child.children, tag));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function makeAnchorIcon(): Element {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: { ariaHidden: 'true', className: ['sl-anchor-icon'] },
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'svg',
|
||||
properties: {
|
||||
width: '16',
|
||||
height: '16',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentcolor',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'path',
|
||||
properties: { d: LINK_ICON_PATH },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function isOptionsTable(table: Element): boolean {
|
||||
const thead = findElement(table.children as ElementContent[], 'thead');
|
||||
if (!thead) return false;
|
||||
const firstTh = findElement(thead.children as ElementContent[], 'th');
|
||||
if (!firstTh) return false;
|
||||
const text = getTextContent(firstTh).trim();
|
||||
return TABLE_HEADERS_TO_MATCH.includes(text.toLowerCase());
|
||||
}
|
||||
|
||||
function processOptionsTable(table: Element): void {
|
||||
const tbody = findElement(table.children as ElementContent[], 'tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
const rows = findAllElements(tbody.children as ElementContent[], 'tr');
|
||||
|
||||
for (const row of rows) {
|
||||
const cells = row.children.filter((c) => isElement(c, 'td')) as Element[];
|
||||
if (cells.length === 0) continue;
|
||||
|
||||
const firstCell = cells[0];
|
||||
const codeEl = findElement(firstCell.children as ElementContent[], 'code');
|
||||
if (!codeEl) continue;
|
||||
|
||||
const optionText = getTextContent(codeEl).trim();
|
||||
if (!optionText) continue;
|
||||
|
||||
const slug = optionSlug(optionText);
|
||||
if (!slug) continue;
|
||||
|
||||
// Set id on the row for anchor targeting
|
||||
row.properties = row.properties || {};
|
||||
row.properties.id = slug;
|
||||
|
||||
// Find the index of the code element in the cell's children
|
||||
const codeIndex = firstCell.children.indexOf(codeEl);
|
||||
if (codeIndex === -1) continue;
|
||||
|
||||
// Wrap the <code> element in an <a> link
|
||||
const anchorLink: Element = {
|
||||
type: 'element',
|
||||
tagName: 'a',
|
||||
properties: {
|
||||
href: `#${slug}`,
|
||||
className: ['sl-option-link'],
|
||||
},
|
||||
children: [codeEl, makeAnchorIcon()],
|
||||
};
|
||||
|
||||
firstCell.children[codeIndex] = anchorLink;
|
||||
}
|
||||
}
|
||||
|
||||
export default function rehypeTableOptionLinks() {
|
||||
return (tree: Root) => {
|
||||
const tables = findAllElements(tree.children as ElementContent[], 'table');
|
||||
for (const table of tables) {
|
||||
if (isOptionsTable(table)) {
|
||||
processOptionsTable(table);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -408,6 +408,13 @@ h6:hover .anchor-link {
|
||||
border-inline-start: none;
|
||||
}
|
||||
|
||||
@media (min-width: 84rem) {
|
||||
.right-sidebar-panel {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.content-panel + .content-panel {
|
||||
border-top: 0;
|
||||
}
|
||||
@@ -424,3 +431,24 @@ table code {
|
||||
font-size: var(--text-sm);
|
||||
align-items: end; /* Fix tab alignment */
|
||||
}
|
||||
|
||||
/* Table option anchor links */
|
||||
.sl-markdown-content td .sl-option-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.sl-markdown-content td .sl-option-link .sl-anchor-icon {
|
||||
opacity: 0;
|
||||
margin-inline-start: 0.25em;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.sl-markdown-content td .sl-option-link .sl-anchor-icon > svg {
|
||||
width: 0.75em;
|
||||
height: 0.75em;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.sl-markdown-content tr:hover .sl-option-link .sl-anchor-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import Markdoc from '@markdoc/markdoc';
|
||||
import {
|
||||
LINK_ICON_PATH,
|
||||
optionSlug,
|
||||
TABLE_HEADERS_TO_MATCH,
|
||||
} from '../plugins/utils/option-slug';
|
||||
|
||||
type MarkdocTag = InstanceType<typeof Markdoc.Tag>;
|
||||
|
||||
/**
|
||||
* Check if a value is a Markdoc Tag via `$$mdtype`
|
||||
* instead of `instanceof` to avoid the issue where the
|
||||
* `@markdoc/markdoc` instance loaded by this file differs from the one
|
||||
* loaded by `@astrojs/markdoc` at runtime.
|
||||
* otherwise the markdoc transform always noops for zero matches
|
||||
*/
|
||||
function isTag(value: unknown): value is MarkdocTag {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
(value as Record<string, unknown>).$$mdtype === 'Tag'
|
||||
);
|
||||
}
|
||||
|
||||
function getTagText(tag: unknown): string {
|
||||
if (typeof tag === 'string') return tag;
|
||||
if (isTag(tag)) {
|
||||
return tag.children.map(getTagText).join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function findChildTag(
|
||||
parent: MarkdocTag,
|
||||
name: string
|
||||
): MarkdocTag | undefined {
|
||||
return parent.children.find(
|
||||
(c): c is MarkdocTag => isTag(c) && c.name === name
|
||||
);
|
||||
}
|
||||
|
||||
function makeAnchorIcon(): MarkdocTag {
|
||||
return new Markdoc.Tag(
|
||||
'span',
|
||||
{ 'aria-hidden': 'true', class: 'sl-anchor-icon' },
|
||||
[
|
||||
new Markdoc.Tag(
|
||||
'svg',
|
||||
{
|
||||
width: '16',
|
||||
height: '16',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentcolor',
|
||||
},
|
||||
[new Markdoc.Tag('path', { d: LINK_ICON_PATH }, [])]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a Markdoc table node, adding anchor links to option rows.
|
||||
* Non-option/property list tables pass through unmodified.
|
||||
*/
|
||||
export function transformOptionsTable(
|
||||
node: Parameters<
|
||||
NonNullable<import('@markdoc/markdoc').Schema['transform']>
|
||||
>[0],
|
||||
config: Parameters<
|
||||
NonNullable<import('@markdoc/markdoc').Schema['transform']>
|
||||
>[1]
|
||||
): MarkdocTag {
|
||||
const children = node.transformChildren(config);
|
||||
const table = new Markdoc.Tag(
|
||||
'table',
|
||||
node.transformAttributes(config),
|
||||
children
|
||||
);
|
||||
|
||||
const thead = findChildTag(table, 'thead');
|
||||
const tbody = findChildTag(table, 'tbody');
|
||||
if (!thead || !tbody) return table;
|
||||
|
||||
const headerRow = findChildTag(thead, 'tr');
|
||||
const firstTh = headerRow ? findChildTag(headerRow, 'th') : undefined;
|
||||
const headerText = firstTh ? getTagText(firstTh).trim() : '';
|
||||
|
||||
// make sure the links only show up for tables that 'options'
|
||||
if (!TABLE_HEADERS_TO_MATCH.includes(headerText.toLowerCase())) return table;
|
||||
|
||||
for (const row of tbody.children) {
|
||||
if (!isTag(row) || row.name !== 'tr') continue;
|
||||
|
||||
const firstTd = findChildTag(row, 'td');
|
||||
if (!firstTd) continue;
|
||||
|
||||
const codeTag = findChildTag(firstTd, 'code');
|
||||
if (!codeTag) continue;
|
||||
|
||||
const slug = optionSlug(getTagText(codeTag).trim());
|
||||
if (!slug) continue;
|
||||
|
||||
row.attributes.id = slug;
|
||||
|
||||
const codeIndex = firstTd.children.indexOf(codeTag);
|
||||
firstTd.children[codeIndex] = new Markdoc.Tag(
|
||||
'a',
|
||||
{ href: `#${slug}`, class: 'sl-option-link' },
|
||||
[codeTag, makeAnchorIcon()]
|
||||
);
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
@@ -437,7 +437,7 @@ From my experience, I've often seen teams start with a single application, which
|
||||
|
||||
## Q: Isn't Nx just for Angular projects?
|
||||
|
||||
This is a common but understandable misconception. Although Nx was heavily inspired by the Angular CLI initially, it is now a completely independent build system and CLI with first-class support for Angular, React, Node, Next.js, TypeScript and more. And with tons of [community plugins](/community) that extend Nx beyond that.
|
||||
This is a common but understandable misconception. Although Nx was heavily inspired by the Angular CLI initially, it is now a completely independent monorepo platform and CLI with first-class support for Angular, React, Node, Next.js, TypeScript and more. And with tons of [community plugins](/community) that extend Nx beyond that.
|
||||
|
||||
## Conclusion
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ If you want to check out the final result, here's the corresponding Github repo:
|
||||
|
||||
But before we jump right into the topic, what is Nx? And more specifically, what are Nx Plugins?
|
||||
|
||||
Nx is an open-source build system that provides tools and techniques to enhance developer productivity. [Check out this 10 min video overview](https://youtu.be/-_4WMl-Fn0w) of Nx if you want to learn more.
|
||||
Nx is an open-source monorepo platform that provides tools and techniques to enhance developer productivity. [Check out this 10 min video overview](https://youtu.be/-_4WMl-Fn0w) of Nx if you want to learn more.
|
||||
|
||||
Our example, in particular, uses Nx as a dev tool for creating a CLI and plugin. Nx plugins are npm packages that provide integrations between Nx and other technologies. You can use Nx without them, but they can provide great value if applied properly. `my-own-react` is the plugin to integrate React and Nx.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ It's been a bit since we launched [Nx 17](/blog/nx-17-release)! In this article,
|
||||
- [Module Federation Updates](#module-federation-updates)
|
||||
- [Nx Release Updates](#nx-release-updates)
|
||||
- [Angular 17 (AND NgRx 17) Support](#angular-17-and-ngrx-17-support)
|
||||
- [Smart Repos — Fast Builds](#smart-repos-fast-builds)
|
||||
- [Smart Monorepos — Fast Builds](#smart-repos-fast-builds)
|
||||
- [New Canary Releases](#new-canary-releases)
|
||||
- [Upcoming Release Livestream](#upcoming-release-livestream)
|
||||
- [Automatically Update Nx](#automatically-update-nx)
|
||||
@@ -139,7 +139,7 @@ nx migrate latest --interactive
|
||||
- Run 'nx migrate --run-migrations'
|
||||
```
|
||||
|
||||
## Smart Repos — Fast Builds
|
||||
## Smart Monorepos — Fast Builds
|
||||
|
||||
We just gave our Nx homepage a small facelift, including a new tagline, subtagline and illustration to better reflect Nx's mission statement.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ It is that time again: getting flooded by Year of Review blog posts. We did it l
|
||||
- [Many OSS repos adopt Nx](#many-oss-repos-adopt-nx)
|
||||
- [Nx Community](#nx-community)
|
||||
- [New Content & Improved Docs](#new-content-improved-docs)
|
||||
- [New Tagline: Smart Repos — Fast Builds](#new-tagline-smart-repos-fast-builds)
|
||||
- [New Tagline: Smart Monorepos — Fast Builds](#new-tagline-smart-repos-fast-builds)
|
||||
- [Nx Conf](#nx-conf)
|
||||
- [Looking ahead — 2024](#looking-ahead-2024)
|
||||
- [Solving CI](#solving-ci)
|
||||
@@ -330,11 +330,11 @@ You can also browse them in the [nx-recipes](https://github.com/nrwl/nx-recipes)
|
||||
|
||||
And obviously, we jumped on the AI train as well. A couple of months ago, we added the [Nx Assistant](/ai-chat). A ChatGPT-powered interface trained in our docs. [Katerina](https://twitter.com/psybercity) wrote about it [on our blog](/blog/nx-docs-ai-assistant). The AI chat allows to interactively ask questions about Nx and will give you relevant answers from our docs (including linking to the sources).
|
||||
|
||||
## New Tagline: Smart Repos — Fast Builds
|
||||
## New Tagline: Smart Monorepos — Fast Builds
|
||||
|
||||
Nx stands out for its flexibility, accommodating for both monorepo and non-monorepo project structures. This approach allows users to begin with simpler project configurations, leveraging the benefits of Nx's robust tooling, and later, when the need arises, seamlessly [migrate to a monorepo](/docs/guides/tips-n-tricks/standalone-to-monorepo).
|
||||
|
||||
However, Nx's true strength becomes most apparent at scale, typically within a monorepo setup. We wanted to capture it in our new tagline: **Smart Repos — Fast Builds**.
|
||||
However, Nx's true strength becomes most apparent at scale, typically within a monorepo setup. We wanted to capture it in our new tagline: **Smart Monorepos — Fast Builds**.
|
||||
|
||||
{% tweet url="https://twitter.com/juristr/status/1734558895547568634" /%}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ In 2014, the state of the art for running tests and builds in your repository we
|
||||
|
||||
Nx was created in 2017 to address this problem. Nx is a build system that operates on a **higher level** where developers define the relationships between tasks and then Nx to decides the optimal way to run those tasks. In the same way, developers can define the inputs and outputs of tasks, then Nx automatically caches those task results. Developers tell Nx what a task does and then Nx can decide how best to run that task.
|
||||
|
||||
With [Nx Agents](/docs/features/ci-features/distribute-task-execution), Nx is applying this same mindset to the problem of slow and costly CI pipelines. Nx gives you both **Smart Repos** and **Fast Builds**.
|
||||
With [Nx Agents](/docs/features/ci-features/distribute-task-execution), Nx is applying this same mindset to the problem of slow and costly CI pipelines. Nx gives you both **Smart Monorepos** and **Fast Builds**.
|
||||
|
||||
## Why is CI So Hard?
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ This blog will show you:
|
||||
|
||||
Before we start, let's answer this question: what is Nx and why should we use it?
|
||||
|
||||
From [nx.dev](): "Nx is a build system with built-in tooling and advanced CI capabilities. It helps you maintain and scale monorepos, both locally and on CI." It sounds good, what benefits does it bring?
|
||||
From [nx.dev](): "Nx is a monorepo platform with built-in tooling and advanced CI capabilities. It helps you maintain and scale monorepos, both locally and on CI." It sounds good, what benefits does it bring?
|
||||
|
||||
Nx adds the following features to your workspace:
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: 'End to End Autonomous AI Agent Workflows with Nx'
|
||||
slug: autonomous-ai-workflows-with-nx
|
||||
authors: ['Juri Strumpflohner']
|
||||
tags: ['nx', 'self-healing']
|
||||
cover_image: /blog/images/articles/local-to-ci-autonomous-agents.avif
|
||||
youtubeUrl: https://youtu.be/LaZroK4b-zQ
|
||||
description: 'Learn how Nx bridges the gap between local AI agents and CI, enabling fully autonomous development workflows with the ci-monitor skill and Self-Healing CI.'
|
||||
---
|
||||
|
||||
If 2025 was the year of agents, **2026 is the year of autonomous workflows**. We started with AI Chats (copy and pasting), moved to agentic tools like Claude Code and Cursor (edit files, run commands, but still babysitting), and are now heading toward agents that perform large chunks of work completely autonomously.
|
||||
|
||||
AI agents no longer just autocomplete a line or a function body. They can operate independently across projects. This shift will change how we work. Organizations that adapt quickly, and have the right tooling in place, will have a significant advantage.
|
||||
|
||||
{% toc /%}
|
||||
|
||||
## CI Breaks Autonomy - How Nx fixes it!
|
||||
|
||||
We wrote about [autonomous AI Agents at scale](/blog/ai-agents-and-continuity) and the potential barriers organizations might hit when adopting them. One such aspect where many current setups hit a wall is CI.
|
||||
|
||||
The local agent implements everything, pushes to CI, and waits. CI fails? You get pulled back in. Context is lost. This disconnect between local development and CI kills full autonomy.
|
||||
|
||||
Nx is modular and can be adopted incrementally, but each piece fits seamlessly together. The `ci-monitor` skill is one such piece.
|
||||
|
||||
{% callout type="info" title="What's a skill?" %}
|
||||
Skills are portable, shareable agent capabilities that extend what your AI coding agent can do. They work across different AI agents and can be shared via [agentskills.io](https://agentskills.io/home).
|
||||
{% /callout %}
|
||||
|
||||
When you have your Nx workspace connected to Nx Cloud, the CI Monitor skill opens a communication channel between your local agent and the Nx Cloud CI run via the Nx MCP server. Your local agent can now:
|
||||
|
||||
- Monitor CI pipeline status in real-time
|
||||
- Receive failure information with full context
|
||||
- Communicate with Nx's Self-Healing CI agent
|
||||
- Apply verified fixes automatically
|
||||
- Keep iterating until CI is green
|
||||
|
||||
The disconnect is bridged.
|
||||
|
||||

|
||||
|
||||
## Let The Agent Handle the Annoying Part
|
||||
|
||||
Fully autonomous AI agents have gained popularity recently, with "Ralph Wiggum loops" being one implementation pattern: autonomous cycles that keep iterating until a task is complete.
|
||||
|
||||
{% callout type="info" title="What's a Ralph loop?" %}
|
||||
Ralph Wiggum loops are autonomous agent workflows where the agent keeps working on a task until completion, without human intervention. The pattern was popularized by [Geoffrey Huntley](https://ghuntley.com/ralph/) and has become a common approach for running AI agents on well-defined tasks.
|
||||
{% /callout %}
|
||||
|
||||
In the [video demo](https://youtu.be/LaZroK4b-zQ), a Ralph loop picks up a well-defined user story from a PRD and implements it autonomously:
|
||||
|
||||
1. The local agent reads requirements and implements the feature
|
||||
2. Runs local quality checks: type checking, linting, testing
|
||||
3. Creates a PR and starts monitoring CI
|
||||
|
||||
When CI fails, the Nx Self-Healing CI agent kicks in. It classifies the failure, proposes a verified fix. The local agent sees this through the ci-monitor, applies the fix, pushes again. Another failure (end-to-end test). Same process. Back and forth until CI is green.
|
||||
|
||||
Then you get notified and you review the PR. No interruptions during all this CI back and forth.
|
||||
|
||||
> The competitive advantage goes to organizations ready to adopt these workflows.
|
||||
|
||||
**But you don't need to go all-in on full autonomy necessarily.** You might prefer interactive collaboration. You pair with the agent on implementation, make decisions together, iterate on the approach. When you're 90% done, you hand off. The agent creates the PR, monitors CI, applies fixes, and notifies you when CI is green and is ready for review.
|
||||
|
||||
## How to Get Started
|
||||
|
||||
Setting up CI monitoring requires two steps:
|
||||
|
||||
### 1. Configure AI Agent Support
|
||||
|
||||
```shell
|
||||
nx configure-ai-agents
|
||||
```
|
||||
|
||||
This sets up your workspace with the MCP configuration and skills that enable agent-CI communication.
|
||||
|
||||
### 2. Use the CI Monitor
|
||||
|
||||
Leverage the `ci-monitor` skill by asking your AI agent:
|
||||
|
||||
```text
|
||||
Commit the work, create a PR and monitor CI.
|
||||
```
|
||||
|
||||
> **Prerequisite:** Your Nx workspace needs to be connected to Nx Cloud and you should have Self-Healing CI enabled. [More about that in the docs](/docs/features/ci-features/self-healing-ci).
|
||||
|
||||
The skill connects to Nx Cloud, watches pipeline progress, and feeds failure information back to your agent. If Self-Healing CI proposes fixes, those become available for your agent to review and apply.
|
||||
|
||||
Want to try Ralph loops yourself? Check out this [example repo](https://github.com/juristr/tusky/tree/30ad2ae3f99c595b6f307162e8a92b8ccc6f92fa/ralph) for a working setup you can reference.
|
||||
|
||||
## Looking Ahead
|
||||
|
||||
Software development is changing rapidly. AI coding agents are becoming part of everyday workflows, and the quality you get out of them vastly differs based on your setup.
|
||||
|
||||
The infrastructure matters. How your codebase is organized. The type and quality of context the AI agent has access to. Whether guardrails and feedback loops are in place. Whether your CI platform integrates deeply enough to close the autonomy gap.
|
||||
|
||||
These factors determine whether your AI agent can become a productivity multiplier.
|
||||
|
||||
Nx enables these autonomous flows. This is just the beginning.
|
||||
|
||||
---
|
||||
|
||||
Learn more:
|
||||
|
||||
- [Autonomous AI Agents at Scale](/blog/ai-agents-and-continuity): Infrastructure requirements for AI agent workflows
|
||||
- [Self-Healing CI Documentation](/docs/features/ci-features/self-healing-ci): How Nx Cloud's Self-Healing CI works
|
||||
- [Ralph Wiggum (original concept)](https://ghuntley.com/ralph/): Community origins of the Ralph loop pattern
|
||||
- 🧠 [Nx Docs](/docs/getting-started/intro)
|
||||
- 👩💻 [Nx GitHub](https://github.com/nrwl/nx)
|
||||
- 💬 [Nx Official Discord Server](https://go.nx.dev/community)
|
||||
- 📹 [Nx Youtube Channel](https://www.youtube.com/@nxdevtools)
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 544 KiB |
@@ -4,7 +4,7 @@ videoUrl: 'https://youtu.be/8mqHXYIl_qI'
|
||||
duration: '4:00'
|
||||
---
|
||||
|
||||
Nx powers the “Smart Repos,” while Nx Cloud brings “Fast Builds” into the mix. Designed to extend Nx’s efficiency into the CI pipeline, Nx Cloud ensures that even large monorepos stay fast and optimized in CI.
|
||||
Nx powers the “Smart Monorepos,” while Nx Cloud brings “Fast Builds” into the mix. Designed to extend Nx’s efficiency into the CI pipeline, Nx Cloud ensures that even large monorepos stay fast and optimized in CI.
|
||||
|
||||
In this lesson, we’ll take the Tasker monorepo, push it to GitHub, set up an Nx Cloud workspace, and link it with your GitHub repository. By the end, your Nx workspace will be fully connected to Nx Cloud, ready to leverage its remote caching and distributed CI capabilities.
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
{
|
||||
"name": "create-nx-workspace",
|
||||
"packageName": "create-nx-workspace",
|
||||
"description": "Smart Repos · Fast Builds",
|
||||
"description": "Smart Monorepos · Fast Builds",
|
||||
"path": "generated/packages/create-nx-workspace.json",
|
||||
"schemas": {
|
||||
"executors": [],
|
||||
|
||||
@@ -86,8 +86,10 @@ describe('Angular Module Federation - Federated Libraries', () => {
|
||||
expect(buildRemoteOutput).toContain('Successfully ran target build');
|
||||
|
||||
if (runE2ETests('cypress')) {
|
||||
const e2eProcess = await runCommandUntil(`e2e ${host}-e2e`, (output) =>
|
||||
output.includes('All specs passed!')
|
||||
const e2eProcess = await runCommandUntil(
|
||||
`e2e ${host}-e2e`,
|
||||
(output) => output.includes('All specs passed!'),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await killProcessAndPorts(e2eProcess.pid, hostPort, hostPort + 1);
|
||||
}
|
||||
@@ -175,8 +177,10 @@ describe('Angular Module Federation - Federated Libraries', () => {
|
||||
expect(buildRemoteOutput).toContain('Successfully ran target build');
|
||||
|
||||
if (runE2ETests('cypress')) {
|
||||
const e2eProcess = await runCommandUntil(`e2e ${host}-e2e`, (output) =>
|
||||
output.includes('All specs passed!')
|
||||
const e2eProcess = await runCommandUntil(
|
||||
`e2e ${host}-e2e`,
|
||||
(output) => output.includes('All specs passed!'),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await killProcessAndPorts(e2eProcess.pid, hostPort, hostPort + 1);
|
||||
}
|
||||
|
||||
@@ -88,8 +88,13 @@ test('renders remotes', async ({ page }) => {
|
||||
});`
|
||||
);
|
||||
if (runE2ETests()) {
|
||||
const e2eProcess = await runCommandUntil(`e2e ${host}-e2e`, (output) =>
|
||||
output.includes(`Successfully ran target e2e for project ${host}-e2e`)
|
||||
const e2eProcess = await runCommandUntil(
|
||||
`e2e ${host}-e2e`,
|
||||
(output) =>
|
||||
output.includes(
|
||||
`Successfully ran target e2e for project ${host}-e2e`
|
||||
),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await killProcessAndPorts(e2eProcess.pid);
|
||||
}
|
||||
|
||||
@@ -151,7 +151,8 @@ describe('Node.js Framework ESM Support', () => {
|
||||
`serve ${expressApp}`,
|
||||
(output) => {
|
||||
return output.includes('Express ESM serve ready on port');
|
||||
}
|
||||
},
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
await promisifiedTreeKill(serveProcess.pid, 'SIGKILL');
|
||||
@@ -269,7 +270,8 @@ describe('Node.js Framework ESM Support', () => {
|
||||
`serve ${fastifyApp}`,
|
||||
(output) => {
|
||||
return output.includes('Fastify ESM serve ready on port');
|
||||
}
|
||||
},
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
await promisifiedTreeKill(serveProcess.pid, 'SIGKILL');
|
||||
@@ -386,7 +388,8 @@ describe('Node.js Framework ESM Support', () => {
|
||||
`serve ${koaApp}`,
|
||||
(output) => {
|
||||
return output.includes('Koa ESM serve ready on port');
|
||||
}
|
||||
},
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
await promisifiedTreeKill(serveProcess.pid, 'SIGKILL');
|
||||
@@ -444,7 +447,8 @@ describe('Node.js Framework ESM Support', () => {
|
||||
`serve ${nestApp}`,
|
||||
(output) => {
|
||||
return output.includes('Nest ESM server ready on port');
|
||||
}
|
||||
},
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await promisifiedTreeKill(serveProcess.pid, 'SIGKILL');
|
||||
}, 600000);
|
||||
@@ -496,7 +500,8 @@ describe('Node.js Framework ESM Support', () => {
|
||||
`serve ${nestApp}`,
|
||||
(output) => {
|
||||
return output.includes('Nest ESM serve ready on port');
|
||||
}
|
||||
},
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
await promisifiedTreeKill(serveProcess.pid, 'SIGKILL');
|
||||
|
||||
@@ -209,6 +209,7 @@ module.exports = {
|
||||
return output.includes(`foobar: test foo bar`);
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
@@ -271,8 +272,8 @@ module.exports = {
|
||||
const p = await runCommandUntil(
|
||||
`serve ${nodeapp}`,
|
||||
(output) => output.includes(`Listening at http://localhost:${port}`),
|
||||
|
||||
{
|
||||
timeout: 120000,
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
@@ -328,6 +329,7 @@ module.exports = {
|
||||
return output.includes(`listening on ws://localhost:${port}`);
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
@@ -395,6 +397,7 @@ module.exports = {
|
||||
return output.includes('Hello World');
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
env: {
|
||||
NX_DAEMON: 'true',
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ xdescribe('--help output', () => {
|
||||
expect(output).toMatch(/--coverage|--watch|--bail/i);
|
||||
|
||||
// Should NOT contain Nx's help
|
||||
expect(output).not.toContain('Smart Repos');
|
||||
expect(output).not.toContain('Smart Monorepos');
|
||||
expect(output).not.toContain('Run target');
|
||||
expect(output).not.toContain('Run a target for a project');
|
||||
});
|
||||
@@ -44,7 +44,7 @@ xdescribe('--help output', () => {
|
||||
expect(output).toMatch(/--coverage|--watch|--bail/i);
|
||||
|
||||
// Should NOT contain Nx's help
|
||||
expect(output).not.toContain('Smart Repos');
|
||||
expect(output).not.toContain('Smart Monorepos');
|
||||
expect(output).not.toContain('Run target');
|
||||
expect(output).not.toContain('Run a target for a project');
|
||||
});
|
||||
@@ -60,7 +60,7 @@ xdescribe('--help output', () => {
|
||||
expect(lintOutput).toMatch(/--fix|--format|--quiet/i);
|
||||
|
||||
// Should NOT contain Nx's help
|
||||
expect(lintOutput).not.toContain('Smart Repos');
|
||||
expect(lintOutput).not.toContain('Smart Monorepos');
|
||||
expect(lintOutput).not.toContain('Run target');
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ xdescribe('--help output', () => {
|
||||
expect(buildOutput).toMatch(/webpack|build.*production/i);
|
||||
|
||||
// Should NOT contain Nx's help
|
||||
expect(buildOutput).not.toContain('Smart Repos');
|
||||
expect(buildOutput).not.toContain('Smart Monorepos');
|
||||
expect(buildOutput).not.toContain('Run target');
|
||||
});
|
||||
});
|
||||
@@ -317,7 +317,7 @@ xdescribe('--help output', () => {
|
||||
|
||||
// Should still show Jest help even with additional flags
|
||||
expect(output).toContain('Usage: jest');
|
||||
expect(output).not.toContain('Smart Repos');
|
||||
expect(output).not.toContain('Smart Monorepos');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -440,7 +440,7 @@ describe('Nx Commands', () => {
|
||||
|
||||
it('should show help if no command provided', () => {
|
||||
const output = runCLI('', { silenceError: true });
|
||||
expect(output).toContain('Smart Repos · Fast Builds');
|
||||
expect(output).toContain('Smart Monorepos · Fast Builds');
|
||||
expect(output).toContain('Commands:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,7 +69,8 @@ describe('React Rspack Module Federation - Basic - Playwright', () => {
|
||||
if (runE2ETests()) {
|
||||
const e2eResultsSwc = await runCommandUntil(
|
||||
`e2e ${shell}-e2e`,
|
||||
(output) => output.includes('Successfully ran target e2e for project')
|
||||
(output) => output.includes('Successfully ran target e2e for project'),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell));
|
||||
|
||||
@@ -75,7 +75,8 @@ describe('React Module Federation - Webpack Basic - Playwright', () => {
|
||||
);
|
||||
const e2eResultsSwc = await runCommandUntil(
|
||||
`e2e ${shell}-e2e`,
|
||||
(output) => output.includes('Successfully ran target e2e for project')
|
||||
(output) => output.includes('Successfully ran target e2e for project'),
|
||||
{ timeout: 120_000 }
|
||||
);
|
||||
console.log(
|
||||
`[core-webpack-basic-playwright] e2e (swc) completed with PID ${e2eResultsSwc.pid}`
|
||||
@@ -96,6 +97,7 @@ describe('React Module Federation - Webpack Basic - Playwright', () => {
|
||||
`e2e ${shell}-e2e`,
|
||||
(output) => output.includes('Successfully ran target e2e for project'),
|
||||
{
|
||||
timeout: 120_000,
|
||||
env: { NX_PREFER_TS_NODE: 'true' },
|
||||
}
|
||||
);
|
||||
|
||||
@@ -63,8 +63,11 @@ describe('React Module Federation - Webpack SSR', () => {
|
||||
`generate @nx/react:host ${shell} --ssr --bundler=webpack --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --skipFormat`
|
||||
);
|
||||
|
||||
const serveResult = await runCommandUntil(`serve ${shell}`, (output) =>
|
||||
output.includes(`Nx SSR Static remotes proxies started successfully`)
|
||||
const serveResult = await runCommandUntil(
|
||||
`serve ${shell}`,
|
||||
(output) =>
|
||||
output.includes(`Nx SSR Static remotes proxies started successfully`),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
await killProcessAndPorts(serveResult.pid);
|
||||
@@ -107,7 +110,8 @@ describe('React Module Federation - Webpack SSR', () => {
|
||||
if (runE2ETests()) {
|
||||
const hostE2eResults = await runCommandUntil(
|
||||
`e2e ${shell}-e2e --no-watch --verbose`,
|
||||
(output) => output.includes('All specs passed!')
|
||||
(output) => output.includes('All specs passed!'),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
await killProcessAndPorts(hostE2eResults.pid);
|
||||
}
|
||||
|
||||
@@ -237,7 +237,11 @@ describe('nx release - independent projects', () => {
|
||||
const versionWithGitActionsCLIOutput = runCLI(
|
||||
`release version 999.9.9-version-git-operations-test.2 -p ${pkg1} --git-commit --git-tag --verbose` // add verbose so we get richer output
|
||||
);
|
||||
expect(versionWithGitActionsCLIOutput).toMatchInlineSnapshot(`
|
||||
const filteredOutput = versionWithGitActionsCLIOutput.replace(
|
||||
/\[plugin-(pool|worker)\].*\n/g,
|
||||
''
|
||||
);
|
||||
expect(filteredOutput).toMatchInlineSnapshot(`
|
||||
|
||||
NX Your filter "{project-name}" matched the following projects:
|
||||
|
||||
@@ -320,7 +324,11 @@ describe('nx release - independent projects', () => {
|
||||
const versionWithGitActionsConfigOutput = runCLI(
|
||||
`release version 999.9.9-version-git-operations-test.3 --verbose --gitTag` // add verbose so we get richer output
|
||||
);
|
||||
expect(versionWithGitActionsConfigOutput).toMatchInlineSnapshot(`
|
||||
const filteredConfigOutput = versionWithGitActionsConfigOutput.replace(
|
||||
/\[plugin-(pool|worker)\].*\n/g,
|
||||
''
|
||||
);
|
||||
expect(filteredConfigOutput).toMatchInlineSnapshot(`
|
||||
|
||||
NX Running release version for project: {project-name}
|
||||
|
||||
@@ -516,7 +524,11 @@ describe('nx release - independent projects', () => {
|
||||
const versionWithGitActionsCLIOutput = runCLI(
|
||||
`release changelog 999.9.9-changelog-git-operations-test.1 -p ${pkg1} --verbose`
|
||||
);
|
||||
expect(versionWithGitActionsCLIOutput).toMatchInlineSnapshot(`
|
||||
const filteredChangelogOutput = versionWithGitActionsCLIOutput.replace(
|
||||
/\[plugin-(pool|worker)\].*\n/g,
|
||||
''
|
||||
);
|
||||
expect(filteredChangelogOutput).toMatchInlineSnapshot(`
|
||||
|
||||
NX Your filter "{project-name}" matched the following projects:
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ expect.addSnapshotSerializer({
|
||||
.replaceAll('pnpm install --lockfile-only', '{lock-file-command}')
|
||||
.replaceAll(getSelectedPackageManager(), '{package-manager}')
|
||||
.replaceAll(e2eRegistryUrl, '{registryUrl}')
|
||||
// Filter out plugin worker verbose logs
|
||||
.replaceAll(/\[plugin-(pool|worker)\].*\n/g, '')
|
||||
// We trim each line to reduce the chances of snapshot flakiness
|
||||
.split('\n')
|
||||
.map((r) => r.trim())
|
||||
|
||||
@@ -47,6 +47,8 @@ expect.addSnapshotSerializer({
|
||||
/Integrity:\s*.*/g,
|
||||
'Integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
|
||||
)
|
||||
// Filter out plugin worker verbose logs
|
||||
.replaceAll(/\[plugin-(pool|worker)\].*\n/g, '')
|
||||
|
||||
.split('\n')
|
||||
.map((r) => r.trim())
|
||||
|
||||
@@ -32,6 +32,8 @@ expect.addSnapshotSerializer({
|
||||
.replaceAll(/Test @[\w\d]+/g, 'Test @{COMMIT_AUTHOR}')
|
||||
// Normalize the version title date.
|
||||
.replaceAll(/\(\d{4}-\d{2}-\d{2}\)/g, '(YYYY-MM-DD)')
|
||||
// Filter out plugin worker verbose logs
|
||||
.replaceAll(/\[plugin-(pool|worker)\].*\n/g, '')
|
||||
// We trim each line to reduce the chances of snapshot flakiness
|
||||
.split('\n')
|
||||
.map((r) => r.trim())
|
||||
|
||||
@@ -29,6 +29,8 @@ expect.addSnapshotSerializer({
|
||||
.replaceAll(/Test @[\w\d]+/g, 'Test @{COMMIT_AUTHOR}')
|
||||
// Normalize the version title date.
|
||||
.replaceAll(/\(\d{4}-\d{2}-\d{2}\)/g, '(YYYY-MM-DD)')
|
||||
// Filter out plugin worker verbose logs
|
||||
.replaceAll(/\[plugin-(pool|worker)\].*\n/g, '')
|
||||
// We trim each line to reduce the chances of snapshot flakiness
|
||||
.split('\n')
|
||||
.map((r) => r.trim())
|
||||
|
||||
@@ -279,11 +279,10 @@ export function runCommandAsync(
|
||||
export function runCommandUntil(
|
||||
command: string,
|
||||
criteria: (output: string) => boolean,
|
||||
opts: RunCmdOpts = {
|
||||
env: undefined,
|
||||
}
|
||||
opts: RunCmdOpts & { timeout?: number } = {}
|
||||
): Promise<ChildProcess> {
|
||||
const pm = getPackageManagerCommand();
|
||||
const timeout = opts.timeout ?? 30_000;
|
||||
const p = exec(`${pm.runNx} ${command}`, {
|
||||
cwd: tmpProjPath(),
|
||||
encoding: 'utf-8',
|
||||
@@ -301,18 +300,37 @@ export function runCommandUntil(
|
||||
let output = '';
|
||||
let complete = false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!complete) {
|
||||
complete = true;
|
||||
p.kill();
|
||||
logError(
|
||||
`Output did not meet the criteria:`,
|
||||
output
|
||||
.split('\n')
|
||||
.map((l) => ` ${l}`)
|
||||
.join('\n')
|
||||
);
|
||||
rej(new Error(`Timed out after ${timeout}ms waiting for criteria`));
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
function checkCriteria(c) {
|
||||
output += c.toString();
|
||||
if (criteria(stripConsoleColors(output)) && !complete) {
|
||||
const strippedOutput = stripConsoleColors(output);
|
||||
if (criteria(strippedOutput) && !complete) {
|
||||
complete = true;
|
||||
clearTimeout(timeoutId);
|
||||
res(p);
|
||||
}
|
||||
}
|
||||
|
||||
p.stdout?.on('data', checkCriteria);
|
||||
p.stderr?.on('data', checkCriteria);
|
||||
p.on('exit', (code) => {
|
||||
p.on('close', (code) => {
|
||||
if (!complete) {
|
||||
complete = true;
|
||||
clearTimeout(timeoutId);
|
||||
logError(
|
||||
`Original output:`,
|
||||
output
|
||||
@@ -320,9 +338,7 @@ export function runCommandUntil(
|
||||
.map((l) => ` ${l}`)
|
||||
.join('\n')
|
||||
);
|
||||
rej(`Exited with ${code}`);
|
||||
} else {
|
||||
res(p);
|
||||
rej(new Error(`Exited with ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,12 +36,9 @@ export default async function (globalConfig: Config.ConfigGlobals) {
|
||||
}
|
||||
|
||||
process.env.npm_config_registry = registry;
|
||||
execSync(
|
||||
`npm config set //${listenAddress}:${port}/:_authToken "${authToken}" --ws=false`,
|
||||
{
|
||||
windowsHide: false,
|
||||
}
|
||||
);
|
||||
// Use environment variable instead of npm config command to avoid polluting other tests
|
||||
process.env[`npm_config_//${listenAddress}:${port}/:_authToken`] =
|
||||
authToken;
|
||||
|
||||
// bun
|
||||
process.env.BUN_CONFIG_REGISTRY = registry;
|
||||
@@ -55,12 +52,8 @@ export default async function (globalConfig: Config.ConfigGlobals) {
|
||||
process.env.NX_SKIP_PROVENANCE_CHECK = 'true';
|
||||
|
||||
global.e2eTeardown = () => {
|
||||
execSync(
|
||||
`npm config delete //${listenAddress}:${port}/:_authToken --ws=false`,
|
||||
{
|
||||
windowsHide: false,
|
||||
}
|
||||
);
|
||||
// Clean up environment variable instead of npm config command
|
||||
delete process.env[`npm_config_//${listenAddress}:${port}/:_authToken`];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -136,7 +136,7 @@ export function FeedContainer(): JSX.Element {
|
||||
<div
|
||||
className={cx(
|
||||
'left0 fixed bottom-0 right-0 w-full px-4 py-4 lg:px-0 lg:py-6',
|
||||
'bg-gradient-to-t from-white via-white/75 dark:from-slate-900 dark:via-slate-900/75'
|
||||
'bg-gradient-to-t from-white via-white/75 dark:from-zinc-900 dark:via-zinc-900/75'
|
||||
)}
|
||||
>
|
||||
<Prompt
|
||||
|
||||
@@ -51,7 +51,7 @@ export function FeedAnswer({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 w-12 items-center justify-center rounded-full bg-white text-slate-900 ring-1 ring-slate-200 dark:bg-slate-900 dark:text-white dark:ring-slate-700">
|
||||
<div className="grid h-12 w-12 items-center justify-center rounded-full bg-white text-zinc-900 ring-1 ring-zinc-200 dark:bg-zinc-900 dark:text-white dark:ring-zinc-700">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -65,23 +65,20 @@ export function FeedAnswer({
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-lg text-slate-900 dark:text-slate-100">
|
||||
<div className="flex items-center gap-2 text-lg text-zinc-900 dark:text-zinc-100">
|
||||
Nx Assistant
|
||||
</div>
|
||||
<p className="mt-0.5 flex items-center gap-x-1 text-sm text-slate-500">
|
||||
<ChatGptLogo
|
||||
className="h-4 w-4 text-slate-400"
|
||||
aria-hidden="true"
|
||||
/>{' '}
|
||||
<p className="mt-0.5 flex items-center gap-x-1 text-sm text-zinc-500">
|
||||
<ChatGptLogo className="h-4 w-4 text-zinc-400" aria-hidden="true" />{' '}
|
||||
AI powered
|
||||
</p>
|
||||
</div>
|
||||
<div className="prose prose-slate dark:prose-invert mt-2 w-full max-w-none 2xl:max-w-4xl">
|
||||
<div className="prose prose-zinc dark:prose-invert mt-2 w-full max-w-none 2xl:max-w-4xl">
|
||||
{!isFirst && callout}
|
||||
{renderMarkdown(normalizedContent, { filePath: '' }).node}
|
||||
</div>
|
||||
{!isFirst && (
|
||||
<div className="text-md group flex-1 gap-4 text-slate-400 transition hover:text-slate-500 md:flex md:items-center md:justify-end">
|
||||
<div className="text-md group flex-1 gap-4 text-zinc-400 transition hover:text-zinc-500 md:flex md:items-center md:justify-end">
|
||||
{feedbackStatement ? (
|
||||
<p className="italic group-hover:flex">
|
||||
{feedbackStatement === 'good'
|
||||
@@ -96,7 +93,7 @@ export function FeedAnswer({
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
className={cx(
|
||||
'p-1 transition-all hover:rotate-12 hover:text-blue-500 disabled:cursor-not-allowed dark:hover:text-sky-500',
|
||||
'p-1 transition-all hover:rotate-12 hover:text-blue-500 disabled:cursor-not-allowed dark:hover:text-blue-500',
|
||||
{ 'text-blue-500': feedbackStatement === 'bad' }
|
||||
)}
|
||||
disabled={!!feedbackStatement}
|
||||
@@ -108,7 +105,7 @@ export function FeedAnswer({
|
||||
</button>
|
||||
<button
|
||||
className={cx(
|
||||
'p-1 transition-all hover:rotate-12 hover:text-blue-500 disabled:cursor-not-allowed dark:hover:text-sky-500',
|
||||
'p-1 transition-all hover:rotate-12 hover:text-blue-500 disabled:cursor-not-allowed dark:hover:text-blue-500',
|
||||
{ 'text-blue-500': feedbackStatement === 'good' }
|
||||
)}
|
||||
disabled={!!feedbackStatement}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export function FeedQuestion({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="flex w-full justify-end">
|
||||
<p className="whitespace-pre-wrap break-words rounded-lg bg-blue-500 px-4 py-2 text-base text-white selection:bg-sky-900 dark:bg-sky-500">
|
||||
<p className="whitespace-pre-wrap break-words rounded-lg bg-blue-500 px-4 py-2 text-base text-white selection:bg-blue-900 dark:bg-blue-500">
|
||||
{content}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export function LoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center gap-4 px-4 py-2 text-blue-500 transition duration-150 ease-in-out dark:text-sky-500">
|
||||
<div className="flex w-full items-center justify-center gap-4 px-4 py-2 text-blue-500 transition duration-150 ease-in-out dark:text-blue-500">
|
||||
<svg
|
||||
className="h-5 w-5 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -62,7 +62,7 @@ export function Prompt({
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={handleSubmit}
|
||||
className="relative mx-auto flex max-w-3xl gap-2 rounded-md border border-slate-300 bg-white px-2 py-2 shadow-lg dark:border-slate-900 dark:bg-slate-700"
|
||||
className="relative mx-auto flex max-w-3xl gap-2 rounded-md border border-zinc-300 bg-white px-2 py-2 shadow-lg dark:border-zinc-900 dark:bg-zinc-700"
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
@@ -74,7 +74,7 @@ export function Prompt({
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className={cx('bg-white dark:bg-slate-900')}
|
||||
className={cx('bg-white dark:bg-zinc-900')}
|
||||
onClick={handleStopGenerating}
|
||||
>
|
||||
<StopIcon aria-hidden="true" className="h-5 w-5" />
|
||||
@@ -85,7 +85,7 @@ export function Prompt({
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className={cx('bg-white dark:bg-slate-900')}
|
||||
className={cx('bg-white dark:bg-zinc-900')}
|
||||
onClick={handleNewChat}
|
||||
>
|
||||
<XMarkIcon aria-hidden="true" className="h-5 w-5" />
|
||||
@@ -96,7 +96,7 @@ export function Prompt({
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className={cx('bg-white dark:bg-slate-900')}
|
||||
className={cx('bg-white dark:bg-zinc-900')}
|
||||
onClick={onRegenerate}
|
||||
>
|
||||
<ArrowPathIcon aria-hidden="true" className="h-5 w-5" />
|
||||
@@ -123,14 +123,14 @@ export function Prompt({
|
||||
name="query"
|
||||
maxLength={500}
|
||||
disabled={isGenerating}
|
||||
className="block w-full resize-none border-none bg-transparent p-0 py-3 pl-2 text-sm placeholder-slate-500 focus-within:outline-none focus:placeholder-slate-400 focus:outline-none focus:ring-0 disabled:cursor-not-allowed dark:text-white dark:focus:placeholder-slate-300"
|
||||
className="block w-full resize-none border-none bg-transparent p-0 py-3 pl-2 text-sm placeholder-zinc-500 focus-within:outline-none focus:placeholder-zinc-400 focus:outline-none focus:ring-0 disabled:cursor-not-allowed dark:text-white dark:focus:placeholder-zinc-300"
|
||||
placeholder="How does caching work?"
|
||||
rows={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="primary"
|
||||
variant="contrast"
|
||||
size="small"
|
||||
type="submit"
|
||||
disabled={isGenerating}
|
||||
|
||||
@@ -2,16 +2,16 @@ import { InformationCircleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export function ActivityLimitReached(): JSX.Element {
|
||||
return (
|
||||
<div className="rounded-md bg-slate-50 p-4 shadow-sm ring-1 ring-slate-100 dark:bg-slate-800/40 dark:ring-slate-700">
|
||||
<div className="rounded-md bg-zinc-50 p-4 shadow-sm ring-1 ring-zinc-100 dark:bg-zinc-800/40 dark:ring-zinc-700">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<InformationCircleIcon
|
||||
className="h-5 w-5 text-slate-500 dark:text-slate-300"
|
||||
className="h-5 w-5 text-zinc-500 dark:text-zinc-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm text-slate-700 dark:text-slate-400">
|
||||
<p className="text-sm text-zinc-700 dark:text-zinc-400">
|
||||
You've reached the maximum message history limit. Previous messages
|
||||
will be removed.
|
||||
</p>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
|
||||
export function SidebarContainer({ children }: { children: ReactNode[] }) {
|
||||
return (
|
||||
<div id="sidebar" data-testid="sidebar">
|
||||
<div className="hidden h-full w-72 flex-col border-r border-slate-200 md:flex dark:border-slate-700 dark:bg-slate-900">
|
||||
<div className="hidden h-full w-72 flex-col border-r border-zinc-200 md:flex dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="relative flex flex-col gap-4 overflow-y-scroll p-4">
|
||||
{...children}
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,11 @@ import { useEffect, useRef, useCallback } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { sendCustomEvent } from './google-analytics';
|
||||
|
||||
function getScrollDepth(pct: number): 0 | 25 | 50 | 75 | 90 {
|
||||
if (pct >= 0.9) return 90;
|
||||
if (pct < 0.25) return 0;
|
||||
if (pct < 0.5) return 25;
|
||||
if (pct < 0.75) return 50;
|
||||
return 75;
|
||||
}
|
||||
const SCROLL_THRESHOLDS = [10, 25, 50, 75, 90] as const;
|
||||
|
||||
export function useWindowScrollDepth(): void {
|
||||
const pathname = usePathname();
|
||||
const scrollDepth = useRef(0);
|
||||
const firedThresholds = useRef<Set<number>>(new Set());
|
||||
const shouldTrackScroll = useRef(true);
|
||||
const rafId = useRef<number | null>(null);
|
||||
|
||||
@@ -23,13 +17,19 @@ export function useWindowScrollDepth(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const scrollPercentage =
|
||||
(window.scrollY + window.innerHeight) /
|
||||
document.documentElement.scrollHeight;
|
||||
const depth = getScrollDepth(scrollPercentage);
|
||||
((window.scrollY + window.innerHeight) /
|
||||
document.documentElement.scrollHeight) *
|
||||
100;
|
||||
|
||||
if (depth > scrollDepth.current) {
|
||||
scrollDepth.current = depth;
|
||||
sendCustomEvent(`scroll_${depth}`, 'scroll', pathname || '/');
|
||||
// Fire events for all thresholds we've passed but haven't fired yet
|
||||
for (const threshold of SCROLL_THRESHOLDS) {
|
||||
if (
|
||||
scrollPercentage >= threshold &&
|
||||
!firedThresholds.current.has(threshold)
|
||||
) {
|
||||
firedThresholds.current.add(threshold);
|
||||
sendCustomEvent(`scroll_${threshold}`, 'scroll', pathname || '/');
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
@@ -46,12 +46,15 @@ export function useWindowScrollDepth(): void {
|
||||
shouldTrackScroll.current = false;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
scrollDepth.current = 0;
|
||||
firedThresholds.current = new Set();
|
||||
shouldTrackScroll.current = true;
|
||||
// Immediately check current scroll position to capture thresholds
|
||||
// that may have been passed during the delay
|
||||
handleScroll();
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [pathname]);
|
||||
}, [pathname, handleScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
@@ -103,7 +103,7 @@ export function DocViewer({
|
||||
}`,
|
||||
width: 1600,
|
||||
height: 800,
|
||||
alt: 'Nx: Smart Repos · Fast Builds',
|
||||
alt: 'Nx: Smart Monorepos · Fast Builds',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
@@ -135,7 +135,7 @@ export function DocViewer({
|
||||
ref={ref}
|
||||
data-document="main"
|
||||
className={cx(
|
||||
'prose prose-slate dark:prose-invert w-full max-w-none 2xl:max-w-4xl',
|
||||
'prose prose-zinc dark:prose-invert w-full max-w-none 2xl:max-w-4xl',
|
||||
{ 'xl:max-w-2xl': !hideTableOfContent }
|
||||
)}
|
||||
>
|
||||
@@ -146,7 +146,7 @@ export function DocViewer({
|
||||
<div>
|
||||
<div
|
||||
className={cx(
|
||||
'sticky top-2 z-20 ml-[max(2rem,calc(50%-8rem))] hidden w-60 space-y-6 overflow-y-auto bg-white text-sm xl:block dark:bg-slate-900'
|
||||
'sticky top-2 z-20 ml-[max(2rem,calc(50%-8rem))] hidden w-60 space-y-6 overflow-y-auto bg-white text-sm xl:block dark:bg-zinc-900'
|
||||
)}
|
||||
>
|
||||
{widgetData.githubStarsCount > 0 && (
|
||||
@@ -163,18 +163,18 @@ export function DocViewer({
|
||||
document={document}
|
||||
>
|
||||
<>
|
||||
<div className="my-4 flex items-center justify-center space-x-2 rounded-md border border-slate-200 pl-2 pr-2 hover:border-slate-400 dark:border-slate-700 print:hidden">
|
||||
<div className="my-4 flex items-center justify-center space-x-2 rounded-md border border-zinc-200 pl-2 pr-2 hover:border-zinc-400 dark:border-zinc-700 print:hidden">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Give feedback on this page"
|
||||
title="Give feedback of this page"
|
||||
className="whitespace-nowrap border-transparent px-4 py-2 font-bold hover:text-slate-900 dark:hover:text-sky-400"
|
||||
className="whitespace-nowrap border-transparent px-4 py-2 font-bold hover:text-zinc-900 dark:hover:text-blue-400"
|
||||
onClick={() => setShowFeedback(true)}
|
||||
>
|
||||
Feedback
|
||||
</button>
|
||||
</div>
|
||||
<div className="my-4 flex items-center justify-center space-x-2 rounded-md border border-slate-200 pl-2 pr-2 hover:border-slate-400 dark:border-slate-700 print:hidden">
|
||||
<div className="my-4 flex items-center justify-center space-x-2 rounded-md border border-zinc-200 pl-2 pr-2 hover:border-zinc-400 dark:border-zinc-700 print:hidden">
|
||||
{document.filePath ? (
|
||||
<a
|
||||
aria-hidden="true"
|
||||
@@ -190,7 +190,7 @@ export function DocViewer({
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Edit this page on GitHub"
|
||||
className="whitespace-nowrap border-transparent px-4 py-2 font-bold hover:text-slate-900 dark:hover:text-sky-400"
|
||||
className="whitespace-nowrap border-transparent px-4 py-2 font-bold hover:text-zinc-900 dark:hover:text-blue-400"
|
||||
>
|
||||
Edit this page
|
||||
</a>
|
||||
@@ -207,7 +207,7 @@ export function DocViewer({
|
||||
<div
|
||||
data-document="related"
|
||||
className={cx(
|
||||
'prose prose-slate dark:prose-invert w-full max-w-none pt-8 2xl:max-w-4xl',
|
||||
'prose prose-zinc dark:prose-invert w-full max-w-none pt-8 2xl:max-w-4xl',
|
||||
{ 'xl:max-w-2xl': !hideTableOfContent }
|
||||
)}
|
||||
>
|
||||
@@ -221,7 +221,7 @@ export function DocViewer({
|
||||
hideTableOfContent ? '' : 'xl:hidden'
|
||||
}`}
|
||||
>
|
||||
<div className="ml-4 flex h-0.5 w-full flex-grow rounded bg-slate-50 dark:bg-slate-800/60" />
|
||||
<div className="ml-4 flex h-0.5 w-full flex-grow rounded bg-zinc-50 dark:bg-zinc-800/60" />
|
||||
<div className="relative z-0 inline-flex flex-shrink-0 rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
@@ -230,7 +230,7 @@ export function DocViewer({
|
||||
className={`relative inline-flex items-center rounded-l-md ${
|
||||
// If there is no file path for this page then don't show edit button.
|
||||
document.filePath ? '' : 'rounded-r-md'
|
||||
}border border-slate-200 bg-white px-4 py-2 text-xs font-medium text-slate-600 focus-within:ring-blue-500 hover:bg-slate-50 focus:z-10 focus:outline-none focus:ring-1 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800`}
|
||||
}border border-zinc-200 bg-white px-4 py-2 text-xs font-medium text-zinc-600 focus-within:ring-blue-500 hover:bg-zinc-50 focus:z-10 focus:outline-none focus:ring-1 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-400 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800`}
|
||||
onClick={() => setShowFeedback(true)}
|
||||
>
|
||||
Feedback
|
||||
@@ -247,7 +247,7 @@ export function DocViewer({
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Edit this page on GitHub"
|
||||
className="relative -ml-px inline-flex items-center rounded-r-md border border-slate-200 bg-white px-4 py-2 text-xs font-medium text-slate-600 focus-within:ring-blue-500 hover:bg-slate-50 focus:z-10 focus:outline-none focus:ring-1 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800"
|
||||
className="relative -ml-px inline-flex items-center rounded-r-md border border-zinc-200 bg-white px-4 py-2 text-xs font-medium text-zinc-600 focus-within:ring-blue-500 hover:bg-zinc-50 focus:z-10 focus:outline-none focus:ring-1 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-400 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800"
|
||||
>
|
||||
Edit this page
|
||||
</a>
|
||||
|
||||
@@ -40,12 +40,12 @@ const iconMap: { [key: string]: JSX.Element } = {
|
||||
|
||||
function CategoryBox({ category }: { category: RelatedDocumentsCategory }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white/60 p-5 dark:border-slate-800/40 dark:bg-slate-800/60">
|
||||
<div className="rounded-lg border border-zinc-200 bg-white/60 p-5 dark:border-zinc-800/40 dark:bg-zinc-800/60">
|
||||
<h4 className="mt-0 flex items-center pb-2 text-xl font-bold">
|
||||
{iconMap[category.id] ?? iconMap.default}
|
||||
{category.name}
|
||||
</h4>
|
||||
<ul className="list-none divide-y divide-slate-300 pl-0 dark:divide-slate-700">
|
||||
<ul className="list-none divide-y divide-zinc-300 pl-0 dark:divide-zinc-700">
|
||||
{category.relatedDocuments.map((d) => (
|
||||
<li
|
||||
key={d.id}
|
||||
@@ -53,11 +53,11 @@ function CategoryBox({ category }: { category: RelatedDocumentsCategory }) {
|
||||
>
|
||||
<Link
|
||||
href={d.path}
|
||||
className="flex flex-grow items-center justify-between no-underline transition-colors ease-out hover:text-blue-700 hover:underline dark:text-sky-500 dark:hover:text-sky-400"
|
||||
className="flex flex-grow items-center justify-between no-underline transition-colors ease-out hover:text-blue-700 hover:underline dark:text-blue-500 dark:hover:text-blue-400"
|
||||
prefetch={false}
|
||||
>
|
||||
<span>{d.name}</span>
|
||||
<ArrowRightIcon className="h-4 w-4 text-slate-500 dark:text-slate-400" />
|
||||
<ArrowRightIcon className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -86,9 +86,9 @@ export function TableOfContents({
|
||||
<Link
|
||||
href={href}
|
||||
className={cx(
|
||||
'block w-full border-l-4 border-slate-200 py-1 pl-3 transition hover:border-slate-500 dark:border-slate-700/40 dark:hover:border-slate-700',
|
||||
'block w-full border-l-4 border-zinc-200 py-1 pl-3 transition hover:border-zinc-500 dark:border-zinc-700/40 dark:hover:border-zinc-700',
|
||||
{
|
||||
'border-slate-500 bg-slate-50 dark:border-slate-700 dark:bg-slate-800/60':
|
||||
'border-zinc-500 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800/60':
|
||||
activeId === item.id && !item.highlightColor,
|
||||
// region Highlight Color
|
||||
'border-blue-200 bg-blue-50 hover:border-blue-500 dark:border-blue-700/40 dark:bg-blue-800/40 dark:hover:border-blue-700':
|
||||
|
||||
@@ -93,10 +93,10 @@ function FeedbackDialog({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel className="relative w-full max-w-2xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-900">
|
||||
<DialogPanel className="relative w-full max-w-2xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-zinc-900">
|
||||
<DialogTitle
|
||||
as="h3"
|
||||
className="bg-white p-4 text-center text-lg font-medium leading-6 text-slate-700 dark:bg-slate-900 dark:text-slate-400"
|
||||
className="bg-white p-4 text-center text-lg font-medium leading-6 text-zinc-700 dark:bg-zinc-900 dark:text-zinc-400"
|
||||
>
|
||||
What is on your mind?
|
||||
<button className={styles.closebutton} onClick={closeDialog}>
|
||||
@@ -154,7 +154,7 @@ function FeedbackDialog({
|
||||
htmlFor="idea"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => keydownHandler(e)}
|
||||
className="inline-flex w-full cursor-pointer items-center justify-between rounded-lg border border-gray-200 bg-white p-5 text-gray-500 hover:bg-slate-50 focus:outline-none focus:ring-1 peer-checked:border-sky-500 peer-checked:text-sky-500 dark:border-gray-700 dark:bg-gray-800 dark:text-slate-400 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800 dark:hover:text-gray-300 dark:peer-checked:text-sky-500"
|
||||
className="inline-flex w-full cursor-pointer items-center justify-between rounded-lg border border-gray-200 bg-white p-5 text-gray-500 hover:bg-zinc-50 focus:outline-none focus:ring-1 peer-checked:border-blue-500 peer-checked:text-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-zinc-400 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800 dark:hover:text-gray-300 dark:peer-checked:text-blue-500"
|
||||
>
|
||||
<div className="block">
|
||||
<div className="w-full text-lg font-semibold">Idea</div>
|
||||
@@ -242,10 +242,10 @@ function FeedbackDialog({
|
||||
onClick={submitFeedback}
|
||||
disabled={formDisabled}
|
||||
className={cx(
|
||||
'rounded-md border border-slate-200 bg-white px-4 py-2 text-base font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400',
|
||||
'rounded-md border border-zinc-200 bg-white px-4 py-2 text-base font-medium text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-400',
|
||||
{ 'cursor-not-allowed': formDisabled },
|
||||
{
|
||||
'focus-within:ring-blue-500 hover:bg-slate-50 focus:z-10 focus:outline-none focus:ring-1 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800':
|
||||
'focus-within:ring-blue-500 hover:bg-zinc-50 focus:z-10 focus:outline-none focus:ring-1 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800':
|
||||
!formDisabled,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -129,7 +129,7 @@ export function Content({
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-tooltip="Schema type"
|
||||
className="relative inline-flex rounded-md border border-slate-200 bg-slate-50 px-4 py-2 text-xs font-medium uppercase dark:border-slate-700 dark:bg-slate-800/60"
|
||||
className="relative inline-flex rounded-md border border-zinc-200 bg-zinc-50 px-4 py-2 text-xs font-medium uppercase dark:border-zinc-700 dark:bg-zinc-800/60"
|
||||
>
|
||||
{schemaViewModel.type}
|
||||
</div>
|
||||
@@ -157,7 +157,7 @@ export function Content({
|
||||
href={schemaViewModel.packageUrl}
|
||||
title="See package information"
|
||||
className={cx(
|
||||
'relative inline-flex items-center rounded-l-md border border-slate-200 bg-white px-4 py-2 text-xs font-medium text-slate-600 focus-within:ring-blue-500 hover:bg-slate-50 focus:z-10 focus:outline-none focus:ring-1 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800',
|
||||
'relative inline-flex items-center rounded-l-md border border-zinc-200 bg-white px-4 py-2 text-xs font-medium text-zinc-600 focus-within:ring-blue-500 hover:bg-zinc-50 focus:z-10 focus:outline-none focus:ring-1 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-400 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800',
|
||||
schemaViewModel.packageName.startsWith('@nx/powerpack')
|
||||
? 'rounded-md'
|
||||
: 'rounded-l-md'
|
||||
@@ -173,7 +173,7 @@ export function Content({
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="See this schema on GitHub"
|
||||
className="relative -ml-px inline-flex items-center rounded-r-md border border-slate-200 bg-white px-4 py-2 text-xs font-medium text-slate-600 focus-within:ring-blue-500 hover:bg-slate-50 focus:z-10 focus:outline-none focus:ring-1 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800"
|
||||
className="relative -ml-px inline-flex items-center rounded-r-md border border-zinc-200 bg-white px-4 py-2 text-xs font-medium text-zinc-600 focus-within:ring-blue-500 hover:bg-zinc-50 focus:z-10 focus:outline-none focus:ring-1 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-400 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800"
|
||||
>
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
@@ -237,7 +237,7 @@ export function Content({
|
||||
{/* We remove the top description on sub property lookup */}
|
||||
{!schemaViewModel.subReference && (
|
||||
<>
|
||||
<div className="prose prose-slate dark:prose-invert max-w-none">
|
||||
<div className="prose prose-zinc dark:prose-invert max-w-none">
|
||||
{vm.markdown.header}
|
||||
{vm.markdown.customContent}
|
||||
{vm.markdown.usageAndExamples}
|
||||
@@ -269,7 +269,7 @@ export function Content({
|
||||
setPresets(p.keys);
|
||||
}}
|
||||
type="button"
|
||||
className="relative inline-flex items-center rounded-md border border-slate-200 bg-white px-4 py-2 text-xs font-medium text-slate-600 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400 dark:hover:bg-slate-800"
|
||||
className="relative inline-flex items-center rounded-md border border-zinc-200 bg-white px-4 py-2 text-xs font-medium text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-400 dark:hover:bg-zinc-800"
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
@@ -278,7 +278,7 @@ export function Content({
|
||||
<button
|
||||
onClick={() => setPresets([])}
|
||||
type="button"
|
||||
className="relative inline-flex items-center rounded-md border border-slate-200 bg-white px-4 py-2 text-xs font-medium text-slate-600 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400 dark:hover:bg-slate-800"
|
||||
className="relative inline-flex items-center rounded-md border border-zinc-200 bg-white px-4 py-2 text-xs font-medium text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-400 dark:hover:bg-zinc-800"
|
||||
>
|
||||
Reset <XCircleIcon className="ml-1.5 h-4 w-4" />
|
||||
</button>
|
||||
@@ -286,7 +286,7 @@ export function Content({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="rounded-md border border-slate-200 p-0.5 dark:border-slate-700">
|
||||
<div className="rounded-md border border-zinc-200 p-0.5 dark:border-zinc-700">
|
||||
<SchemaEditor
|
||||
packageName={schemaViewModel.packageName}
|
||||
schemaName={schemaViewModel.schemaMetadata.name}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function MigrationViewer({
|
||||
<summary className="cursor-pointer">
|
||||
<Heading3 title={schema.name}></Heading3>
|
||||
</summary>
|
||||
<div className="prose prose-slate dark:prose-invert mb-6 ml-5">
|
||||
<div className="prose prose-zinc dark:prose-invert mb-6 ml-5">
|
||||
<p>{schema.description}</p>
|
||||
<div className="my-1">
|
||||
<strong>Version</strong>: {schema.version}
|
||||
|
||||
@@ -79,7 +79,7 @@ export function PackageSchemaList({
|
||||
url: vm.seo.imageUrl,
|
||||
width: 1600,
|
||||
height: 800,
|
||||
alt: 'Nx: Smart Repos · Fast Builds',
|
||||
alt: 'Nx: Smart Monorepos · Fast Builds',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
@@ -125,7 +125,7 @@ export function PackageSchemaList({
|
||||
|
||||
<div className="h-12">{/* SPACER */}</div>
|
||||
<Heading2 title={'Migrations'} />
|
||||
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
<ul className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
{!!filesAndLabels.length
|
||||
? filesAndLabels.map((schema) =>
|
||||
typeof schema === 'string' ? (
|
||||
|
||||
@@ -79,7 +79,7 @@ export function PackageSchemaSubList({
|
||||
url: vm.seo.imageUrl,
|
||||
width: 1600,
|
||||
height: 800,
|
||||
alt: 'Nx: Smart Repos · Fast Builds',
|
||||
alt: 'Nx: Smart Monorepos · Fast Builds',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
@@ -116,7 +116,7 @@ export function PackageSchemaSubList({
|
||||
) : null}
|
||||
|
||||
{vm.type === 'migration' ? (
|
||||
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
<ul className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
{filesAndLabels.map((schema) =>
|
||||
typeof schema === 'string' ? (
|
||||
<VersionLabelListItem
|
||||
@@ -142,7 +142,7 @@ export function PackageSchemaSubList({
|
||||
|
||||
export const VersionLabelListItem = ({ label }: { label: string }) => {
|
||||
return label ? (
|
||||
<li className="relative flex px-1 pt-2 transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-slate-50 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800/60">
|
||||
<li className="relative flex px-1 pt-2 transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-zinc-50 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800/60">
|
||||
<div className="pt-2">
|
||||
<span className="text-sm font-bold">
|
||||
<Heading2 title={label} />
|
||||
|
||||
@@ -56,7 +56,7 @@ export function PackageSchemaViewer({
|
||||
url: vm.seo.imageUrl,
|
||||
width: 1600,
|
||||
height: 800,
|
||||
alt: 'Nx: Smart Repos · Fast Builds',
|
||||
alt: 'Nx: Smart Monorepos · Fast Builds',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -24,13 +24,13 @@ export const ParameterView = (props: {
|
||||
{props.alias && (
|
||||
<span
|
||||
data-tooltip="Property alias"
|
||||
className="relative -top-0.5 inline-flex rounded-md px-2 text-xs font-semibold leading-5 dark:bg-slate-700"
|
||||
className="relative -top-0.5 inline-flex rounded-md px-2 text-xs font-semibold leading-5 dark:bg-zinc-700"
|
||||
>
|
||||
{props.alias}
|
||||
</span>
|
||||
)}
|
||||
{props.required && (
|
||||
<span className="relative -top-0.5 inline-flex rounded-md bg-slate-100 px-2 text-xs font-semibold uppercase leading-5 dark:bg-slate-700">
|
||||
<span className="relative -top-0.5 inline-flex rounded-md bg-zinc-100 px-2 text-xs font-semibold uppercase leading-5 dark:bg-zinc-700">
|
||||
Required
|
||||
</span>
|
||||
)}
|
||||
@@ -68,7 +68,7 @@ export const ParameterView = (props: {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="prose prose-slate dark:prose-invert -mt-4 max-w-none">
|
||||
<div className="prose prose-zinc dark:prose-invert -mt-4 max-w-none">
|
||||
{
|
||||
renderMarkdown(props.description, {
|
||||
filePath: '',
|
||||
@@ -78,7 +78,7 @@ export const ParameterView = (props: {
|
||||
|
||||
{props.deprecated &&
|
||||
typeof (props.schema as any)['x-deprecated'] === 'string' ? (
|
||||
<div className="prose prose-slate dark:prose-invert mt-2 rounded-md bg-red-100 px-4 text-red-800 dark:bg-red-800 dark:text-red-100">
|
||||
<div className="prose prose-zinc dark:prose-invert mt-2 rounded-md bg-red-100 px-4 text-red-800 dark:bg-red-800 dark:text-red-100">
|
||||
{
|
||||
renderMarkdown(String((props.schema as any)['x-deprecated']), {
|
||||
filePath: '',
|
||||
|
||||
@@ -5,7 +5,7 @@ import Link from 'next/link';
|
||||
export const Heading1 = ({ title }: { title: string }) => (
|
||||
<h1
|
||||
id={slugify(title)}
|
||||
className="group mb-5 text-4xl font-extrabold tracking-tight text-slate-900 dark:text-slate-100"
|
||||
className="group mb-5 text-4xl font-extrabold tracking-tight text-zinc-900 dark:text-zinc-100"
|
||||
>
|
||||
<span>{title}</span>
|
||||
<Link aria-hidden="true" tabIndex={-1} href={'#' + slugify(title)}>
|
||||
@@ -20,7 +20,7 @@ export const Heading1 = ({ title }: { title: string }) => (
|
||||
export const Heading2 = ({ title }: { title: string }) => (
|
||||
<h2
|
||||
id={slugify(title)}
|
||||
className="group mb-5 text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200"
|
||||
className="group mb-5 text-2xl font-bold tracking-tight text-zinc-800 dark:text-zinc-200"
|
||||
>
|
||||
<span>{title}</span>
|
||||
<Link aria-hidden="true" tabIndex={-1} href={'#' + slugify(title)}>
|
||||
@@ -35,7 +35,7 @@ export const Heading2 = ({ title }: { title: string }) => (
|
||||
export const Heading3 = ({ title }: { title: string }) => (
|
||||
<h3
|
||||
id={slugify(title)}
|
||||
className="group text-xl font-semibold tracking-tight text-slate-700 dark:text-slate-300"
|
||||
className="group text-xl font-semibold tracking-tight text-zinc-700 dark:text-zinc-300"
|
||||
>
|
||||
<span>{title}</span>
|
||||
<Link aria-hidden="true" tabIndex={-1} href={'#' + slugify(title)}>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function DocumentList({
|
||||
documents: DocumentMetadata[];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
<ul className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
{!!documents.length ? (
|
||||
documents.map((guide) => (
|
||||
<DocumentListItem key={guide.id} document={guide} />
|
||||
@@ -36,9 +36,9 @@ function DocumentListItem({
|
||||
return (
|
||||
<li
|
||||
key={document.name}
|
||||
className="relative flex px-2 py-2 transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-slate-50 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800/60"
|
||||
className="relative flex px-2 py-2 transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-zinc-50 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800/60"
|
||||
>
|
||||
<div className="flex-shrink-0 self-start rounded-lg border-slate-200 bg-slate-100 p-2 dark:border-slate-600 dark:bg-slate-700">
|
||||
<div className="flex-shrink-0 self-start rounded-lg border-zinc-200 bg-zinc-100 p-2 dark:border-zinc-600 dark:bg-zinc-700">
|
||||
<DocumentIcon className="h-5 w-5" role="img" />
|
||||
</div>
|
||||
<div className="ml-3 py-2">
|
||||
@@ -61,7 +61,7 @@ export function SchemaList({
|
||||
type: 'executor' | 'generator';
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
<ul className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
{!!files.length ? (
|
||||
files.map((schema) => (
|
||||
<SchemaListItem key={schema.name} file={schema} />
|
||||
@@ -77,9 +77,9 @@ function SchemaListItem({ file }: { file: FileMetadata }): JSX.Element {
|
||||
return (
|
||||
<li
|
||||
key={file.name}
|
||||
className="relative flex px-2 py-2 transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-slate-50 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800/60"
|
||||
className="relative flex px-2 py-2 transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-zinc-50 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800/60"
|
||||
>
|
||||
<div className="flex-shrink-0 self-start rounded-lg border-slate-200 bg-slate-100 p-2 dark:border-slate-600 dark:bg-slate-700">
|
||||
<div className="flex-shrink-0 self-start rounded-lg border-zinc-200 bg-zinc-100 p-2 dark:border-zinc-600 dark:bg-zinc-700">
|
||||
{file.type === 'executor' ? (
|
||||
<CpuChipIcon className="h-5 w-5" role="img" />
|
||||
) : (
|
||||
@@ -99,7 +99,7 @@ function SchemaListItem({ file }: { file: FileMetadata }): JSX.Element {
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<div className="prose prose-slate dark:prose-invert prose-sm">
|
||||
<div className="prose prose-zinc dark:prose-invert prose-sm">
|
||||
{
|
||||
renderMarkdown(file.description, {
|
||||
filePath: '',
|
||||
@@ -117,10 +117,10 @@ function EmptyList({
|
||||
type: 'executor' | 'generator' | 'document';
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<li className="relative flex px-2 py-2 transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-slate-50 dark:focus-within:ring-sky-500 dark:hover:bg-slate-800/60">
|
||||
<div className="flex-shrink-0 self-start rounded-lg border-slate-200 bg-slate-100 p-2 dark:border-slate-600 dark:bg-slate-700">
|
||||
<li className="relative flex px-2 py-2 transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-zinc-50 dark:focus-within:ring-blue-500 dark:hover:bg-zinc-800/60">
|
||||
<div className="flex-shrink-0 self-start rounded-lg border-zinc-200 bg-zinc-100 p-2 dark:border-zinc-600 dark:bg-zinc-700">
|
||||
<InformationCircleIcon
|
||||
className="h-5 w-5 flex-shrink-0 rounded-md border-slate-200 bg-slate-50 dark:bg-slate-700 dark:bg-slate-800"
|
||||
className="h-5 w-5 flex-shrink-0 rounded-md border-zinc-200 bg-zinc-50 dark:bg-zinc-700 dark:bg-zinc-800"
|
||||
role="img"
|
||||
/>
|
||||
</div>
|
||||
@@ -136,7 +136,7 @@ function EmptyList({
|
||||
{type} available for this package yet!
|
||||
</Link>
|
||||
</p>
|
||||
<div className="prose prose-slate dark:prose-invert prose-sm">
|
||||
<div className="prose prose-zinc dark:prose-invert prose-sm">
|
||||
<a
|
||||
href="https://github.com/nrwl/nx/discussions"
|
||||
target="_blank"
|
||||
|
||||
@@ -11,7 +11,7 @@ export function TopSchemaLayout({
|
||||
<div className="mb-8 flex w-full items-center space-x-2">
|
||||
<div className="w-full flex-grow">
|
||||
<div
|
||||
className="relative inline-flex rounded-md border border-slate-200 bg-slate-50 px-4 py-2 text-xs font-medium uppercase dark:border-slate-700 dark:bg-slate-800/60"
|
||||
className="relative inline-flex rounded-md border border-zinc-200 bg-zinc-50 px-4 py-2 text-xs font-medium uppercase dark:border-zinc-700 dark:bg-zinc-800/60"
|
||||
aria-hidden="true"
|
||||
data-tooltip="Installable Package"
|
||||
>
|
||||
@@ -26,7 +26,7 @@ export function TopSchemaLayout({
|
||||
rel="noreferrer"
|
||||
aria-hidden="true"
|
||||
title="See package on GitHub"
|
||||
className="relative inline-flex items-center rounded-md border border-slate-200 bg-slate-50 px-4 py-2 text-xs font-medium dark:border-slate-700 dark:bg-slate-800/60"
|
||||
className="relative inline-flex items-center rounded-md border border-zinc-200 bg-zinc-50 px-4 py-2 text-xs font-medium dark:border-zinc-700 dark:bg-zinc-800/60"
|
||||
>
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
|
||||
@@ -92,15 +92,15 @@ export function AlgoliaSearch({
|
||||
type="button"
|
||||
ref={searchButtonRef}
|
||||
onClick={handleOpen}
|
||||
className="flex w-full items-center rounded-md bg-white px-2 py-1.5 text-sm leading-4 ring-1 ring-slate-300 transition dark:bg-slate-700 dark:ring-slate-900"
|
||||
className="flex w-full items-center rounded-md bg-white px-2 py-1.5 text-sm leading-4 ring-1 ring-zinc-300 transition dark:bg-zinc-700 dark:ring-zinc-900"
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 flex-none" />
|
||||
<span className="mx-3 inline-flex text-xs text-slate-300 md:text-sm dark:text-slate-400">
|
||||
<span className="mx-3 inline-flex text-xs text-zinc-300 md:text-sm dark:text-zinc-400">
|
||||
Search
|
||||
</span>
|
||||
<span
|
||||
style={{ opacity: browserDetected ? '1' : '0' }}
|
||||
className="ml-auto hidden flex-none rounded-md border border-slate-200 bg-slate-50 px-1 py-0.5 text-xs font-semibold text-slate-500 md:block dark:border-slate-700 dark:bg-slate-800/60"
|
||||
className="ml-auto hidden flex-none rounded-md border border-zinc-200 bg-zinc-50 px-1 py-0.5 text-xs font-semibold text-zinc-500 md:block dark:border-zinc-700 dark:bg-zinc-800/60"
|
||||
>
|
||||
<span className="sr-only">Press </span>
|
||||
<kbd className="font-sans">
|
||||
@@ -122,7 +122,7 @@ export function AlgoliaSearch({
|
||||
>
|
||||
<span
|
||||
style={{ opacity: browserDetected ? '1' : '0' }}
|
||||
className="ml-auto block flex-none rounded-md border border-slate-200 bg-slate-50/60 px-1 py-0.5 text-xs font-semibold text-slate-400 transition hover:text-slate-500 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-500 dark:hover:text-slate-400"
|
||||
className="ml-auto block flex-none rounded-md border border-zinc-200 bg-zinc-50/60 px-1 py-0.5 text-xs font-semibold text-zinc-400 transition hover:text-zinc-500 dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-500 dark:hover:text-zinc-400"
|
||||
>
|
||||
<span className="sr-only">Press </span>
|
||||
<kbd className="font-sans">
|
||||
|
||||
@@ -3,5 +3,5 @@ import { test, expect } from '@playwright/test';
|
||||
test('should display the primary heading', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const heading = page.locator('[data-cy="primary-heading"]');
|
||||
await expect(heading).toContainText('Smart ReposFast Builds');
|
||||
await expect(heading).toContainText('Smart MonoreposFast Builds');
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function generateMetadata(
|
||||
url: post.ogImage,
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart, Fast and Extensible Build System',
|
||||
alt: 'Nx: Smart, Fast and Extensible Monorepo Platform',
|
||||
type: `image/${post.ogImageType}`,
|
||||
},
|
||||
...previousImages,
|
||||
|
||||
@@ -20,7 +20,7 @@ export const metadata: Metadata = {
|
||||
url: 'https://nx.dev/socials/nx-media.png',
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart Repos · Fast Builds',
|
||||
alt: 'Nx: Smart Monorepos · Fast Builds',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -28,7 +28,7 @@ export const metadata: Metadata = {
|
||||
url: 'https://nx.dev/socials/nx-media.png',
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart Repos · Fast Builds',
|
||||
alt: 'Nx: Smart Monorepos · Fast Builds',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -30,7 +30,7 @@ export const metadata: Metadata = {
|
||||
url: 'https://nx.dev/socials/nx-media.png',
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart Repos · Fast Builds',
|
||||
alt: 'Nx: Smart Monorepos · Fast Builds',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="h-full bg-white text-slate-700 antialiased selection:bg-blue-500 selection:text-white dark:bg-slate-900 dark:text-slate-400 dark:selection:bg-sky-500">
|
||||
<body className="h-full bg-white text-zinc-700 antialiased selection:bg-blue-500 selection:text-white dark:bg-zinc-900 dark:text-zinc-400 dark:selection:bg-blue-500">
|
||||
<GlobalSearchHandler />
|
||||
{children}
|
||||
{bannerCollection.map((bannerConfig) => {
|
||||
|
||||