From 2d26b76d6b0478aba618431651033c1fccf1e439 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Feb 2025 23:01:30 +0800 Subject: [PATCH 001/114] Modify sign in component docs to show how SDKFilter should be used --- docs/components/authentication/sign-in.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/components/authentication/sign-in.mdx b/docs/components/authentication/sign-in.mdx index 7a80c28448..8972d4b4d9 100644 --- a/docs/components/authentication/sign-in.mdx +++ b/docs/components/authentication/sign-in.mdx @@ -105,6 +105,7 @@ All props are optional. An optional element to be rendered while the component is mounting. + ## Usage with frameworks The following example includes basic implementation of the `` component. You can use this as a starting point for your own implementation. @@ -187,6 +188,9 @@ The following example includes basic implementation of the `` componen ``` + + + ## Usage with JavaScript @@ -341,6 +345,8 @@ clerk.openSignIn() clerk.closeSignIn() ``` + + ## Customization To learn about how to customize Clerk components, see the [customization documentation](/docs/customization/overview). From a390825fb232e67f98ef82bab7cd9ae93c0924f9 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 8 Feb 2025 00:44:34 +0800 Subject: [PATCH 002/114] Expand filter to display framework section for all frameworks with a component regardless of if they have an example --- docs/components/authentication/sign-in.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/components/authentication/sign-in.mdx b/docs/components/authentication/sign-in.mdx index 8972d4b4d9..240152ec33 100644 --- a/docs/components/authentication/sign-in.mdx +++ b/docs/components/authentication/sign-in.mdx @@ -105,7 +105,7 @@ All props are optional. An optional element to be rendered while the component is mounting. - + ## Usage with frameworks The following example includes basic implementation of the `` component. You can use this as a starting point for your own implementation. From 2f91e1fc6b9fc2ad28c6247c9bd64fa16a562c1a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 8 Feb 2025 01:18:30 +0800 Subject: [PATCH 003/114] Filter Clerk Components to only frontend and full stack javascript based sdks --- docs/manifest.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/manifest.json b/docs/manifest.json index cfb896f71a..d537f53d99 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2057,6 +2057,18 @@ [ { "title": "Clerk Components", + "sdk": [ + "nextjs", + "react", + "astro", + "chrome-extension", + "expo", + "nuxt", + "react-router", + "remix", + "tanstack-start", + "vue" + ], "items": [ [ { From 4c5c2807c27f6c43bf06dda276321dd814d030d3 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 10 Feb 2025 21:05:34 +0800 Subject: [PATCH 004/114] Update the example to use the component --- docs/components/authentication/sign-in.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/components/authentication/sign-in.mdx b/docs/components/authentication/sign-in.mdx index 240152ec33..fe22892e68 100644 --- a/docs/components/authentication/sign-in.mdx +++ b/docs/components/authentication/sign-in.mdx @@ -105,7 +105,7 @@ All props are optional. An optional element to be rendered while the component is mounting. - + ## Usage with frameworks The following example includes basic implementation of the `` component. You can use this as a starting point for your own implementation. @@ -188,9 +188,9 @@ The following example includes basic implementation of the `` componen ``` - + - + ## Usage with JavaScript @@ -345,7 +345,7 @@ clerk.openSignIn() clerk.closeSignIn() ``` - + ## Customization From 720d0f7a625c1a753d3b96b76524af3a70f09ffa Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 10 Feb 2025 21:05:56 +0800 Subject: [PATCH 005/114] Add the javascript sdk to the sdk filter in the manifest --- docs/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/manifest.json b/docs/manifest.json index d537f53d99..80d644a896 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2060,6 +2060,7 @@ "sdk": [ "nextjs", "react", + "javascript-frontend", "astro", "chrome-extension", "expo", From 2d5a2c6ace1f6dcf7bbec1b801cb35d730fee7b3 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 15 Feb 2025 00:42:24 +0800 Subject: [PATCH 006/114] step 1 of the build script, generating a sdk specific manifest --- .gitignore | 1 + docs/manifest.schema.json | 6 - package-lock.json | 794 ++++++++++++++++++++++++-------------- package.json | 5 +- scripts/build-docs.ts | 233 +++++++++++ 5 files changed, 734 insertions(+), 305 deletions(-) create mode 100644 scripts/build-docs.ts diff --git a/.gitignore b/.gitignore index 5b57d3cf4c..176f1bda5a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ # production /build +/dist # misc .DS_Store diff --git a/docs/manifest.schema.json b/docs/manifest.schema.json index 720cb43ba2..d49d87c057 100644 --- a/docs/manifest.schema.json +++ b/docs/manifest.schema.json @@ -55,12 +55,6 @@ "target": { "type": "string", "enum": ["_blank"] - }, - "sdk": { - "type": "array", - "items": { - "$ref": "#/$defs/sdk" - } } } }, diff --git a/package-lock.json b/package-lock.json index f564915d4f..09baa4bfe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "clerk-docs-2023", "version": "0.1.0", "devDependencies": { - "@clerk/testing": "^1.2.18", + "@types/node": "^22.13.2", "concurrently": "^8.2.2", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", @@ -19,6 +19,7 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", + "tsx": "^4.19.2", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0" @@ -127,99 +128,412 @@ "node": ">=6.9.0" } }, - "node_modules/@clerk/backend": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.9.2.tgz", - "integrity": "sha512-8vCYux8Xbu5TQ2iq9tYuDnNhv3K/XhZ+34QJG+n4ZX4w4FfTTuzwb5OylcmP69vPvYybhoQfjpK57kBOW22deg==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@clerk/shared": "2.6.2", - "@clerk/types": "4.19.0", - "cookie": "0.5.0", - "snakecase-keys": "5.4.4", - "tslib": "2.4.1" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=18.17.0" + "node": ">=18" } }, - "node_modules/@clerk/backend/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "0BSD" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@clerk/shared": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-2.6.2.tgz", - "integrity": "sha512-RkrNknIr98GPu3srXLhjJViC1Mom1gUsRMNnf1deibX2yvRnndloZGnFb0qxf+pFL/NCkKd3nSHtK3eCJQrVYQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@clerk/types": "4.19.0", - "glob-to-regexp": "0.4.1", - "js-cookie": "3.0.5", - "std-env": "^3.7.0", - "swr": "^2.2.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "react": ">=18 || >=19.0.0-beta", - "react-dom": ">=18 || >=19.0.0-beta" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@clerk/testing": { - "version": "1.2.18", - "resolved": "https://registry.npmjs.org/@clerk/testing/-/testing-1.2.18.tgz", - "integrity": "sha512-F2NLAFib0FrroQmuR5dFPa1flBlAgAUwfff/oUAyUmhM6de8EtAiOMrEn4pfhz92+8G1XUkNNVkFQL27xH9AzA==", + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@clerk/backend": "1.9.2", - "@clerk/shared": "2.6.2", - "@clerk/types": "4.19.0", - "dotenv": "16.4.5" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "@playwright/test": "^1", - "cypress": "^13" - }, - "peerDependenciesMeta": { - "@playwright/test": { - "optional": true - }, - "cypress": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@clerk/types": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.19.0.tgz", - "integrity": "sha512-bDN/nRUD5PFCehQ+Kjdcft0I3b9CIyCcY3OBNDSc1L6RQhlXH+J48EtaP/cbRdslb83LJiBPQ2i/gV4VgblzwA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "csstype": "3.1.1" - }, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.17.0" + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@jridgewell/gen-mapping": { @@ -323,6 +637,16 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", "dev": true }, + "node_modules/@types/node": { + "version": "22.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.2.tgz", + "integrity": "sha512-Z+r8y3XL9ZpI2EY52YYygAFmo2/oWfNSj4BCpAXE2McAexDk8VcnBMGC9Djn9gTKt4d2T/hhXqmPzo4hfIXtTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/supports-color": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", @@ -415,13 +739,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "dev": true, - "license": "MIT" - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -562,16 +879,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -586,13 +893,6 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true, - "license": "MIT" - }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -671,30 +971,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -707,6 +983,46 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -776,6 +1092,21 @@ "node": ">=0.4.x" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -785,12 +1116,18 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, - "license": "BSD-2-Clause" + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", @@ -823,24 +1160,6 @@ "@types/estree": "*" } }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -864,30 +1183,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -898,19 +1193,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/markdown-table": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", @@ -2162,17 +2444,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, "node_modules/periscopic": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", @@ -2242,35 +2513,6 @@ "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2393,6 +2635,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -2417,17 +2669,6 @@ "suf-log": "^2.5.3" } }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/shell-quote": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", @@ -2437,32 +2678,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/snakecase-keys": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-5.4.4.tgz", - "integrity": "sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "map-obj": "^4.1.0", - "snake-case": "^3.0.4", - "type-fest": "^2.5.2" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -2479,13 +2694,6 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, - "node_modules/std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2571,20 +2779,6 @@ "node": ">=16" } }, - "node_modules/swr": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", - "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "client-only": "^0.0.1", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2610,19 +2804,33 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, "engines": { - "node": ">=12.20" + "node": ">=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "fsevents": "~2.3.3" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", @@ -2724,16 +2932,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/vfile": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", diff --git a/package.json b/package.json index 255cc5680a..4a28deec4f 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,11 @@ "format": "prettier . --write", "lint:check-links": "node ./scripts/check-links.mjs", "lint:formatting": "prettier . --check", - "lint:check-quickstarts": "node ./scripts/check-quickstarts.mjs" + "lint:check-quickstarts": "node ./scripts/check-quickstarts.mjs", + "build": "tsx ./scripts/build-docs.ts" }, "devDependencies": { + "@types/node": "^22.13.2", "concurrently": "^8.2.2", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", @@ -20,6 +22,7 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", + "tsx": "^4.19.2", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0" diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts new file mode 100644 index 0000000000..cdf1117368 --- /dev/null +++ b/scripts/build-docs.ts @@ -0,0 +1,233 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +const BASE_PATH = process.cwd() +const MANIFEST_FILE_PATH = path.join(BASE_PATH, './docs/manifest.json') +const DIST_PATH = path.join(BASE_PATH, './dist') +const CLERK_PATH = path.join(BASE_PATH, "../clerk") +const IGNORE = ["/docs/core-1"] + +const VALID_SDKS = [ + "nextjs", + "react", + "javascript-frontend", + "chrome-extension", + "expo", + "ios", + "nodejs", + "expressjs", + "fastify", + "react-router", + "remix", + "tanstack-start", + "go", + "astro", + "nuxt", + "vue", + "ruby", + "python", + "javascript-backend", + "sdk-development", + "community-sdk" +] as const + +type SDK = typeof VALID_SDKS[number] + +type ManifestItem = { + title: string + href: string + target?: '_blank' + sdk?: SDK[] +} + +type ManifestGroup = { + title: string + items: Manifest + sdk?: SDK[] +} + +type Manifest = (ManifestItem | ManifestGroup)[][] + + +const isValidSdk = (sdk: string): sdk is SDK => { + return VALID_SDKS.includes(sdk as SDK) +} + +const isValidSdks = (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk) +} + +const readManifest = async (): Promise => { + const manifest = await fs.readFile(MANIFEST_FILE_PATH, 'utf8') + return JSON.parse(manifest).navigation +} + +const readMarkdownFile = async (docPath: string): Promise => { + const filePath = path.join(process.cwd(), `${docPath}.mdx`) + const fileContent = await fs.readFile(filePath, 'utf8') + return fileContent +} + +const parseFrontmatter = (content: string, key: string) => { + const frontmatterMatch = content.match(/---\n([\s\S]*?)---/) + if (!frontmatterMatch) { + return null + } + + const frontmatter = frontmatterMatch[1] + const keyRegex = new RegExp(`${key}:\\s*([\\s\\S]*?)\\n`) + const valueMatch = frontmatter.match(keyRegex) + + if (!valueMatch) { + return null + } + + const rawValue = valueMatch[1].trim() + + if (rawValue.includes(',')) { + return rawValue.split(/\s*,\s*/) + } + + return rawValue +} + +const ensureDirectory = async (path: string): Promise => { + try { + await fs.access(path) + } catch { + await fs.mkdir(path) + } +} + +const writeSDKFile = async (sdk: SDK, filePath: string, contents: string) => { + const fullPath = path.join(DIST_PATH, sdk, filePath) + await ensureDirectory(path.dirname(fullPath)) + await fs.writeFile(fullPath, contents) +} + +type ItemCallback = (item: ManifestItem) => Promise +type GroupCallback = (item: ManifestGroup) => Promise + +// this will recursively traverse the manifest +// if you return null it will filter out the item (and filter out groups that become empty) +const traverseManifest = async ( + manifest: Manifest, + itemCallback: ItemCallback = async (item) => item, + groupCallback: GroupCallback = async (item) => item +): Promise => { + const result = await Promise.all(manifest.map(async (navGroup) => { + return Promise.all(navGroup.map(async (item) => { + if ('href' in item) { + return await itemCallback(item) + } + + if ('items' in item && Array.isArray(item.items)) { + return await groupCallback({ + ...item, + items: (await traverseManifest(item.items, itemCallback, groupCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) + }) + } + + return item + })) + })) + + return result.map(group => group.filter((item): item is NonNullable => item !== null)) +} + +const scopeItemToSDK = (item: Omit, itemSDK: undefined | SDK[], targetSDK: SDK): ManifestItem => { + + // This is external so can't change it + if (item.href.startsWith('/docs') === false) return item + + // This item is not scoped to a specific sdk, so leave it alone + if (itemSDK === undefined) return item + + const hrefSegments = item.href.split('/') + + // This is a little hacky so we might change it + // if the url already contains the sdk, we don't need to change it + if (hrefSegments.includes(targetSDK)) { + return item + } + + // Add the sdk to the url + return { + ...item, + href: `/docs/${targetSDK}/${hrefSegments.slice(1).join('/')}` + } +} + +const main = async () => { + await ensureDirectory(DIST_PATH) + + const manifest = await readManifest() + + // This first pass goes through and grabs the sdk scoping out of the markdown files frontmatter + const fullManifest = await traverseManifest(manifest, + async (item) => { + + if (!item.href?.startsWith('/docs/')) return item + if (item.target !== undefined) return item + if (IGNORE.includes(item.href)) return item + + const fileContent = await readMarkdownFile(item.href) + const frontmatterSDK = parseFrontmatter(fileContent, 'sdk') + + if (frontmatterSDK === null) return item + + const sdks = Array.isArray(frontmatterSDK) ? frontmatterSDK : [frontmatterSDK] + + if (isValidSdks(sdks) === false) { + throw new Error(`Invalid SDK ${JSON.stringify(sdks)} found in: ${item.href}`) + } + + return { + ...item, + sdk: sdks + } + }, + async (group) => { + const sdk = Array.from(new Set(group.items?.flatMap((item) => + item.flatMap((item) => item.sdk)))).filter((sdk): sdk is SDK => Boolean(sdk)) + + const { items, ...details } = group + if (sdk.length === 0) return { ...details, items } + return { + ...details, + sdk, + items + } + } + ) + + for (const targetSdk of VALID_SDKS) { + + // This second pass goes through and removes any items that are not scoped to the target sdk + const sdkSpecificManifest = await traverseManifest(fullManifest, + async ({ sdk, ...item }) => { + + // this means its generic, not scoped to a specific sdk, so we keep it + if (sdk === undefined) return item + + // this item is not scoped to the target sdk, so we remove it + if (sdk.includes(targetSdk) === false) return null + + // this item is scoped to the target sdk, so we keep it + return scopeItemToSDK(item, sdk, targetSdk) + }, + async ({ sdk, ...group }) => { + + if (sdk === undefined) return group + + if (sdk.includes(targetSdk) === false) return null + + return group + } + ) + + await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation: sdkSpecificManifest })) + } +} + +main() \ No newline at end of file From 2b21ce8932a43385df142f13655d593b80850a6b Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 18 Feb 2025 04:41:02 +0800 Subject: [PATCH 007/114] wip --- package-lock.json | 65 ++++++++- package.json | 5 +- scripts/build-docs.ts | 326 +++++++++++++++++++++++++++++++----------- 3 files changed, 311 insertions(+), 85 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09baa4bfe1..dd3cbea7c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "clerk-docs-2023", "version": "0.1.0", "devDependencies": { + "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", "concurrently": "^8.2.2", "prettier": "^3.2.5", @@ -20,9 +21,11 @@ "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", "tsx": "^4.19.2", + "typescript": "^5.7.3", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", - "vfile-reporter": "^8.0.0" + "vfile-reporter": "^8.0.0", + "yaml": "^2.7.0" } }, "node_modules/@ampproject/remapping": { @@ -589,6 +592,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -2824,6 +2860,20 @@ "fsevents": "~2.3.3" } }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -3140,6 +3190,19 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 4a28deec4f..771bb09cba 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build": "tsx ./scripts/build-docs.ts" }, "devDependencies": { + "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", "concurrently": "^8.2.2", "prettier": "^3.2.5", @@ -23,8 +24,10 @@ "remark-gfm": "^4.0.0", "remark-mdx": "^3.0.1", "tsx": "^4.19.2", + "typescript": "^5.7.3", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", - "vfile-reporter": "^8.0.0" + "vfile-reporter": "^8.0.0", + "yaml": "^2.7.0" } } diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index cdf1117368..c1fecffcbd 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1,5 +1,12 @@ import fs from 'node:fs/promises' import path from 'node:path' +import remarkMdx from 'remark-mdx' +import { remark } from 'remark' +import { visit } from 'unist-util-visit' +import remarkFrontmatter from 'remark-frontmatter' +import yaml from "yaml" +import { slugifyWithCounter } from '@sindresorhus/slugify' +import { toString } from 'mdast-util-to-string' const BASE_PATH = process.cwd() const MANIFEST_FILE_PATH = path.join(BASE_PATH, './docs/manifest.json') @@ -33,6 +40,14 @@ const VALID_SDKS = [ type SDK = typeof VALID_SDKS[number] +const isValidSdk = (sdk: string): sdk is SDK => { + return VALID_SDKS.includes(sdk as SDK) +} + +const isValidSdks = (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk) +} + type ManifestItem = { title: string href: string @@ -48,114 +63,110 @@ type ManifestGroup = { type Manifest = (ManifestItem | ManifestGroup)[][] - -const isValidSdk = (sdk: string): sdk is SDK => { - return VALID_SDKS.includes(sdk as SDK) -} - -const isValidSdks = (sdks: string[]): sdks is SDK[] => { - return sdks.every(isValidSdk) -} - const readManifest = async (): Promise => { - const manifest = await fs.readFile(MANIFEST_FILE_PATH, 'utf8') + const manifest = await fs.readFile(MANIFEST_FILE_PATH, { "encoding": "utf-8" }) return JSON.parse(manifest).navigation } const readMarkdownFile = async (docPath: string): Promise => { - const filePath = path.join(process.cwd(), `${docPath}.mdx`) - const fileContent = await fs.readFile(filePath, 'utf8') + const filePath = path.join(BASE_PATH, `${docPath}.mdx`) + const fileContent = await fs.readFile(filePath, { "encoding": "utf-8" }) return fileContent } -const parseFrontmatter = (content: string, key: string) => { - const frontmatterMatch = content.match(/---\n([\s\S]*?)---/) - if (!frontmatterMatch) { - return null - } - - const frontmatter = frontmatterMatch[1] - const keyRegex = new RegExp(`${key}:\\s*([\\s\\S]*?)\\n`) - const valueMatch = frontmatter.match(keyRegex) - - if (!valueMatch) { - return null - } - - const rawValue = valueMatch[1].trim() - - if (rawValue.includes(',')) { - return rawValue.split(/\s*,\s*/) - } - - return rawValue +const markdownProcessor = remark() + .use(remarkFrontmatter) + .use(remarkMdx) + .freeze() + +const parseFrontmatter = (fileContent: string): Record | undefined => { + let frontmatter: Record | undefined = undefined + + markdownProcessor() + .use(() => (tree, vfile) => { + visit(tree, + node => node.type === 'yaml' && "value" in node, + node => { + if (!("value" in node)) return; + if (typeof node.value !== "string") return; + + frontmatter = yaml.parse(node.value) + } + ) + }) + .process(fileContent) + + return frontmatter } const ensureDirectory = async (path: string): Promise => { try { await fs.access(path) } catch { - await fs.mkdir(path) + await fs.mkdir(path, { recursive: true }) } } -const writeSDKFile = async (sdk: SDK, filePath: string, contents: string) => { - const fullPath = path.join(DIST_PATH, sdk, filePath) +const writeDistFile = async (filePath: string, contents: string) => { + const fullPath = path.join(DIST_PATH, filePath) await ensureDirectory(path.dirname(fullPath)) - await fs.writeFile(fullPath, contents) + await fs.writeFile(fullPath, contents, { "encoding": "utf-8" }) + // console.log(`wrote ${fullPath}`) } -type ItemCallback = (item: ManifestItem) => Promise -type GroupCallback = (item: ManifestGroup) => Promise - -// this will recursively traverse the manifest -// if you return null it will filter out the item (and filter out groups that become empty) -const traverseManifest = async ( - manifest: Manifest, - itemCallback: ItemCallback = async (item) => item, - groupCallback: GroupCallback = async (item) => item -): Promise => { - const result = await Promise.all(manifest.map(async (navGroup) => { - return Promise.all(navGroup.map(async (item) => { +const writeSDKFile = async (sdk: SDK, filePath: string, contents: string) => { + await writeDistFile(path.join(sdk, filePath), contents) +} + +type BlankTree }> = Array>; + +const traverseTree = async < + Tree extends BlankTree, + InItem extends Extract, + InGroup extends Extract }>, + OutItem extends { href: string }, + OutGroup extends { items: BlankTree }, + OutTree extends BlankTree +>( + tree: Tree, + itemCallback: (item: InItem) => Promise = async (item) => item, + groupCallback: (group: InGroup) => Promise = async (group) => group +): Promise => { + const result = await Promise.all(tree.map(async (group) => { + return await Promise.all(group.map(async (item) => { if ('href' in item) { - return await itemCallback(item) + return await itemCallback(item); } if ('items' in item && Array.isArray(item.items)) { return await groupCallback({ ...item, - items: (await traverseManifest(item.items, itemCallback, groupCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) - }) + items: (await traverseTree(item.items, itemCallback, groupCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) + }); } - return item - })) - })) + return item as OutItem; + })); + })); - return result.map(group => group.filter((item): item is NonNullable => item !== null)) -} + return result.map(group => group.filter((item): item is NonNullable => item !== null)) as unknown as OutTree; +}; -const scopeItemToSDK = (item: Omit, itemSDK: undefined | SDK[], targetSDK: SDK): ManifestItem => { +const scopeHrefToSDK = (item: Omit, targetSDK: SDK) => { // This is external so can't change it - if (item.href.startsWith('/docs') === false) return item - - // This item is not scoped to a specific sdk, so leave it alone - if (itemSDK === undefined) return item + if (item.href.startsWith('/docs') === false) return item.href const hrefSegments = item.href.split('/') // This is a little hacky so we might change it // if the url already contains the sdk, we don't need to change it if (hrefSegments.includes(targetSDK)) { - return item + return item.href } // Add the sdk to the url - return { - ...item, - href: `/docs/${targetSDK}/${hrefSegments.slice(1).join('/')}` - } + return `/docs/${targetSDK}/${hrefSegments.slice(2).join('/')}` } const main = async () => { @@ -163,8 +174,10 @@ const main = async () => { const manifest = await readManifest() + const guides = new Map }>() + // This first pass goes through and grabs the sdk scoping out of the markdown files frontmatter - const fullManifest = await traverseManifest(manifest, + const fullManifest = await traverseTree(manifest, async (item) => { if (!item.href?.startsWith('/docs/')) return item @@ -172,49 +185,143 @@ const main = async () => { if (IGNORE.includes(item.href)) return item const fileContent = await readMarkdownFile(item.href) - const frontmatterSDK = parseFrontmatter(fileContent, 'sdk') - if (frontmatterSDK === null) return item + const slugify = slugifyWithCounter() + + const headingsHashs: Array = [] - const sdks = Array.isArray(frontmatterSDK) ? frontmatterSDK : [frontmatterSDK] + markdownProcessor() + .use(() => (tree) => { + visit(tree, + node => node.type === "heading", + node => { + const slug = slugify(toString(node).trim()) + headingsHashs.push(slug) + } + ) + }) + .process(fileContent) + + const frontmatter = parseFrontmatter<"name" | "description" | "sdk">(fileContent) + + if (frontmatter === undefined) { + throw new Error(`Frontmatter parsing failed for ${item.href}`) + } + + if (frontmatter.sdk === undefined) { + guides.set(item.href, { + ...item, + fileContent, + headingsHashs + }) + + return { + ...item, + fileContent, + frontmatter + } + } + + const sdks = frontmatter.sdk.split(', ') if (isValidSdks(sdks) === false) { throw new Error(`Invalid SDK ${JSON.stringify(sdks)} found in: ${item.href}`) } + guides.set(item.href, { + ...item, + sdk: sdks, + fileContent, + headingsHashs + }) + return { ...item, - sdk: sdks + sdk: sdks, + fileContent, + frontmatter } }, async (group) => { - const sdk = Array.from(new Set(group.items?.flatMap((item) => - item.flatMap((item) => item.sdk)))).filter((sdk): sdk is SDK => Boolean(sdk)) + const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter((sdk): sdk is SDK => sdk !== undefined) const { items, ...details } = group - if (sdk.length === 0) return { ...details, items } + + if (itemsSDKs.length === 0) return { ...details, items } + return { ...details, - sdk, + sdk: Array.from(new Set([...details.sdk ?? [], ...itemsSDKs])) ?? [], items } } ) + await traverseTree(fullManifest, + async (item) => { + if (item.sdk === undefined && "fileContent" in item) { + + let updatedFileContent: string | null = null + + markdownProcessor() + .use(() => (tree, vfile) => { + visit(tree, + node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), + node => { + if ("url" in node && typeof node.url === "string") { + const [url, hash] = node.url.split("#") + + const guide = guides.get(url) + + if (guide === undefined) { + throw new Error(`Guide not found for ${url} in ${item.href}`) + } + + if (hash !== undefined) { + const hasHash = guide.headingsHashs.includes(hash) + + if (hasHash === false) { + throw new Error(`Heading "${hash}" not found in ${url} linked from ${item.href.replace('/', '')}${node.position?.start.line ? `:${node.position?.start.line}` : ''}`) + } + } + + // update the links if they need to point to scoped hrefs + // I am thinking /docs/:sdk:/*.mdx then `clerk` can pick that up and put in the users current sdk + // but it needs to know what sdks it can fallback to + + } + } + ) + }).process(item.fileContent) + + if (updatedFileContent === null) { + throw new Error(`Frontmatter parsing failed for ${item.href}`) + } + + await writeDistFile(`${item.href.replace("/docs/", "")}.mdx`, updatedFileContent) + } + return null + }) + for (const targetSdk of VALID_SDKS) { // This second pass goes through and removes any items that are not scoped to the target sdk - const sdkSpecificManifest = await traverseManifest(fullManifest, + const sdkFilteredManifest = await traverseTree(fullManifest, async ({ sdk, ...item }) => { - // this means its generic, not scoped to a specific sdk, so we keep it - if (sdk === undefined) return item + // This means its generic, not scoped to a specific sdk, so we keep it + if (sdk === undefined) return { + ...item, + } - // this item is not scoped to the target sdk, so we remove it + // This item is not scoped to the target sdk, so we remove it if (sdk.includes(targetSdk) === false) return null - // this item is scoped to the target sdk, so we keep it - return scopeItemToSDK(item, sdk, targetSdk) + // This is a scoped item and its scoped to our target sdk + return { + ...item, + scopedHref: scopeHrefToSDK(item, targetSdk) + } }, async ({ sdk, ...group }) => { @@ -226,7 +333,60 @@ const main = async () => { } ) - await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation: sdkSpecificManifest })) + await traverseTree(sdkFilteredManifest, + async (item) => { + if ("fileContent" in item && "scopedHref" in item) { + + markdownProcessor() + .use(() => (tree, vfile) => { + visit(tree, + node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), + node => { + if ("url" in node && typeof node.url === "string") { + const [url, hash] = node.url.split("#") + + const guide = guides.get(url) + + if (guide === undefined) { + throw new Error(`Guide not found for ${url} in ${item.href}`) + } + + if (hash !== undefined) { + const hasHash = guide.headingsHashs.includes(hash) + + if (hasHash === false) { + throw new Error(`Heading "${hash}" not found in ${url} linked from ${item.href.replace('/', '')}${node.position?.start.line ? `:${node.position?.start.line}` : ''}`) + } + } + + // update the links if they need to point to scoped hrefs + // Should just be able to point to the targetSDK but need to look in to that + + } + } + ) + }).process(item.fileContent) + + + const filePath = `${item.href.replace("/docs/", "")}.mdx` + await writeSDKFile(targetSdk, filePath, item.fileContent) + } + return null + }) + + const navigation = await traverseTree(sdkFilteredManifest, + async (item) => { + // @ts-expect-error - simplest way to remove these properties + const { scopedHref, fileContent, frontmatter, ...details } = item + + return { + ...details, + href: scopedHref ?? details.href, + } + }, + ) + + await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) } } From 91b40db3481a18a6be5a21c7cd678ef44ae95277 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 18 Feb 2025 23:06:32 +0800 Subject: [PATCH 008/114] use the vfile reporter --- scripts/build-docs.ts | 159 ++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 74 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index c1fecffcbd..ceec8af7f2 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -7,12 +7,25 @@ import remarkFrontmatter from 'remark-frontmatter' import yaml from "yaml" import { slugifyWithCounter } from '@sindresorhus/slugify' import { toString } from 'mdast-util-to-string' +import reporter from 'vfile-reporter' const BASE_PATH = process.cwd() const MANIFEST_FILE_PATH = path.join(BASE_PATH, './docs/manifest.json') const DIST_PATH = path.join(BASE_PATH, './dist') const CLERK_PATH = path.join(BASE_PATH, "../clerk") -const IGNORE = ["/docs/core-1"] +const IGNORE = [ + "/docs/core-1", + '/pricing', + '/docs/reference/backend-api', + '/docs/reference/frontend-api', + '/support', + '/discord', + '/contact', + '/contact/sales', + '/contact/support', + '/blog', + '/changelog/2024-04-19', +] const VALID_SDKS = [ "nextjs", @@ -111,7 +124,6 @@ const writeDistFile = async (filePath: string, contents: string) => { const fullPath = path.join(DIST_PATH, filePath) await ensureDirectory(path.dirname(fullPath)) await fs.writeFile(fullPath, contents, { "encoding": "utf-8" }) - // console.log(`wrote ${fullPath}`) } const writeSDKFile = async (sdk: SDK, filePath: string, contents: string) => { @@ -152,17 +164,39 @@ const traverseTree = async < return result.map(group => group.filter((item): item is NonNullable => item !== null)) as unknown as OutTree; }; -const scopeHrefToSDK = (item: Omit, targetSDK: SDK) => { +function flattenTree< + Tree extends BlankTree, + InItem extends Extract, + InGroup extends Extract }> +>(tree: Tree): InItem[] { + const result: InItem[] = []; + + for (const group of tree) { + for (const itemOrGroup of group) { + if ("href" in itemOrGroup) { + // It's an item + result.push(itemOrGroup); + } else if ("items" in itemOrGroup && Array.isArray(itemOrGroup.items)) { + // It's a group with its own sub-tree, flatten it + result.push(...flattenTree(itemOrGroup.items)); + } + } + } + + return result; +} + +const scopeHrefToSDK = (href: string, targetSDK: SDK) => { // This is external so can't change it - if (item.href.startsWith('/docs') === false) return item.href + if (href.startsWith('/docs') === false) return href - const hrefSegments = item.href.split('/') + const hrefSegments = href.split('/') // This is a little hacky so we might change it // if the url already contains the sdk, we don't need to change it if (hrefSegments.includes(targetSDK)) { - return item.href + return href } // Add the sdk to the url @@ -182,7 +216,9 @@ const main = async () => { if (!item.href?.startsWith('/docs/')) return item if (item.target !== undefined) return item - if (IGNORE.includes(item.href)) return item + + const ignore = IGNORE.some((ignoreItem) => item.href.startsWith(ignoreItem)) + if (ignore === true) return item // even thou we are not processing them, we still need to keep them const fileContent = await readMarkdownFile(item.href) @@ -257,51 +293,58 @@ const main = async () => { } ) - await traverseTree(fullManifest, - async (item) => { - if (item.sdk === undefined && "fileContent" in item) { - - let updatedFileContent: string | null = null + const flatManifest = flattenTree(fullManifest) - markdownProcessor() - .use(() => (tree, vfile) => { - visit(tree, - node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), - node => { - if ("url" in node && typeof node.url === "string") { - const [url, hash] = node.url.split("#") + const vfiles = (await Promise.all(flatManifest.map(async (item) => { + if ("fileContent" in item) { - const guide = guides.get(url) + const vfile = await markdownProcessor() + .use(() => (tree, vfile) => { + visit(tree, + node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), + node => { + if ("url" in node && typeof node.url === "string") { + const [url, hash] = node.url.split("#") - if (guide === undefined) { - throw new Error(`Guide not found for ${url} in ${item.href}`) - } + const ignore = IGNORE.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return; - if (hash !== undefined) { - const hasHash = guide.headingsHashs.includes(hash) + const guide = guides.get(url) - if (hasHash === false) { - throw new Error(`Heading "${hash}" not found in ${url} linked from ${item.href.replace('/', '')}${node.position?.start.line ? `:${node.position?.start.line}` : ''}`) - } - } + if (guide === undefined) { + vfile.message(`Guide ${url} not found`, node.position) + return; + } - // update the links if they need to point to scoped hrefs - // I am thinking /docs/:sdk:/*.mdx then `clerk` can pick that up and put in the users current sdk - // but it needs to know what sdks it can fallback to + if (hash !== undefined) { + const hasHash = guide.headingsHashs.includes(hash) + if (hasHash === false) { + vfile.message(`Hash "${hash}" not found in ${url}`, node.position) + return; + } } } - ) - }).process(item.fileContent) - - if (updatedFileContent === null) { - throw new Error(`Frontmatter parsing failed for ${item.href}`) - } + } + ) + }).process({ + path: `${item.href.startsWith('/') ? item.href.slice(1) : item.href}.mdx`, + value: item.fileContent + }) - await writeDistFile(`${item.href.replace("/docs/", "")}.mdx`, updatedFileContent) + if (item.sdk === undefined) { + await writeDistFile(`${item.href.replace("/docs/", "")}.mdx`, item.fileContent) } - return null - }) + + return vfile + } + }))).filter((item): item is NonNullable => item !== undefined) + + const output = reporter(vfiles, { quiet: true }) + + if (output !== "") { + console.info(output) + } for (const targetSdk of VALID_SDKS) { @@ -320,7 +363,7 @@ const main = async () => { // This is a scoped item and its scoped to our target sdk return { ...item, - scopedHref: scopeHrefToSDK(item, targetSdk) + scopedHref: scopeHrefToSDK(item.href, targetSdk) } }, async ({ sdk, ...group }) => { @@ -336,38 +379,6 @@ const main = async () => { await traverseTree(sdkFilteredManifest, async (item) => { if ("fileContent" in item && "scopedHref" in item) { - - markdownProcessor() - .use(() => (tree, vfile) => { - visit(tree, - node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), - node => { - if ("url" in node && typeof node.url === "string") { - const [url, hash] = node.url.split("#") - - const guide = guides.get(url) - - if (guide === undefined) { - throw new Error(`Guide not found for ${url} in ${item.href}`) - } - - if (hash !== undefined) { - const hasHash = guide.headingsHashs.includes(hash) - - if (hasHash === false) { - throw new Error(`Heading "${hash}" not found in ${url} linked from ${item.href.replace('/', '')}${node.position?.start.line ? `:${node.position?.start.line}` : ''}`) - } - } - - // update the links if they need to point to scoped hrefs - // Should just be able to point to the targetSDK but need to look in to that - - } - } - ) - }).process(item.fileContent) - - const filePath = `${item.href.replace("/docs/", "")}.mdx` await writeSDKFile(targetSdk, filePath, item.fileContent) } From 95a886b0f9104b5e6f061a96c668e19cdd46b22e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 19 Feb 2025 03:24:16 +0800 Subject: [PATCH 009/114] Check for markdown files that can be found in /docs/ but not manifest.json --- .github/workflows/build.yml | 12 +++ scripts/build-docs.ts | 159 +++++++++++++++++++++++------------- 2 files changed, 115 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..5ad408a555 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,12 @@ +name: Build Docs + +on: + push: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: npm i + - run: npm run build diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index ceec8af7f2..3d880f5539 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1,3 +1,13 @@ +// Things this build script does +// - [x] Copies all "core" docs to the dist folder +// - [ ] Copies over Partials +// - [x] Duplicates out the sdk specific docs to their respective folders +// - [ ] stripping filtered content +// - [x] Checks that links (including hashes) between docs are valid +// - [x] Generates a manifest that is specific to each SDK +// - [x] Checks sdk key in frontmatter to ensure its valid +// - [x] Pares the markdown files, ensures they are valid + import fs from 'node:fs/promises' import path from 'node:path' import remarkMdx from 'remark-mdx' @@ -8,11 +18,13 @@ import yaml from "yaml" import { slugifyWithCounter } from '@sindresorhus/slugify' import { toString } from 'mdast-util-to-string' import reporter from 'vfile-reporter' +import readdirp from 'readdirp' const BASE_PATH = process.cwd() -const MANIFEST_FILE_PATH = path.join(BASE_PATH, './docs/manifest.json') +const DOCS_FOLDER = path.join(BASE_PATH, './docs') +const MANIFEST_FILE_PATH = path.join(DOCS_FOLDER, './manifest.json') const DIST_PATH = path.join(BASE_PATH, './dist') -const CLERK_PATH = path.join(BASE_PATH, "../clerk") +// const CLERK_PATH = path.join(BASE_PATH, "../clerk") const IGNORE = [ "/docs/core-1", '/pricing', @@ -25,6 +37,7 @@ const IGNORE = [ '/contact/support', '/blog', '/changelog/2024-04-19', + "/docs/_partials" ] const VALID_SDKS = [ @@ -87,6 +100,13 @@ const readMarkdownFile = async (docPath: string): Promise => { return fileContent } +const readInDocsFolder = () => { + return readdirp.promise(DOCS_FOLDER, { + type: 'files', + fileFilter: (entry) => IGNORE.some((ignoreItem) => `/docs/${entry.path}`.startsWith(ignoreItem)) === false && entry.path.endsWith('.mdx') + }) +} + const markdownProcessor = remark() .use(remarkFrontmatter) .use(remarkMdx) @@ -130,6 +150,13 @@ const writeSDKFile = async (sdk: SDK, filePath: string, contents: string) => { await writeDistFile(path.join(sdk, filePath), contents) } +const removeMdxSuffix = (filePath: string) => { + if (filePath.endsWith('.mdx')) { + return filePath.slice(0, -4) + } + return filePath +} + type BlankTree }> = Array>; const traverseTree = async < @@ -203,12 +230,63 @@ const scopeHrefToSDK = (href: string, targetSDK: SDK) => { return `/docs/${targetSDK}/${hrefSegments.slice(2).join('/')}` } +const parseInMarkdownFile = async (item: ManifestItem) => { + + const fileContent = await readMarkdownFile(item.href) + + const slugify = slugifyWithCounter() + + const headingsHashs: Array = [] + + markdownProcessor() + .use(() => (tree) => { + visit(tree, + node => node.type === "heading", + node => { + const slug = slugify(toString(node).trim()) + headingsHashs.push(slug) + } + ) + }) + .process(fileContent) + + const frontmatter = parseFrontmatter<"name" | "description" | "sdk">(fileContent) + + if (frontmatter === undefined) { + throw new Error(`Frontmatter parsing failed for ${item.href}`) + } + + if (frontmatter.sdk === undefined) { + return { + ...item, + fileContent, + headingsHashs, + frontmatter + } + } + + const sdks = frontmatter.sdk.split(', ') + + if (isValidSdks(sdks) === false) { + throw new Error(`Invalid SDK ${JSON.stringify(sdks)} found in: ${item.href}`) + } + + return { + ...item, + sdk: sdks, + fileContent, + headingsHashs, + frontmatter + } +} + const main = async () => { await ensureDirectory(DIST_PATH) const manifest = await readManifest() + const docsFiles = await readInDocsFolder() - const guides = new Map }>() + const guides = new Map, inManifest: boolean }>() // This first pass goes through and grabs the sdk scoping out of the markdown files frontmatter const fullManifest = await traverseTree(manifest, @@ -220,63 +298,15 @@ const main = async () => { const ignore = IGNORE.some((ignoreItem) => item.href.startsWith(ignoreItem)) if (ignore === true) return item // even thou we are not processing them, we still need to keep them - const fileContent = await readMarkdownFile(item.href) - - const slugify = slugifyWithCounter() - - const headingsHashs: Array = [] - - markdownProcessor() - .use(() => (tree) => { - visit(tree, - node => node.type === "heading", - node => { - const slug = slugify(toString(node).trim()) - headingsHashs.push(slug) - } - ) - }) - .process(fileContent) - - const frontmatter = parseFrontmatter<"name" | "description" | "sdk">(fileContent) - - if (frontmatter === undefined) { - throw new Error(`Frontmatter parsing failed for ${item.href}`) - } - - if (frontmatter.sdk === undefined) { - guides.set(item.href, { - ...item, - fileContent, - headingsHashs - }) - - return { - ...item, - fileContent, - frontmatter - } - } - - const sdks = frontmatter.sdk.split(', ') - - if (isValidSdks(sdks) === false) { - throw new Error(`Invalid SDK ${JSON.stringify(sdks)} found in: ${item.href}`) - } + const markdownFile = await parseInMarkdownFile(item) guides.set(item.href, { - ...item, - sdk: sdks, - fileContent, - headingsHashs + ...markdownFile, + inManifest: true }) - return { - ...item, - sdk: sdks, - fileContent, - frontmatter - } + return { ...markdownFile } as const + }, async (group) => { const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter((sdk): sdk is SDK => sdk !== undefined) @@ -293,6 +323,23 @@ const main = async () => { } ) + await Promise.all(docsFiles.map(async (file) => { + const href = removeMdxSuffix(`/docs/${file.path}`) + if (guides.has(href) === false) { + console.log(`Guide /docs/${file.path} not found in manifest`) + + const markdownFile = await parseInMarkdownFile({ + title: "Unknown Title (Not referenced in manifest)", + href + }) + + guides.set(href, { + ...markdownFile, + inManifest: false + }) + } + })) + const flatManifest = flattenTree(fullManifest) const vfiles = (await Promise.all(flatManifest.map(async (item) => { From c0165a4d30d615a89a3d4a5da7af239074587772 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 19 Feb 2025 03:39:23 +0800 Subject: [PATCH 010/114] Better error message for failure to read in markdown file references from manifest --- scripts/build-docs.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 3d880f5539..f4342c6e4a 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -94,10 +94,15 @@ const readManifest = async (): Promise => { return JSON.parse(manifest).navigation } -const readMarkdownFile = async (docPath: string): Promise => { +const readMarkdownFile = async (docPath: string) => { const filePath = path.join(BASE_PATH, `${docPath}.mdx`) - const fileContent = await fs.readFile(filePath, { "encoding": "utf-8" }) - return fileContent + + try { + const fileContent = await fs.readFile(filePath, { "encoding": "utf-8" }) + return [null, fileContent] as const + } catch (error) { + return [new Error(`file ${filePath} doesn't exist`, { cause: error }), null] as const + } } const readInDocsFolder = () => { @@ -232,7 +237,11 @@ const scopeHrefToSDK = (href: string, targetSDK: SDK) => { const parseInMarkdownFile = async (item: ManifestItem) => { - const fileContent = await readMarkdownFile(item.href) + const [error, fileContent] = await readMarkdownFile(item.href) + + if (error !== null) { + throw new Error(`Attempting to read in "${item.title}" from ${item.href}.mdx failed, with error message: ${error.message}`, { cause: error }) + } const slugify = slugifyWithCounter() From 118843621869a31beb2480b5b044b0fc7efd3f8e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 19 Feb 2025 04:37:05 +0800 Subject: [PATCH 011/114] Validate the manifest with zod --- package-lock.json | 13 ++++++++++- package.json | 3 ++- scripts/build-docs.ts | 51 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index dd3cbea7c2..3191fec190 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,8 @@ "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", - "yaml": "^2.7.0" + "yaml": "^2.7.0", + "zod": "^3.24.2" } }, "node_modules/@ampproject/remapping": { @@ -3230,6 +3231,16 @@ "node": ">=12" } }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 771bb09cba..60432ff576 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", - "yaml": "^2.7.0" + "yaml": "^2.7.0", + "zod": "^3.24.2" } } diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index f4342c6e4a..f443eecf66 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1,4 +1,5 @@ // Things this build script does +// - [x] Validates the Manifest // - [x] Copies all "core" docs to the dist folder // - [ ] Copies over Partials // - [x] Duplicates out the sdk specific docs to their respective folders @@ -19,6 +20,7 @@ import { slugifyWithCounter } from '@sindresorhus/slugify' import { toString } from 'mdast-util-to-string' import reporter from 'vfile-reporter' import readdirp from 'readdirp' +import { z } from "zod" const BASE_PATH = process.cwd() const DOCS_FOLDER = path.join(BASE_PATH, './docs') @@ -66,13 +68,11 @@ const VALID_SDKS = [ type SDK = typeof VALID_SDKS[number] -const isValidSdk = (sdk: string): sdk is SDK => { - return VALID_SDKS.includes(sdk as SDK) -} +const sdk = z.enum(VALID_SDKS) -const isValidSdks = (sdks: string[]): sdks is SDK[] => { - return sdks.every(isValidSdk) -} +const icon = z.enum(["apple", "application-2", "arrow-up-circle", "astro", "angular", "block", "bolt", "book", "box", "c-sharp", "chart", "checkmark-circle", "chrome", "clerk", "code-bracket", "cog-6-teeth", "door", "elysia", "expressjs", "globe", "go", "home", "hono", "javascript", "koa", "link", "linkedin", "lock", "nextjs", "nodejs", "plug", "plus-circle", "python", "react", "redwood", "remix", "react-router", "rocket", "route", "ruby", "rust", "speedometer", "stacked-rectangle", "solid", "svelte", "tanstack", "user-circle", "user-dotted-circle", "vue", "x", "expo", "nuxt", "fastify"]) + +const tag = z.enum(["(Beta)", "(Community)"]) type ManifestItem = { title: string @@ -81,17 +81,54 @@ type ManifestItem = { sdk?: SDK[] } +const manifestItem: z.ZodType = z.object({ + title: z.string(), + href: z.string(), + tag: tag.optional(), + wrap: z.boolean().default(true), + icon: icon.optional(), + target: z.enum(["_blank"]).optional() +}).strict() + type ManifestGroup = { title: string items: Manifest sdk?: SDK[] } +const manifestGroup: z.ZodType = z.object({ + title: z.string(), + items: z.lazy(() => manifestSchema), + collapse: z.boolean().default(false), + tag: tag.optional(), + wrap: z.boolean().default(true), + icon: icon.optional(), + hideTitle: z.boolean().default(false), + sdk: z.array(sdk).optional() +}).strict() + type Manifest = (ManifestItem | ManifestGroup)[][] +const manifestSchema: z.ZodType = z.array( + z.array( + z.union([ + manifestItem, + manifestGroup + ]) + ) +) + +const isValidSdk = (sdk: string): sdk is SDK => { + return VALID_SDKS.includes(sdk as SDK) +} + +const isValidSdks = (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk) +} + const readManifest = async (): Promise => { const manifest = await fs.readFile(MANIFEST_FILE_PATH, { "encoding": "utf-8" }) - return JSON.parse(manifest).navigation + return await manifestSchema.parseAsync(JSON.parse(manifest).navigation) } const readMarkdownFile = async (docPath: string) => { From d1ffa88e821a7308f7a20cf1062d9c54b926e8b0 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 19 Feb 2025 04:49:54 +0800 Subject: [PATCH 012/114] Start work on compiling in partials --- scripts/build-docs.ts | 52 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index f443eecf66..27fb586017 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1,7 +1,7 @@ // Things this build script does // - [x] Validates the Manifest // - [x] Copies all "core" docs to the dist folder -// - [ ] Copies over Partials +// - [ ] Compile partials in to docs // - [x] Duplicates out the sdk specific docs to their respective folders // - [ ] stripping filtered content // - [x] Checks that links (including hashes) between docs are valid @@ -23,8 +23,10 @@ import readdirp from 'readdirp' import { z } from "zod" const BASE_PATH = process.cwd() -const DOCS_FOLDER = path.join(BASE_PATH, './docs') +const DOCS_FOLDER_RELATIVE = './docs' +const DOCS_FOLDER = path.join(BASE_PATH, DOCS_FOLDER_RELATIVE) const MANIFEST_FILE_PATH = path.join(DOCS_FOLDER, './manifest.json') +const PARTIALS_PATH = './_partials' const DIST_PATH = path.join(BASE_PATH, './dist') // const CLERK_PATH = path.join(BASE_PATH, "../clerk") const IGNORE = [ @@ -132,7 +134,7 @@ const readManifest = async (): Promise => { } const readMarkdownFile = async (docPath: string) => { - const filePath = path.join(BASE_PATH, `${docPath}.mdx`) + const filePath = path.join(BASE_PATH, docPath) try { const fileContent = await fs.readFile(filePath, { "encoding": "utf-8" }) @@ -142,13 +144,37 @@ const readMarkdownFile = async (docPath: string) => { } } -const readInDocsFolder = () => { +const readDocsFolder = () => { return readdirp.promise(DOCS_FOLDER, { type: 'files', fileFilter: (entry) => IGNORE.some((ignoreItem) => `/docs/${entry.path}`.startsWith(ignoreItem)) === false && entry.path.endsWith('.mdx') }) } +const readPartialsFolder = () => { + return readdirp.promise(path.join(DOCS_FOLDER, './_partials'), { + type: 'files', + fileFilter: '*.mdx', + }) +} + +const readPartialsMarkdown = (paths: string[]) => { + return Promise.all(paths.map(async (markdownPath) => { + const fullPath = path.join(DOCS_FOLDER_RELATIVE, PARTIALS_PATH, markdownPath) + + const [error, content] = await readMarkdownFile(fullPath) + + if (error) { + throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) + } + + return { + path: markdownPath, + content, + } + })) +} + const markdownProcessor = remark() .use(remarkFrontmatter) .use(remarkMdx) @@ -272,9 +298,12 @@ const scopeHrefToSDK = (href: string, targetSDK: SDK) => { return `/docs/${targetSDK}/${hrefSegments.slice(2).join('/')}` } -const parseInMarkdownFile = async (item: ManifestItem) => { +const parseInMarkdownFile = async (item: ManifestItem, partials: { + path: string; + content: string; +}[]) => { - const [error, fileContent] = await readMarkdownFile(item.href) + const [error, fileContent] = await readMarkdownFile(`${item.href}.mdx`) if (error !== null) { throw new Error(`Attempting to read in "${item.title}" from ${item.href}.mdx failed, with error message: ${error.message}`, { cause: error }) @@ -330,7 +359,8 @@ const main = async () => { await ensureDirectory(DIST_PATH) const manifest = await readManifest() - const docsFiles = await readInDocsFolder() + const docsFiles = await readDocsFolder() + const partials = await readPartialsMarkdown((await readPartialsFolder()).map(item => item.path)) const guides = new Map, inManifest: boolean }>() @@ -344,7 +374,7 @@ const main = async () => { const ignore = IGNORE.some((ignoreItem) => item.href.startsWith(ignoreItem)) if (ignore === true) return item // even thou we are not processing them, we still need to keep them - const markdownFile = await parseInMarkdownFile(item) + const markdownFile = await parseInMarkdownFile(item, partials) guides.set(item.href, { ...markdownFile, @@ -377,12 +407,16 @@ const main = async () => { const markdownFile = await parseInMarkdownFile({ title: "Unknown Title (Not referenced in manifest)", href - }) + }, partials) guides.set(href, { ...markdownFile, inManifest: false }) + + if (markdownFile.sdk === undefined) { + await writeDistFile(`${markdownFile.href.replace("/docs/", "")}.mdx`, markdownFile.fileContent) + } } })) From 03351f6831271e23ab23fe23e70c0992d49c7208 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 20 Feb 2025 01:24:39 +0800 Subject: [PATCH 013/114] embed partials in to the markdown files we are generating --- scripts/build-docs.ts | 144 +++++++++++++++++++++++++++++++++++------- 1 file changed, 122 insertions(+), 22 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 27fb586017..9f26a06a32 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1,7 +1,7 @@ // Things this build script does // - [x] Validates the Manifest // - [x] Copies all "core" docs to the dist folder -// - [ ] Compile partials in to docs +// - [x] Compile partials in to docs // - [x] Duplicates out the sdk specific docs to their respective folders // - [ ] stripping filtered content // - [x] Checks that links (including hashes) between docs are valid @@ -74,11 +74,18 @@ const sdk = z.enum(VALID_SDKS) const icon = z.enum(["apple", "application-2", "arrow-up-circle", "astro", "angular", "block", "bolt", "book", "box", "c-sharp", "chart", "checkmark-circle", "chrome", "clerk", "code-bracket", "cog-6-teeth", "door", "elysia", "expressjs", "globe", "go", "home", "hono", "javascript", "koa", "link", "linkedin", "lock", "nextjs", "nodejs", "plug", "plus-circle", "python", "react", "redwood", "remix", "react-router", "rocket", "route", "ruby", "rust", "speedometer", "stacked-rectangle", "solid", "svelte", "tanstack", "user-circle", "user-dotted-circle", "vue", "x", "expo", "nuxt", "fastify"]) +type Icon = z.infer + const tag = z.enum(["(Beta)", "(Community)"]) +type Tag = z.infer + type ManifestItem = { title: string href: string + tag?: Tag + wrap?: boolean + icon?: Icon target?: '_blank' sdk?: SDK[] } @@ -89,12 +96,18 @@ const manifestItem: z.ZodType = z.object({ tag: tag.optional(), wrap: z.boolean().default(true), icon: icon.optional(), - target: z.enum(["_blank"]).optional() + target: z.enum(["_blank"]).optional(), + sdk: z.array(sdk).optional() }).strict() type ManifestGroup = { title: string items: Manifest + collapse?: boolean + tag?: Tag + wrap?: boolean + icon?: Icon + hideTitle?: boolean sdk?: SDK[] } @@ -180,10 +193,12 @@ const markdownProcessor = remark() .use(remarkMdx) .freeze() -const parseFrontmatter = (fileContent: string): Record | undefined => { +type VFile = Awaited> + +const parseFrontmatter = async (fileContent: string): Promise | undefined> => { let frontmatter: Record | undefined = undefined - markdownProcessor() + await markdownProcessor() .use(() => (tree, vfile) => { visit(tree, node => node.type === 'yaml' && "value" in node, @@ -309,11 +324,19 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { throw new Error(`Attempting to read in "${item.title}" from ${item.href}.mdx failed, with error message: ${error.message}`, { cause: error }) } + const frontmatter = await parseFrontmatter<"name" | "description" | "sdk">(fileContent) + + if (frontmatter === undefined) { + throw new Error(`Frontmatter parsing failed for ${item.href}`) + } + const slugify = slugifyWithCounter() const headingsHashs: Array = [] - markdownProcessor() + let editableFileContent = fileContent + + const fileWarnings = await markdownProcessor() .use(() => (tree) => { visit(tree, node => node.type === "heading", @@ -323,20 +346,89 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { } ) }) - .process(fileContent) + .use(() => (tree, vfile) => { + let offset = 0 - const frontmatter = parseFrontmatter<"name" | "description" | "sdk">(fileContent) + visit(tree, + node => node.type === "mdxJsxFlowElement" && "name" in node && node.name === "Include", + node => { - if (frontmatter === undefined) { - throw new Error(`Frontmatter parsing failed for ${item.href}`) - } + if (node.position === undefined) { + vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) + return; + } + + if (node.position.start.offset === undefined || node.position.end.offset === undefined) { + vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) + return; + } + + if (!("attributes" in node)) { + vfile.message(` node has no props`, node.position) + return; + } + + if (!Array.isArray(node.attributes)) { + vfile.message(` node attributes is not an array (this is a bug with the build script, please report)`, node.position) + return; + } + + const srcAttribute = node.attributes.find((attribute) => attribute.name === "src") + + if (srcAttribute === undefined) { + vfile.message(` node has no "src" attribute`, node.position) + return; + } + + const partialSrc = srcAttribute.value + + if (partialSrc === undefined) { + vfile.message(` attribute "src" has no value (this is a bug with the build script, please report)`, node.position) + return; + } + + if (typeof partialSrc !== "string") { + vfile.message(` attribute "src" is not a string`, node.position) + return; + } + + if (partialSrc.startsWith('_partials/') === false) { + vfile.message(` attribute "src" must start with "_partials/"`, node.position) + return; + } + + const partial = partials.find((partial) => `_partials/${partial.path}` === `${partialSrc.replace(/\.mdx$/, '')}.mdx`) + + if (partial === undefined) { + vfile.message(`Partial /docs/${partialSrc.replace(/\.mdx$/, '')}.mdx not found`, node.position) + return; + } + + // This takes the position offset of the and appends it to each line of the partial content + const tabbedPartial = partial.content.split('\n').map((line, index) => (index === 0 || line === "") ? line : `${" ".repeat(node.position?.start.column ? node.position?.start.column - 1 : 0)}${line}`).join('\n') + + // We must keep a record of the offset we adjust the file by, as the node.position doesn't update when we insert content. + editableFileContent = editableFileContent.slice(0, offset + node.position.start.offset) + tabbedPartial + editableFileContent.slice(offset + node.position.end.offset) + + offset += (tabbedPartial.length - (node.position.end.offset - node.position.start.offset)) + + } + ) + }) + .process({ + path: `${item.href}.mdx`, + value: fileContent + }) if (frontmatter.sdk === undefined) { return { - ...item, - fileContent, - headingsHashs, - frontmatter + file: { + ...item, + fileContent: editableFileContent, + headingsHashs, + frontmatter + }, + fileWarnings } } @@ -347,11 +439,14 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { } return { - ...item, - sdk: sdks, - fileContent, - headingsHashs, - frontmatter + file: { + ...item, + sdk: sdks, + fileContent: editableFileContent, + headingsHashs, + frontmatter + }, + fileWarnings } } @@ -363,6 +458,7 @@ const main = async () => { const partials = await readPartialsMarkdown((await readPartialsFolder()).map(item => item.path)) const guides = new Map, inManifest: boolean }>() + const markdownFileWarnings: VFile[] = [] // This first pass goes through and grabs the sdk scoping out of the markdown files frontmatter const fullManifest = await traverseTree(manifest, @@ -374,13 +470,15 @@ const main = async () => { const ignore = IGNORE.some((ignoreItem) => item.href.startsWith(ignoreItem)) if (ignore === true) return item // even thou we are not processing them, we still need to keep them - const markdownFile = await parseInMarkdownFile(item, partials) + const { file: markdownFile, fileWarnings } = await parseInMarkdownFile(item, partials) guides.set(item.href, { ...markdownFile, inManifest: true }) + markdownFileWarnings.push(fileWarnings) + return { ...markdownFile } as const }, @@ -404,7 +502,7 @@ const main = async () => { if (guides.has(href) === false) { console.log(`Guide /docs/${file.path} not found in manifest`) - const markdownFile = await parseInMarkdownFile({ + const { file: markdownFile, fileWarnings } = await parseInMarkdownFile({ title: "Unknown Title (Not referenced in manifest)", href }, partials) @@ -414,6 +512,8 @@ const main = async () => { inManifest: false }) + markdownFileWarnings.push(fileWarnings) + if (markdownFile.sdk === undefined) { await writeDistFile(`${markdownFile.href.replace("/docs/", "")}.mdx`, markdownFile.fileContent) } @@ -467,7 +567,7 @@ const main = async () => { } }))).filter((item): item is NonNullable => item !== undefined) - const output = reporter(vfiles, { quiet: true }) + const output = reporter([...vfiles, ...markdownFileWarnings], { quiet: true }) if (output !== "") { console.info(output) From 4fab404a5620ee727cedb0ff856cd60da2f8b288 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 20 Feb 2025 01:39:47 +0800 Subject: [PATCH 014/114] catch partials inside partials --- scripts/build-docs.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 9f26a06a32..9613200b63 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -397,13 +397,33 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { return; } - const partial = partials.find((partial) => `_partials/${partial.path}` === `${partialSrc.replace(/\.mdx$/, '')}.mdx`) + const partial = partials.find((partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`) if (partial === undefined) { - vfile.message(`Partial /docs/${partialSrc.replace(/\.mdx$/, '')}.mdx not found`, node.position) + vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) return; } + const partialContentVFile = markdownProcessor() + .use(() => (tree, vfile) => { + visit(tree, + node => node.type === "mdxJsxFlowElement" && "name" in node && node.name === "Include", + () => { + vfile.fail("Partials inside of partials is not yet supported, please report if you are seeing this error", node.position) + } + ) + }) + .processSync({ + path: partial.path, + value: partial.content + }) + + const partialContentReport = reporter([partialContentVFile], { quiet: true }) + + if (partialContentReport !== "") { + console.error(partialContentReport) + } + // This takes the position offset of the and appends it to each line of the partial content const tabbedPartial = partial.content.split('\n').map((line, index) => (index === 0 || line === "") ? line : `${" ".repeat(node.position?.start.column ? node.position?.start.column - 1 : 0)}${line}`).join('\n') From 9d7841aea13837861191415b496040ded9824d72 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 20 Feb 2025 20:27:11 +0800 Subject: [PATCH 015/114] Filter out content for different sdks --- package-lock.json | 28 +++++ package.json | 2 + scripts/build-docs.ts | 261 +++++++++++++++++++++++++++++++++++------- 3 files changed, 248 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3191fec190..1af468f82a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,8 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", + "unist-util-filter": "^5.0.1", + "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", @@ -2901,6 +2903,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-filter": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz", + "integrity": "sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -2914,6 +2928,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-map/-/unist-util-map-4.0.0.tgz", + "integrity": "sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-position-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", diff --git a/package.json b/package.json index 60432ff576..df50deaef1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", + "unist-util-filter": "^5.0.1", + "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 9613200b63..147634f83c 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -3,17 +3,21 @@ // - [x] Copies all "core" docs to the dist folder // - [x] Compile partials in to docs // - [x] Duplicates out the sdk specific docs to their respective folders -// - [ ] stripping filtered content +// - [x] stripping filtered out content // - [x] Checks that links (including hashes) between docs are valid // - [x] Generates a manifest that is specific to each SDK // - [x] Checks sdk key in frontmatter to ensure its valid // - [x] Pares the markdown files, ensures they are valid +// - [ ] Updates the links in the content to point to the sdk specific docs +// - [x] Checks that filters used in are available sdks defined by the frontmatter sdk (if the frontmatter sdk is set) import fs from 'node:fs/promises' import path from 'node:path' import remarkMdx from 'remark-mdx' import { remark } from 'remark' -import { visit } from 'unist-util-visit' +import { visit as mdastVisit } from 'unist-util-visit' +import { filter as mdastFilter } from 'unist-util-filter' +import { map as mdastMap } from 'unist-util-map' import remarkFrontmatter from 'remark-frontmatter' import yaml from "yaml" import { slugifyWithCounter } from '@sindresorhus/slugify' @@ -21,6 +25,7 @@ import { toString } from 'mdast-util-to-string' import reporter from 'vfile-reporter' import readdirp from 'readdirp' import { z } from "zod" +import { Node } from 'unist' const BASE_PATH = process.cwd() const DOCS_FOLDER_RELATIVE = './docs' @@ -200,7 +205,7 @@ const parseFrontmatter = async (fileContent: string): Promi await markdownProcessor() .use(() => (tree, vfile) => { - visit(tree, + mdastVisit(tree, node => node.type === 'yaml' && "value" in node, node => { if (!("value" in node)) return; @@ -331,14 +336,11 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { } const slugify = slugifyWithCounter() - const headingsHashs: Array = [] - let editableFileContent = fileContent - const fileWarnings = await markdownProcessor() .use(() => (tree) => { - visit(tree, + mdastVisit(tree, node => node.type === "heading", node => { const slug = slugify(toString(node).trim()) @@ -347,71 +349,84 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { ) }) .use(() => (tree, vfile) => { - let offset = 0 - - visit(tree, - node => node.type === "mdxJsxFlowElement" && "name" in node && node.name === "Include", + return mdastMap(tree, node => { + if (node.type !== "mdxJsxFlowElement") { + return node + } + + if (!("name" in node)) { + return node + } + + if (node.name !== "Include") { + return node + } + if (node.position === undefined) { vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) - return; + return node } if (node.position.start.offset === undefined || node.position.end.offset === undefined) { vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) - return; + return node } if (!("attributes" in node)) { - vfile.message(` node has no props`, node.position) - return; + vfile.message(` component has no props`, node.position) + return node } if (!Array.isArray(node.attributes)) { vfile.message(` node attributes is not an array (this is a bug with the build script, please report)`, node.position) - return; + return node } const srcAttribute = node.attributes.find((attribute) => attribute.name === "src") if (srcAttribute === undefined) { - vfile.message(` node has no "src" attribute`, node.position) - return; + vfile.message(` component has no "src" attribute`, node.position) + return node } const partialSrc = srcAttribute.value if (partialSrc === undefined) { vfile.message(` attribute "src" has no value (this is a bug with the build script, please report)`, node.position) - return; + return node } if (typeof partialSrc !== "string") { - vfile.message(` attribute "src" is not a string`, node.position) - return; + vfile.message(` prop "src" is not a string`, node.position) + return node } if (partialSrc.startsWith('_partials/') === false) { - vfile.message(` attribute "src" must start with "_partials/"`, node.position) - return; + vfile.message(` prop "src" must start with "_partials/"`, node.position) + return node } const partial = partials.find((partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`) if (partial === undefined) { vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) - return; + return node } + let partialNode: Node | null = null + const partialContentVFile = markdownProcessor() .use(() => (tree, vfile) => { - visit(tree, + mdastVisit(tree, node => node.type === "mdxJsxFlowElement" && "name" in node && node.name === "Include", () => { vfile.fail("Partials inside of partials is not yet supported, please report if you are seeing this error", node.position) } ) + + partialNode = tree }) .processSync({ path: partial.path, @@ -424,13 +439,12 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { console.error(partialContentReport) } - // This takes the position offset of the and appends it to each line of the partial content - const tabbedPartial = partial.content.split('\n').map((line, index) => (index === 0 || line === "") ? line : `${" ".repeat(node.position?.start.column ? node.position?.start.column - 1 : 0)}${line}`).join('\n') - - // We must keep a record of the offset we adjust the file by, as the node.position doesn't update when we insert content. - editableFileContent = editableFileContent.slice(0, offset + node.position.start.offset) + tabbedPartial + editableFileContent.slice(offset + node.position.end.offset) + if (partialNode === null) { + vfile.fail(`Failed to parse the content of ${partial.path}`, node.position) + return node + } - offset += (tabbedPartial.length - (node.position.end.offset - node.position.start.offset)) + return partialNode } ) @@ -444,7 +458,7 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { return { file: { ...item, - fileContent: editableFileContent, + fileContent: String(fileWarnings), headingsHashs, frontmatter }, @@ -455,14 +469,15 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { const sdks = frontmatter.sdk.split(', ') if (isValidSdks(sdks) === false) { - throw new Error(`Invalid SDK ${JSON.stringify(sdks)} found in: ${item.href}`) + const invalidSDKs = sdks.filter(sdk => isValidSdk(sdk) === false) + throw new Error(`Invalid SDK ${JSON.stringify(invalidSDKs)} found in: ${item.href}`) } return { file: { ...item, sdk: sdks, - fileContent: editableFileContent, + fileContent: String(fileWarnings), headingsHashs, frontmatter }, @@ -547,7 +562,7 @@ const main = async () => { const vfile = await markdownProcessor() .use(() => (tree, vfile) => { - visit(tree, + mdastVisit(tree, node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), node => { if ("url" in node && typeof node.url === "string") { @@ -587,11 +602,7 @@ const main = async () => { } }))).filter((item): item is NonNullable => item !== undefined) - const output = reporter([...vfiles, ...markdownFileWarnings], { quiet: true }) - - if (output !== "") { - console.info(output) - } + const sdkSpecificMarkdownFileWarnings: VFile[] = [] for (const targetSdk of VALID_SDKS) { @@ -623,19 +634,175 @@ const main = async () => { } ) + // Here we are filtering out content for different sdks, and updating links to make them scoped to the sdk when necessary await traverseTree(sdkFilteredManifest, async (item) => { - if ("fileContent" in item && "scopedHref" in item) { + if ("fileContent" in item) { const filePath = `${item.href.replace("/docs/", "")}.mdx` - await writeSDKFile(targetSdk, filePath, item.fileContent) + + const vfile = await markdownProcessor() + .use(() => (tree, vfile) => { + return mdastFilter(tree, + node => { + + if (node.type !== "mdxJsxFlowElement") { + return true + } + + if (!("name" in node)) { + return true + } + + if (node.name !== "If") { + return true + } + + if (node.position === undefined) { + vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) + return true + } + + if (node.position.start.offset === undefined || node.position.end.offset === undefined) { + vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) + return true + } + + if (!("attributes" in node)) { + vfile.message(` component has no props`, node.position) + return true + } + + if (!Array.isArray(node.attributes)) { + vfile.message(` node attributes is not an array (this is a bug with the build script, please report)`, node.position) + return true + } + + const sdkAttribute = node.attributes.find((attribute) => attribute.name === "sdk") + + if (sdkAttribute === undefined) { + vfile.message(` component has no "sdk" attribute`, node.position) + return true + } + + const sdk = sdkAttribute.value + + if (sdk === undefined) { + vfile.message(` attribute "sdk" has no value (this is a bug with the build script, please report)`, node.position) + return true + } + + const sdks = (() => { + + if (typeof sdk === "string") { + if (isValidSdk(sdk)) { + return [sdk] + } else { + vfile.message(`sdk "${sdk}" in component is not a valid SDK`, node.position) + } + } + + else if (typeof sdk === "object") { + const sdks = JSON.parse(sdk.value) + if (isValidSdks(sdks)) { + return sdks + } else { + vfile.message(`sdks "${sdk.value}" in are not valid all SDKs`, node.position) + } + } + + })() + + if (sdks === undefined) { + vfile.message(`SDKs not found in (this is a bug with the build script, please report)`, node.position) + return true + } + + if (sdks.length === 0) { + vfile.message(`No SDKs found in `, node.position) + return true + } + + console.log({ + sdks, + targetSdk, + href: item.href, + }) + + if (sdks.includes(targetSdk)) { + + const guide = guides.get(item.href) + + if (guide === undefined) { + vfile.fail(`Guide not found for ${item.href}, (this is a bug, please report it)`, node.position) + return; + } + + console.log(guide.sdk) + + if (guide.sdk === undefined) { + vfile.fail(`Guide "${guide.title}" (${item.href}) is generic to all sdks, but we are doing checks in a sdk specific context, (this is a bug, please report it)`, node.position) + return true + } + + sdks.forEach(sdk => { + if (guide.sdk === undefined) { + vfile.fail('Guide.sdk is undefined, (this is a bug, please report it)', node.position) + return; + } + + const available = guide.sdk.includes(sdk) + + if (available === false) { + vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${guide.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) + } + + }) + + return true + } + + return false + + } + ) + }) + // .use(() => (tree, vfile) => { + // let offset = 0 + + // visit(tree, + // node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), + // node => { + + // if (!("url" in node)) { + + // } + + // console.log(node) + // } + // ) + // }) + .process({ + path: filePath, + value: item.fileContent + }) + + sdkSpecificMarkdownFileWarnings.push(vfile) + + await writeSDKFile(targetSdk, filePath, String(vfile)) } return null }) + // const report = reporter(markdownFileWarnings, { quiet: true }) + + // if (report !== "") { + // console.info(report) + // } + const navigation = await traverseTree(sdkFilteredManifest, async (item) => { // @ts-expect-error - simplest way to remove these properties - const { scopedHref, fileContent, frontmatter, ...details } = item + const { scopedHref, fileContent, frontmatter, headingsHashs, ...details } = item return { ...details, @@ -646,6 +813,14 @@ const main = async () => { await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) } + + const output = reporter([...vfiles, ...markdownFileWarnings, ...sdkSpecificMarkdownFileWarnings], { quiet: true }) + + if (output !== "") { + console.info(output) + } + + } main() \ No newline at end of file From 739613ffe51c19e3ff3ebbc3bdec441a8cd1bf68 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 20 Feb 2025 21:00:35 +0800 Subject: [PATCH 016/114] split up linting from generation to ensure full linting file coverage --- scripts/build-docs.ts | 325 +++++++++++++++++++----------------------- 1 file changed, 149 insertions(+), 176 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 147634f83c..34423b6522 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -318,6 +318,115 @@ const scopeHrefToSDK = (href: string, targetSDK: SDK) => { return `/docs/${targetSDK}/${hrefSegments.slice(2).join('/')}` } +const extractComponentPropValueFromNode = ( + node: Node, + vfile: VFile, + componentName: string, + propName: string +): string | undefined => { + // Check if it's an MDX component + if (node.type !== "mdxJsxFlowElement") { + return undefined; + } + + // Check if it's the correct component + if (!("name" in node) || node.name !== componentName) { + return undefined; + } + + // Validate node position for error reporting + if (node.position === undefined) { + vfile.message( + `<${componentName} /> node has no position (this is a bug with the build script, please report)`, + node.position + ); + return undefined; + } + + if ( + node.position.start.offset === undefined || + node.position.end.offset === undefined + ) { + vfile.message( + `<${componentName} /> node has no position offsets (this is a bug with the build script, please report)`, + node.position + ); + return undefined; + } + + // Check for attributes + if (!("attributes" in node)) { + vfile.message( + `<${componentName} /> component has no props`, + node.position + ); + return undefined; + } + + if (!Array.isArray(node.attributes)) { + vfile.message( + `<${componentName} /> node attributes is not an array (this is a bug with the build script, please report)`, + node.position + ); + return undefined; + } + + // Find the requested prop + const propAttribute = node.attributes.find( + (attribute) => attribute.name === propName + ); + + if (propAttribute === undefined) { + vfile.message( + `<${componentName} /> component has no "${propName}" attribute`, + node.position + ); + return undefined; + } + + const value = propAttribute.value; + + if (value === undefined) { + vfile.message( + `<${componentName} /> attribute "${propName}" has no value (this is a bug with the build script, please report)`, + node.position + ); + return undefined; + } + + // Handle both string values and object values (like JSX expressions) + if (typeof value === "string") { + return value; + } else if (typeof value === "object" && "value" in value) { + return value.value; + } + + vfile.message( + `<${componentName} /> attribute "${propName}" has an unsupported value type`, + node.position + ); + return undefined; +} + +const extractSDKsFromIfProp = (node: Node, vfile: VFile, sdkProp: string) => { + if (sdkProp.includes(', ')) { + const sdks = sdkProp.split(', ') + if (isValidSdks(sdks)) { + return sdks + } else { + const invalidSDKs = sdks.filter(sdk => !isValidSdk(sdk)) + vfile.message(`sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, node.position) + } + } else { + if (isValidSdk(sdkProp)) { + return [sdkProp] + } else { + vfile.message(`sdk "${sdkProp}" in is not a valid SDK`, node.position) + } + } + +} + const parseInMarkdownFile = async (item: ManifestItem, partials: { path: string; content: string; @@ -335,6 +444,13 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { throw new Error(`Frontmatter parsing failed for ${item.href}`) } + const frontmatterSDKs = frontmatter.sdk?.split(', ') + + if (frontmatterSDKs !== undefined && isValidSdks(frontmatterSDKs) === false) { + const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(sdk) === false) + throw new Error(`Invalid SDK ${JSON.stringify(invalidSDKs)} found in: ${item.href}`) + } + const slugify = slugifyWithCounter() const headingsHashs: Array = [] @@ -352,54 +468,9 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { return mdastMap(tree, node => { - if (node.type !== "mdxJsxFlowElement") { - return node - } - - if (!("name" in node)) { - return node - } - - if (node.name !== "Include") { - return node - } - - if (node.position === undefined) { - vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) - return node - } - - if (node.position.start.offset === undefined || node.position.end.offset === undefined) { - vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) - return node - } - - if (!("attributes" in node)) { - vfile.message(` component has no props`, node.position) - return node - } - - if (!Array.isArray(node.attributes)) { - vfile.message(` node attributes is not an array (this is a bug with the build script, please report)`, node.position) - return node - } - - const srcAttribute = node.attributes.find((attribute) => attribute.name === "src") - - if (srcAttribute === undefined) { - vfile.message(` component has no "src" attribute`, node.position) - return node - } - - const partialSrc = srcAttribute.value + const partialSrc = extractComponentPropValueFromNode(tree, vfile, "Include", "src") if (partialSrc === undefined) { - vfile.message(` attribute "src" has no value (this is a bug with the build script, please report)`, node.position) - return node - } - - if (typeof partialSrc !== "string") { - vfile.message(` prop "src" is not a string`, node.position) return node } @@ -449,34 +520,41 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { } ) }) + .use(() => (tree, vfile) => { + + // We are only checking files that have opted in to sdk filtering by frontmatter + if (frontmatterSDKs === undefined) return; + + mdastVisit(tree, + node => { + const sdk = extractComponentPropValueFromNode(node, vfile, "If", "sdk") + + if (sdk === undefined) return; + + const sdksFilter = extractSDKsFromIfProp(node, vfile, sdk) + + if (sdksFilter === undefined) return + + sdksFilter.forEach(sdk => { + const available = frontmatterSDKs.includes(sdk) + + if (available === false) { + vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${frontmatterSDKs.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) + } + + }) + } + ) + }) .process({ path: `${item.href}.mdx`, value: fileContent }) - if (frontmatter.sdk === undefined) { - return { - file: { - ...item, - fileContent: String(fileWarnings), - headingsHashs, - frontmatter - }, - fileWarnings - } - } - - const sdks = frontmatter.sdk.split(', ') - - if (isValidSdks(sdks) === false) { - const invalidSDKs = sdks.filter(sdk => isValidSdk(sdk) === false) - throw new Error(`Invalid SDK ${JSON.stringify(invalidSDKs)} found in: ${item.href}`) - } - return { file: { ...item, - sdk: sdks, + sdk: frontmatterSDKs, fileContent: String(fileWarnings), headingsHashs, frontmatter @@ -644,120 +722,15 @@ const main = async () => { .use(() => (tree, vfile) => { return mdastFilter(tree, node => { + const sdk = extractComponentPropValueFromNode(node, vfile, "If", "sdk") - if (node.type !== "mdxJsxFlowElement") { - return true - } - - if (!("name" in node)) { - return true - } - - if (node.name !== "If") { - return true - } - - if (node.position === undefined) { - vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) - return true - } - - if (node.position.start.offset === undefined || node.position.end.offset === undefined) { - vfile.message(` node has no position (this is a bug with the build script, please report)`, node.position) - return true - } - - if (!("attributes" in node)) { - vfile.message(` component has no props`, node.position) - return true - } - - if (!Array.isArray(node.attributes)) { - vfile.message(` node attributes is not an array (this is a bug with the build script, please report)`, node.position) - return true - } - - const sdkAttribute = node.attributes.find((attribute) => attribute.name === "sdk") - - if (sdkAttribute === undefined) { - vfile.message(` component has no "sdk" attribute`, node.position) - return true - } - - const sdk = sdkAttribute.value - - if (sdk === undefined) { - vfile.message(` attribute "sdk" has no value (this is a bug with the build script, please report)`, node.position) - return true - } - - const sdks = (() => { - - if (typeof sdk === "string") { - if (isValidSdk(sdk)) { - return [sdk] - } else { - vfile.message(`sdk "${sdk}" in component is not a valid SDK`, node.position) - } - } - - else if (typeof sdk === "object") { - const sdks = JSON.parse(sdk.value) - if (isValidSdks(sdks)) { - return sdks - } else { - vfile.message(`sdks "${sdk.value}" in are not valid all SDKs`, node.position) - } - } - - })() - - if (sdks === undefined) { - vfile.message(`SDKs not found in (this is a bug with the build script, please report)`, node.position) - return true - } - - if (sdks.length === 0) { - vfile.message(`No SDKs found in `, node.position) - return true - } - - console.log({ - sdks, - targetSdk, - href: item.href, - }) - - if (sdks.includes(targetSdk)) { - - const guide = guides.get(item.href) - - if (guide === undefined) { - vfile.fail(`Guide not found for ${item.href}, (this is a bug, please report it)`, node.position) - return; - } - - console.log(guide.sdk) - - if (guide.sdk === undefined) { - vfile.fail(`Guide "${guide.title}" (${item.href}) is generic to all sdks, but we are doing checks in a sdk specific context, (this is a bug, please report it)`, node.position) - return true - } - - sdks.forEach(sdk => { - if (guide.sdk === undefined) { - vfile.fail('Guide.sdk is undefined, (this is a bug, please report it)', node.position) - return; - } - - const available = guide.sdk.includes(sdk) + if (sdk === undefined) return; - if (available === false) { - vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${guide.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) - } + const sdksFilter = extractSDKsFromIfProp(node, vfile, sdk) - }) + if (sdksFilter === undefined) return + if (sdksFilter.includes(targetSdk)) { return true } From f7d1ef589520c7dcbc53c2833ded377009575687 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 21 Feb 2025 03:16:32 +0800 Subject: [PATCH 017/114] refactor for efficiency 25s down to 8s --- package-lock.json | 16 +- package.json | 3 +- scripts/build-docs.ts | 475 +++++++++++++++++++++--------------------- 3 files changed, 255 insertions(+), 239 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1af468f82a..a2a2a0b6da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,8 @@ "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", "yaml": "^2.7.0", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zod-validation-error": "^3.4.0" } }, "node_modules/@ampproject/remapping": { @@ -3269,6 +3270,19 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index df50deaef1..f8eb0576c5 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", "yaml": "^2.7.0", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zod-validation-error": "^3.4.0" } } diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 34423b6522..a331947334 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1,15 +1,18 @@ // Things this build script does -// - [x] Validates the Manifest -// - [x] Copies all "core" docs to the dist folder -// - [x] Compile partials in to docs -// - [x] Duplicates out the sdk specific docs to their respective folders -// - [x] stripping filtered out content -// - [x] Checks that links (including hashes) between docs are valid + +// - [x] Validates the manifest +// - [x] Validates the markdown files contents +// - [x] Validates links (including hashes) between docs are valid +// - [x] Validates the sdk filtering in the manifest +// - [x] Validates the sdk filtering in the frontmatter +// - [x] Validates the sdk filtering in the component + +// - [x] Embeds the includes in the markdown files +// - [x] Copies over "core" docs to the dist folder +// - [ ] Updates the links in the content if they point to the sdk specific docs // - [x] Generates a manifest that is specific to each SDK -// - [x] Checks sdk key in frontmatter to ensure its valid -// - [x] Pares the markdown files, ensures they are valid -// - [ ] Updates the links in the content to point to the sdk specific docs -// - [x] Checks that filters used in are available sdks defined by the frontmatter sdk (if the frontmatter sdk is set) +// - [x] Duplicates out the sdk specific docs to their respective folders +// - [x] stripping filtered out content import fs from 'node:fs/promises' import path from 'node:path' @@ -25,6 +28,7 @@ import { toString } from 'mdast-util-to-string' import reporter from 'vfile-reporter' import readdirp from 'readdirp' import { z } from "zod" +import { fromError } from 'zod-validation-error'; import { Node } from 'unist' const BASE_PATH = process.cwd() @@ -138,6 +142,8 @@ const manifestSchema: z.ZodType = z.array( ) ) +const pleaseReport = "(this is a bug with the build script, please report)" + const isValidSdk = (sdk: string): sdk is SDK => { return VALID_SDKS.includes(sdk as SDK) } @@ -147,8 +153,15 @@ const isValidSdks = (sdks: string[]): sdks is SDK[] => { } const readManifest = async (): Promise => { - const manifest = await fs.readFile(MANIFEST_FILE_PATH, { "encoding": "utf-8" }) - return await manifestSchema.parseAsync(JSON.parse(manifest).navigation) + const unsafe_manifest = await fs.readFile(MANIFEST_FILE_PATH, { "encoding": "utf-8" }) + + const manifest = await manifestSchema.safeParseAsync(JSON.parse(unsafe_manifest).navigation) + + if (manifest.success === true) { + return manifest.data + } + + throw new Error(`Failed to parse manifest: ${fromError(manifest.error)}`) } const readMarkdownFile = async (docPath: string) => { @@ -279,28 +292,6 @@ const traverseTree = async < return result.map(group => group.filter((item): item is NonNullable => item !== null)) as unknown as OutTree; }; -function flattenTree< - Tree extends BlankTree, - InItem extends Extract, - InGroup extends Extract }> ->(tree: Tree): InItem[] { - const result: InItem[] = []; - - for (const group of tree) { - for (const itemOrGroup of group) { - if ("href" in itemOrGroup) { - // It's an item - result.push(itemOrGroup); - } else if ("items" in itemOrGroup && Array.isArray(itemOrGroup.items)) { - // It's a group with its own sub-tree, flatten it - result.push(...flattenTree(itemOrGroup.items)); - } - } - } - - return result; -} - const scopeHrefToSDK = (href: string, targetSDK: SDK) => { // This is external so can't change it @@ -320,43 +311,23 @@ const scopeHrefToSDK = (href: string, targetSDK: SDK) => { const extractComponentPropValueFromNode = ( node: Node, - vfile: VFile, + vfile: VFile | undefined, componentName: string, - propName: string + propName: string, ): string | undefined => { + // Check if it's an MDX component if (node.type !== "mdxJsxFlowElement") { return undefined; } // Check if it's the correct component - if (!("name" in node) || node.name !== componentName) { - return undefined; - } - - // Validate node position for error reporting - if (node.position === undefined) { - vfile.message( - `<${componentName} /> node has no position (this is a bug with the build script, please report)`, - node.position - ); - return undefined; - } - - if ( - node.position.start.offset === undefined || - node.position.end.offset === undefined - ) { - vfile.message( - `<${componentName} /> node has no position offsets (this is a bug with the build script, please report)`, - node.position - ); - return undefined; - } + if (!("name" in node)) return undefined; + if (node.name !== componentName) return undefined; // Check for attributes if (!("attributes" in node)) { - vfile.message( + vfile?.message( `<${componentName} /> component has no props`, node.position ); @@ -364,7 +335,7 @@ const extractComponentPropValueFromNode = ( } if (!Array.isArray(node.attributes)) { - vfile.message( + vfile?.message( `<${componentName} /> node attributes is not an array (this is a bug with the build script, please report)`, node.position ); @@ -377,7 +348,7 @@ const extractComponentPropValueFromNode = ( ); if (propAttribute === undefined) { - vfile.message( + vfile?.message( `<${componentName} /> component has no "${propName}" attribute`, node.position ); @@ -387,7 +358,7 @@ const extractComponentPropValueFromNode = ( const value = propAttribute.value; if (value === undefined) { - vfile.message( + vfile?.message( `<${componentName} /> attribute "${propName}" has no value (this is a bug with the build script, please report)`, node.position ); @@ -401,74 +372,70 @@ const extractComponentPropValueFromNode = ( return value.value; } - vfile.message( + vfile?.message( `<${componentName} /> attribute "${propName}" has an unsupported value type`, node.position ); return undefined; } -const extractSDKsFromIfProp = (node: Node, vfile: VFile, sdkProp: string) => { - if (sdkProp.includes(', ')) { - const sdks = sdkProp.split(', ') +const extractSDKsFromIfProp = (node: Node, vfile: VFile | undefined, sdkProp: string) => { + if (sdkProp.includes('", "')) { + const sdks = JSON.parse(sdkProp) if (isValidSdks(sdks)) { return sdks } else { const invalidSDKs = sdks.filter(sdk => !isValidSdk(sdk)) - vfile.message(`sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, node.position) + vfile?.message(`sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, node.position) } } else { if (isValidSdk(sdkProp)) { return [sdkProp] } else { - vfile.message(`sdk "${sdkProp}" in is not a valid SDK`, node.position) + vfile?.message(`sdk "${sdkProp}" in is not a valid SDK`, node.position) } } } -const parseInMarkdownFile = async (item: ManifestItem, partials: { +const parseInMarkdownFile = async (href: string, partials: { path: string; content: string; -}[]) => { - - const [error, fileContent] = await readMarkdownFile(`${item.href}.mdx`) +}[], inManifest: boolean) => { + const [error, fileContent] = await readMarkdownFile(`${href}.mdx`) if (error !== null) { - throw new Error(`Attempting to read in "${item.title}" from ${item.href}.mdx failed, with error message: ${error.message}`, { cause: error }) + throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { cause: error }) } const frontmatter = await parseFrontmatter<"name" | "description" | "sdk">(fileContent) if (frontmatter === undefined) { - throw new Error(`Frontmatter parsing failed for ${item.href}`) + throw new Error(`Frontmatter parsing failed for ${href}`) } const frontmatterSDKs = frontmatter.sdk?.split(', ') if (frontmatterSDKs !== undefined && isValidSdks(frontmatterSDKs) === false) { const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(sdk) === false) - throw new Error(`Invalid SDK ${JSON.stringify(invalidSDKs)} found in: ${item.href}`) + throw new Error(`Invalid SDK ${JSON.stringify(invalidSDKs)} found in: ${href}`) } const slugify = slugifyWithCounter() const headingsHashs: Array = [] - const fileWarnings = await markdownProcessor() - .use(() => (tree) => { - mdastVisit(tree, - node => node.type === "heading", - node => { - const slug = slugify(toString(node).trim()) - headingsHashs.push(slug) - } - ) + const vfile = await markdownProcessor() + .use(() => (tree, vfile) => { + if (inManifest === false) { + vfile.message("This guide is not in the manifest.json, but will still be publicly accessible and other guides can link to it") + } }) + // Validate and embed the .use(() => (tree, vfile) => { return mdastMap(tree, node => { - const partialSrc = extractComponentPropValueFromNode(tree, vfile, "Include", "src") + const partialSrc = extractComponentPropValueFromNode(node, vfile, "Include", "src") if (partialSrc === undefined) { return node @@ -515,18 +482,26 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { return node } - return partialNode + return Object.assign(node, partialNode) } ) }) + // extract out the headings to check hashes in links + .use(() => (tree) => { + mdastVisit(tree, + node => node.type === "heading", + node => { + const slug = slugify(toString(node).trim()) + headingsHashs.push(slug) + } + ) + }) + // Validate the components .use(() => (tree, vfile) => { - // We are only checking files that have opted in to sdk filtering by frontmatter - if (frontmatterSDKs === undefined) return; - mdastVisit(tree, - node => { + (node) => { const sdk = extractComponentPropValueFromNode(node, vfile, "If", "sdk") if (sdk === undefined) return; @@ -534,6 +509,7 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { const sdksFilter = extractSDKsFromIfProp(node, vfile, sdk) if (sdksFilter === undefined) return + if (frontmatterSDKs === undefined) return; sdksFilter.forEach(sdk => { const available = frontmatterSDKs.includes(sdk) @@ -547,53 +523,89 @@ const parseInMarkdownFile = async (item: ManifestItem, partials: { ) }) .process({ - path: `${item.href}.mdx`, + path: `${href}.mdx`, value: fileContent }) return { - file: { - ...item, - sdk: frontmatterSDKs, - fileContent: String(fileWarnings), - headingsHashs, - frontmatter - }, - fileWarnings + href, + sdk: frontmatterSDKs, + vfile, + headingsHashs, + frontmatter } } const main = async () => { await ensureDirectory(DIST_PATH) - const manifest = await readManifest() + const userManifest = await readManifest() + console.info('✔️ Read Manifest') + const docsFiles = await readDocsFolder() + console.info('✔️ Read Docs Folder') + const partials = await readPartialsMarkdown((await readPartialsFolder()).map(item => item.path)) + console.info('✔️ Read Partials') - const guides = new Map, inManifest: boolean }>() - const markdownFileWarnings: VFile[] = [] + const guides = new Map>>() + const guidesInManifest = new Set() - // This first pass goes through and grabs the sdk scoping out of the markdown files frontmatter - const fullManifest = await traverseTree(manifest, + // Grab all the docs links in the manifest + await traverseTree(userManifest, async (item) => { - if (!item.href?.startsWith('/docs/')) return item if (item.target !== undefined) return item const ignore = IGNORE.some((ignoreItem) => item.href.startsWith(ignoreItem)) - if (ignore === true) return item // even thou we are not processing them, we still need to keep them + if (ignore === true) return item - const { file: markdownFile, fileWarnings } = await parseInMarkdownFile(item, partials) + guidesInManifest.add(item.href) - guides.set(item.href, { - ...markdownFile, - inManifest: true - }) + return item + } + ) + console.info('✔️ Parsed in Manifest') + + // Read in all the guides + const docs = (await Promise.all(docsFiles.map(async (file) => { + const href = removeMdxSuffix(`/docs/${file.path}`) - markdownFileWarnings.push(fileWarnings) + const alreadyLoaded = guides.get(href) - return { ...markdownFile } as const + if (alreadyLoaded) return null // already processed + const inManifest = guidesInManifest.has(href) + + // we aren't awaiting here so we can move on while IO processes + const markdownFile = await parseInMarkdownFile(href, partials, inManifest) + + guides.set(href, markdownFile) + + return markdownFile + }))).filter((item): item is NonNullable => item !== null) + console.info('✔️ Loaded in guides') + + // Goes through and grabs the sdk scoping out of the manifest + const sdkScopedManifest = await traverseTree(userManifest, + async (item) => { + + if (!item.href?.startsWith('/docs/')) return item + if (item.target !== undefined) return item + + const ignore = IGNORE.some((ignoreItem) => item.href.startsWith(ignoreItem)) + if (ignore === true) return item // even thou we are not processing them, we still need to keep them + + const guide = guides.get(item.href) + + if (guide === undefined) { + throw new Error(`Guide ${item.href} not found`) + } + + return { + ...item, + sdk: guide.sdk + } }, async (group) => { const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter((sdk): sdk is SDK => sdk !== undefined) @@ -609,99 +621,87 @@ const main = async () => { } } ) + console.info('✔️ Applied manifest sdk scoping') - await Promise.all(docsFiles.map(async (file) => { - const href = removeMdxSuffix(`/docs/${file.path}`) - if (guides.has(href) === false) { - console.log(`Guide /docs/${file.path} not found in manifest`) + await writeDistFile('m.json', JSON.stringify(sdkScopedManifest, null, 2)) - const { file: markdownFile, fileWarnings } = await parseInMarkdownFile({ - title: "Unknown Title (Not referenced in manifest)", - href - }, partials) + const coreVFiles = docs.map(async (doc) => { + const vfile = await markdownProcessor() + // Validate links between guides are valid + .use(() => (tree: Node, vfile: VFile) => { + mdastVisit(tree, - guides.set(href, { - ...markdownFile, - inManifest: false - }) + // Get all the relative links + node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), - markdownFileWarnings.push(fileWarnings) + node => { + if ("url" in node && typeof node.url === "string") { + const [url, hash] = node.url.split("#") - if (markdownFile.sdk === undefined) { - await writeDistFile(`${markdownFile.href.replace("/docs/", "")}.mdx`, markdownFile.fileContent) - } - } - })) + const ignore = IGNORE.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return; - const flatManifest = flattenTree(fullManifest) + const guide = guides.get(url) - const vfiles = (await Promise.all(flatManifest.map(async (item) => { - if ("fileContent" in item) { - - const vfile = await markdownProcessor() - .use(() => (tree, vfile) => { - mdastVisit(tree, - node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), - node => { - if ("url" in node && typeof node.url === "string") { - const [url, hash] = node.url.split("#") - - const ignore = IGNORE.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return; + if (guide === undefined) { + vfile.message(`Guide ${url} not found`, node.position) + return; + } - const guide = guides.get(url) + if (hash === undefined) return; // We only need the markdown contents if we are checking the link hash - if (guide === undefined) { - vfile.message(`Guide ${url} not found`, node.position) - return; - } + const hasHash = guide.headingsHashs.includes(hash) - if (hash !== undefined) { - const hasHash = guide.headingsHashs.includes(hash) - - if (hasHash === false) { - vfile.message(`Hash "${hash}" not found in ${url}`, node.position) - return; - } - } + if (hasHash === false) { + vfile.message(`Hash "${hash}" not found in ${url}`, node.position) + return; } } - ) - }).process({ - path: `${item.href.startsWith('/') ? item.href.slice(1) : item.href}.mdx`, - value: item.fileContent - }) + } + ) + }) + // to do - update links to sdk specific docs + .process(doc.vfile) - if (item.sdk === undefined) { - await writeDistFile(`${item.href.replace("/docs/", "")}.mdx`, item.fileContent) - } + if (doc.sdk !== undefined) return vfile; // skip sdk specific docs - return vfile - } - }))).filter((item): item is NonNullable => item !== undefined) + await writeDistFile(`${doc.href.replace("/docs/", "")}.mdx`, String(vfile)) + + return vfile + }) - const sdkSpecificMarkdownFileWarnings: VFile[] = [] + Promise.all(coreVFiles).then(() => console.info('✔️ Wrote out core docs')) - for (const targetSdk of VALID_SDKS) { + const sdkSpecificVFiles = Promise.all(VALID_SDKS.map(async (targetSdk) => { - // This second pass goes through and removes any items that are not scoped to the target sdk - const sdkFilteredManifest = await traverseTree(fullManifest, + // Goes through and removes any items that are not scoped to the target sdk + const navigation = await traverseTree(sdkScopedManifest, async ({ sdk, ...item }) => { // This means its generic, not scoped to a specific sdk, so we keep it if (sdk === undefined) return { - ...item, - } + title: item.title, + href: item.href, + tag: item.tag, + wrap: item.wrap, + icon: item.icon, + target: item.target + } as const // This item is not scoped to the target sdk, so we remove it if (sdk.includes(targetSdk) === false) return null // This is a scoped item and its scoped to our target sdk return { - ...item, - scopedHref: scopeHrefToSDK(item.href, targetSdk) - } + title: item.title, + href: scopeHrefToSDK(item.href, targetSdk), + tag: item.tag, + wrap: item.wrap, + icon: item.icon, + target: item.target + } as const }, + // @ts-expect-error - This traverseTree function might just be the death of me async ({ sdk, ...group }) => { if (sdk === undefined) return group @@ -712,88 +712,89 @@ const main = async () => { } ) - // Here we are filtering out content for different sdks, and updating links to make them scoped to the sdk when necessary - await traverseTree(sdkFilteredManifest, - async (item) => { - if ("fileContent" in item) { - const filePath = `${item.href.replace("/docs/", "")}.mdx` + const sdkSpecificVFiles = await Promise.all(docs.map(async (doc) => { + if (doc.sdk === undefined) return null; // skip core docs - const vfile = await markdownProcessor() - .use(() => (tree, vfile) => { - return mdastFilter(tree, - node => { - const sdk = extractComponentPropValueFromNode(node, vfile, "If", "sdk") + const vfile = await markdownProcessor() + // filter out content that is only available to other sdk's + .use(() => (tree, vfile) => { + return mdastFilter(tree, + node => { - if (sdk === undefined) return; + // We aren't passing the vfile here as the as the warning + // should have already been reported above when we initially + // parsed the file - const sdksFilter = extractSDKsFromIfProp(node, vfile, sdk) + const sdk = extractComponentPropValueFromNode(node, undefined, "If", "sdk") - if (sdksFilter === undefined) return + if (sdk === undefined) return true - if (sdksFilter.includes(targetSdk)) { - return true - } + const sdksFilter = extractSDKsFromIfProp(node, undefined, sdk) - return false + if (sdksFilter === undefined) return true - } - ) - }) - // .use(() => (tree, vfile) => { - // let offset = 0 + if (sdksFilter.includes(targetSdk)) { + return true + } - // visit(tree, - // node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), - // node => { + return false - // if (!("url" in node)) { + } + ) + }) + // scope urls so they point to the current sdk + .use(() => (tree, vfile) => { + return mdastMap(tree, + node => { + if (node.type !== "link") return node + if (!("url" in node)) { + vfile.fail(`Link node does not have a url property ${pleaseReport}`, node.position) + return node + } + if (typeof node.url !== "string") { + vfile.fail(`Link node url must be a string ${pleaseReport}`, node.position) + return node + } + if (!node.url.startsWith("/docs/")) { + return node + } - // } + const guide = guides.get(node.url) - // console.log(node) - // } - // ) - // }) - .process({ - path: filePath, - value: item.fileContent - }) + if (guide === undefined) { } - sdkSpecificMarkdownFileWarnings.push(vfile) + return node + } + ) + }) + .process({ + ...doc.vfile, messages: [] // reset the messages + }) - await writeSDKFile(targetSdk, filePath, String(vfile)) - } - return null - }) + await writeSDKFile(targetSdk, `${doc.href.replace("/docs/", "")}.mdx`, String(vfile)) - // const report = reporter(markdownFileWarnings, { quiet: true }) + return vfile + })) - // if (report !== "") { - // console.info(report) - // } + await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) - const navigation = await traverseTree(sdkFilteredManifest, - async (item) => { - // @ts-expect-error - simplest way to remove these properties - const { scopedHref, fileContent, frontmatter, headingsHashs, ...details } = item + return sdkSpecificVFiles + })) - return { - ...details, - href: scopedHref ?? details.href, - } - }, - ) + const [awaitedCoreVFiles, awaitedSdkSpecificVFiles] = await Promise.all([Promise.all(coreVFiles), sdkSpecificVFiles]) - await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) - } + const flatSdkSpecificVFiles = awaitedSdkSpecificVFiles.flat() - const output = reporter([...vfiles, ...markdownFileWarnings, ...sdkSpecificMarkdownFileWarnings], { quiet: true }) + const output = reporter([ + ...awaitedCoreVFiles.filter((item): item is NonNullable => item !== null), + ...flatSdkSpecificVFiles.filter((item): item is NonNullable => item !== null) + ], + { quiet: true }) if (output !== "") { console.info(output) } - } main() \ No newline at end of file From e8455ea6c2b28a20973fe6beab38e24bf013cbc6 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 21 Feb 2025 03:29:24 +0800 Subject: [PATCH 018/114] Better error message for links with 404 hrefs --- scripts/build-docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index a331947334..6c501a1836 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -599,7 +599,7 @@ const main = async () => { const guide = guides.get(item.href) if (guide === undefined) { - throw new Error(`Guide ${item.href} not found`) + throw new Error(`Guide "${item.title}" not found in the docs folder at ${item.href}.mdx`) } return { From dd8f6b8ed2264b3e4dd327a9dbf03a51e6b0ae8f Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 21 Feb 2025 04:07:27 +0800 Subject: [PATCH 019/114] accept jsx arrays of sdk prop --- scripts/build-docs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 6c501a1836..b9f879188e 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -380,8 +380,8 @@ const extractComponentPropValueFromNode = ( } const extractSDKsFromIfProp = (node: Node, vfile: VFile | undefined, sdkProp: string) => { - if (sdkProp.includes('", "')) { - const sdks = JSON.parse(sdkProp) + if (sdkProp.includes('", "') || sdkProp.includes("', '")) { + const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) if (isValidSdks(sdks)) { return sdks } else { From cde5c642d196d60f28b43095e41308c3779b26ff Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 24 Feb 2025 15:27:22 +0800 Subject: [PATCH 020/114] Optimise build script by reducing times we parse the markdown --- scripts/build-docs.ts | 98 +++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index b9f879188e..4671447b0e 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -213,26 +213,6 @@ const markdownProcessor = remark() type VFile = Awaited> -const parseFrontmatter = async (fileContent: string): Promise | undefined> => { - let frontmatter: Record | undefined = undefined - - await markdownProcessor() - .use(() => (tree, vfile) => { - mdastVisit(tree, - node => node.type === 'yaml' && "value" in node, - node => { - if (!("value" in node)) return; - if (typeof node.value !== "string") return; - - frontmatter = yaml.parse(node.value) - } - ) - }) - .process(fileContent) - - return frontmatter -} - const ensureDirectory = async (path: string): Promise => { try { await fs.access(path) @@ -336,7 +316,7 @@ const extractComponentPropValueFromNode = ( if (!Array.isArray(node.attributes)) { vfile?.message( - `<${componentName} /> node attributes is not an array (this is a bug with the build script, please report)`, + `<${componentName} /> node attributes is not an array ${pleaseReport}`, node.position ); return undefined; @@ -359,7 +339,7 @@ const extractComponentPropValueFromNode = ( if (value === undefined) { vfile?.message( - `<${componentName} /> attribute "${propName}" has no value (this is a bug with the build script, please report)`, + `<${componentName} /> attribute "${propName}" has no value ${pleaseReport}`, node.position ); return undefined; @@ -408,18 +388,13 @@ const parseInMarkdownFile = async (href: string, partials: { throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { cause: error }) } - const frontmatter = await parseFrontmatter<"name" | "description" | "sdk">(fileContent) - - if (frontmatter === undefined) { - throw new Error(`Frontmatter parsing failed for ${href}`) + type Frontmatter = { + title: string; + description?: string; + sdk?: SDK[] } - const frontmatterSDKs = frontmatter.sdk?.split(', ') - - if (frontmatterSDKs !== undefined && isValidSdks(frontmatterSDKs) === false) { - const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(sdk) === false) - throw new Error(`Invalid SDK ${JSON.stringify(invalidSDKs)} found in: ${href}`) - } + let frontmatter: Frontmatter | undefined = undefined const slugify = slugifyWithCounter() const headingsHashs: Array = [] @@ -430,6 +405,42 @@ const parseInMarkdownFile = async (href: string, partials: { vfile.message("This guide is not in the manifest.json, but will still be publicly accessible and other guides can link to it") } }) + .use(() => (tree, vfile) => { + mdastVisit(tree, + node => node.type === 'yaml' && "value" in node, + node => { + if (!("value" in node)) return; + if (typeof node.value !== "string") return; + + const frontmatterYaml: Record<"title" | "description" | "sdk", string | undefined> = yaml.parse(node.value) + + const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') + + if (frontmatterSDKs !== undefined && isValidSdks(frontmatterSDKs) === false) { + const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(sdk) === false) + vfile.fail(`Invalid SDK ${JSON.stringify(invalidSDKs)}`, node.position) + return; + } + + if (frontmatterYaml.title === undefined) { + vfile.fail(`Frontmatter must have a "title" property`, node.position) + return; + } + + frontmatter = { + title: frontmatterYaml.title, + description: frontmatterYaml.description, + sdk: frontmatterSDKs + } + } + ) + + if (frontmatter === undefined) { + vfile.fail(`Frontmatter parsing failed for ${href}`) + return; + } + + }) // Validate and embed the .use(() => (tree, vfile) => { return mdastMap(tree, @@ -460,7 +471,7 @@ const parseInMarkdownFile = async (href: string, partials: { mdastVisit(tree, node => node.type === "mdxJsxFlowElement" && "name" in node && node.name === "Include", () => { - vfile.fail("Partials inside of partials is not yet supported, please report if you are seeing this error", node.position) + vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) } ) @@ -509,13 +520,15 @@ const parseInMarkdownFile = async (href: string, partials: { const sdksFilter = extractSDKsFromIfProp(node, vfile, sdk) if (sdksFilter === undefined) return - if (frontmatterSDKs === undefined) return; + if (frontmatter?.sdk === undefined) return; sdksFilter.forEach(sdk => { - const available = frontmatterSDKs.includes(sdk) + if (frontmatter?.sdk === undefined) return; + + const available = frontmatter.sdk.includes(sdk) if (available === false) { - vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${frontmatterSDKs.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) + vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${frontmatter.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) } }) @@ -527,12 +540,16 @@ const parseInMarkdownFile = async (href: string, partials: { value: fileContent }) + if (frontmatter === undefined) { + throw new Error(`Frontmatter parsing failed for ${href}`) + } + return { href, - sdk: frontmatterSDKs, + sdk: (frontmatter as Frontmatter).sdk, vfile, headingsHashs, - frontmatter + frontmatter: frontmatter as Frontmatter } } @@ -623,8 +640,9 @@ const main = async () => { ) console.info('✔️ Applied manifest sdk scoping') - await writeDistFile('m.json', JSON.stringify(sdkScopedManifest, null, 2)) - + // It would definitely be preferable we didn't need to do this markdown processing twice + // But because we need a full list / hashmap of all the existing docs, we can't + // Unless maybe we do some kind of lazy loading of the docs, but this would add complexity const coreVFiles = docs.map(async (doc) => { const vfile = await markdownProcessor() // Validate links between guides are valid From a4b02416233d400e63e1ef4f9a8462818bd9761e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 24 Feb 2025 21:07:28 +0800 Subject: [PATCH 021/114] undo changes that where for testing --- docs/components/authentication/sign-in.mdx | 6 ------ docs/manifest.json | 13 ------------- docs/manifest.schema.json | 6 ++++++ 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/docs/components/authentication/sign-in.mdx b/docs/components/authentication/sign-in.mdx index e87635663b..76fe4652cf 100644 --- a/docs/components/authentication/sign-in.mdx +++ b/docs/components/authentication/sign-in.mdx @@ -105,7 +105,6 @@ All props are optional. An optional element to be rendered while the component is mounting. - ## Usage with frameworks The following example includes basic implementation of the `` component. You can use this as a starting point for your own implementation. @@ -188,9 +187,6 @@ The following example includes basic implementation of the `` componen ``` - - - ## Usage with JavaScript @@ -345,8 +341,6 @@ clerk.openSignIn() clerk.closeSignIn() ``` - - ## Customization To learn about how to customize Clerk components, see the [customization documentation](/docs/customization/overview). diff --git a/docs/manifest.json b/docs/manifest.json index 59b8ca139c..66113c9fa3 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2057,19 +2057,6 @@ [ { "title": "Clerk Components", - "sdk": [ - "nextjs", - "react", - "javascript-frontend", - "astro", - "chrome-extension", - "expo", - "nuxt", - "react-router", - "remix", - "tanstack-start", - "vue" - ], "items": [ [ { diff --git a/docs/manifest.schema.json b/docs/manifest.schema.json index aad0ba6850..30399b808f 100644 --- a/docs/manifest.schema.json +++ b/docs/manifest.schema.json @@ -55,6 +55,12 @@ "target": { "type": "string", "enum": ["_blank"] + }, + "sdk": { + "type": "array", + "items": { + "$ref": "#/$defs/sdk" + } } } }, From 5b34f3097fd9119897bfa0e9426bcf856ef90b7b Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 24 Feb 2025 23:54:19 +0800 Subject: [PATCH 022/114] Better error message for not resolvable markdown file --- scripts/build-docs.ts | 49 ++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 4671447b0e..b2f92bd93e 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -250,22 +250,31 @@ const traverseTree = async < >( tree: Tree, itemCallback: (item: InItem) => Promise = async (item) => item, - groupCallback: (group: InGroup) => Promise = async (group) => group + groupCallback: (group: InGroup) => Promise = async (group) => group, + errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, ): Promise => { const result = await Promise.all(tree.map(async (group) => { return await Promise.all(group.map(async (item) => { - if ('href' in item) { - return await itemCallback(item); - } + try { + if ('href' in item) { + return await itemCallback(item); + } - if ('items' in item && Array.isArray(item.items)) { - return await groupCallback({ - ...item, - items: (await traverseTree(item.items, itemCallback, groupCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) - }); - } + if ('items' in item && Array.isArray(item.items)) { + return await groupCallback({ + ...item, + items: (await traverseTree(item.items, itemCallback, groupCallback, errorCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) + }); + } - return item as OutItem; + return item as OutItem; + } catch (error) { + if (error instanceof Error && errorCallback !== undefined) { + errorCallback(item, error); + } else { + throw error + } + } })); })); @@ -616,7 +625,7 @@ const main = async () => { const guide = guides.get(item.href) if (guide === undefined) { - throw new Error(`Guide "${item.title}" not found in the docs folder at ${item.href}.mdx`) + throw new Error(`Guide "${item.title}" in manifest.json not found in the docs folder at ${item.href}.mdx`) } return { @@ -636,6 +645,10 @@ const main = async () => { sdk: Array.from(new Set([...details.sdk ?? [], ...itemsSDKs])) ?? [], items } + }, + (item, error) => { + console.error('↳', item.title) + throw error } ) console.info('✔️ Applied manifest sdk scoping') @@ -683,7 +696,13 @@ const main = async () => { if (doc.sdk !== undefined) return vfile; // skip sdk specific docs - await writeDistFile(`${doc.href.replace("/docs/", "")}.mdx`, String(vfile)) + const distFilePath = `${doc.href.replace("/docs/", "")}.mdx` + + if (isValidSdk(distFilePath.split('/')[0])) { + throw new Error(`Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`) + } + + await writeDistFile(distFilePath, String(vfile)) return vfile }) @@ -730,7 +749,7 @@ const main = async () => { } ) - const sdkSpecificVFiles = await Promise.all(docs.map(async (doc) => { + const vFiles = await Promise.all(docs.map(async (doc) => { if (doc.sdk === undefined) return null; // skip core docs const vfile = await markdownProcessor() @@ -796,7 +815,7 @@ const main = async () => { await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) - return sdkSpecificVFiles + return vFiles })) const [awaitedCoreVFiles, awaitedSdkSpecificVFiles] = await Promise.all([Promise.all(coreVFiles), sdkSpecificVFiles]) From 2a586d5a83768a83993b7d2dcf83a7965a04e38e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 25 Feb 2025 00:23:12 +0800 Subject: [PATCH 023/114] strip out .mdx extension from links to ensure they work as expected in the website --- scripts/build-docs.ts | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index b2f92bd93e..63b6b49d07 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -660,38 +660,41 @@ const main = async () => { const vfile = await markdownProcessor() // Validate links between guides are valid .use(() => (tree: Node, vfile: VFile) => { - mdastVisit(tree, + return mdastMap(tree, + node => { - // Get all the relative links - node => node.type === "link" && "url" in node && typeof node.url === "string" && node.url.startsWith("/docs/"), + if (node.type !== "link") return node + if (!("url" in node)) return node + if (typeof node.url !== "string") return node + if (!node.url.startsWith("/docs/")) return node - node => { - if ("url" in node && typeof node.url === "string") { - const [url, hash] = node.url.split("#") + node.url = removeMdxSuffix(node.url) - const ignore = IGNORE.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return; + const [url, hash] = (node.url as string).split("#") - const guide = guides.get(url) + const ignore = IGNORE.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return node; - if (guide === undefined) { - vfile.message(`Guide ${url} not found`, node.position) - return; - } + const guide = guides.get(url) - if (hash === undefined) return; // We only need the markdown contents if we are checking the link hash + if (guide === undefined) { + vfile.message(`Guide ${url} not found`, node.position) + return node; + } - const hasHash = guide.headingsHashs.includes(hash) + if (hash === undefined) return node; // We only need the markdown contents if we are checking the link hash - if (hasHash === false) { - vfile.message(`Hash "${hash}" not found in ${url}`, node.position) - return; - } + const hasHash = guide.headingsHashs.includes(hash) + + if (hasHash === false) { + vfile.message(`Hash "${hash}" not found in ${url}`, node.position) + return node; } + + return node; } ) }) - // to do - update links to sdk specific docs .process(doc.vfile) if (doc.sdk !== undefined) return vfile; // skip sdk specific docs From 02865cc1c228deb4334e9a413a76640848293807 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 25 Feb 2025 02:00:06 +0800 Subject: [PATCH 024/114] Setup the groundwork for a new component and component --- package-lock.json | 16 +++++++++++++ package.json | 1 + scripts/build-docs.ts | 54 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2a2a0b6da..63ce1c52a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", + "unist-builder": "^4.0.0", "unist-util-filter": "^5.0.1", "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", @@ -2656,6 +2657,7 @@ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", "dev": true, + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", @@ -2904,6 +2906,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-builder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", + "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-filter": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz", diff --git a/package.json b/package.json index f8eb0576c5..f9f63ef894 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", + "unist-builder": "^4.0.0", "unist-util-filter": "^5.0.1", "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 63b6b49d07..4891cd2426 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -8,8 +8,9 @@ // - [x] Validates the sdk filtering in the component // - [x] Embeds the includes in the markdown files +// - [x] Updates the links in the content if they point to the sdk specific docs // - [x] Copies over "core" docs to the dist folder -// - [ ] Updates the links in the content if they point to the sdk specific docs +// - [x] Generates "landing" pages for the sdk specific docs at the original url // - [x] Generates a manifest that is specific to each SDK // - [x] Duplicates out the sdk specific docs to their respective folders // - [x] stripping filtered out content @@ -21,6 +22,7 @@ import { remark } from 'remark' import { visit as mdastVisit } from 'unist-util-visit' import { filter as mdastFilter } from 'unist-util-filter' import { map as mdastMap } from 'unist-util-map' +import { u as mdastBuilder } from 'unist-builder' import remarkFrontmatter from 'remark-frontmatter' import yaml from "yaml" import { slugifyWithCounter } from '@sindresorhus/slugify' @@ -427,7 +429,7 @@ const parseInMarkdownFile = async (href: string, partials: { if (frontmatterSDKs !== undefined && isValidSdks(frontmatterSDKs) === false) { const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(sdk) === false) - vfile.fail(`Invalid SDK ${JSON.stringify(invalidSDKs)}`, node.position) + vfile.fail(`Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(VALID_SDKS)}`, node.position) return; } @@ -667,6 +669,7 @@ const main = async () => { if (!("url" in node)) return node if (typeof node.url !== "string") return node if (!node.url.startsWith("/docs/")) return node + if (!("children" in node)) return node node.url = removeMdxSuffix(node.url) @@ -682,13 +685,34 @@ const main = async () => { return node; } - if (hash === undefined) return node; // We only need the markdown contents if we are checking the link hash + if (hash !== undefined) { + const hasHash = guide.headingsHashs.includes(hash) - const hasHash = guide.headingsHashs.includes(hash) + if (hasHash === false) { + vfile.message(`Hash "${hash}" not found in ${url}`, node.position) + } + } - if (hasHash === false) { - vfile.message(`Hash "${hash}" not found in ${url}`, node.position) - return node; + if (guide.sdk !== undefined) { + // we are going to swap it for the sdk link component to give the users a great experience + + return mdastBuilder('mdxJsxFlowElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: url + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + // value: `['${guide.sdk.join("', '")}']` + value: mdastBuilder('mdxJsxAttributeValueExpression', { + // value: `["${guide.sdk.join('", "')}"]` + value: JSON.stringify(guide.sdk) + }) + }) + ] + }) } return node; @@ -697,14 +721,24 @@ const main = async () => { }) .process(doc.vfile) - if (doc.sdk !== undefined) return vfile; // skip sdk specific docs - const distFilePath = `${doc.href.replace("/docs/", "")}.mdx` if (isValidSdk(distFilePath.split('/')[0])) { throw new Error(`Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`) } + if (doc.sdk !== undefined) { + // This is a sdk specific guide, so we want to put a landing page here to redirect the user to a guide customised to their sdk. + + await writeDistFile( + distFilePath, + // It's possible we will want to / need to put some frontmatter here + `` + ) + + return vfile + } + await writeDistFile(distFilePath, String(vfile)) return vfile @@ -808,7 +842,7 @@ const main = async () => { ) }) .process({ - ...doc.vfile, messages: [] // reset the messages + ...doc.vfile, messages: [] // reset the messages, otherwise they will be duplicated }) await writeSDKFile(targetSdk, `${doc.href.replace("/docs/", "")}.mdx`, String(vfile)) From eeab48b5ad2362eb1813a40024ebdc7eb4bf171a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 25 Feb 2025 02:17:30 +0800 Subject: [PATCH 025/114] update validation comment --- scripts/build-docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 4891cd2426..8cfe089553 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1,7 +1,7 @@ // Things this build script does // - [x] Validates the manifest -// - [x] Validates the markdown files contents +// - [x] Validates the markdown files contents (including frontmatter) // - [x] Validates links (including hashes) between docs are valid // - [x] Validates the sdk filtering in the manifest // - [x] Validates the sdk filtering in the frontmatter From 02fa9853c7985cc50f54df6db6bcabfacc50f1d0 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 25 Feb 2025 03:03:35 +0800 Subject: [PATCH 026/114] Don't generate out docs that are not for the specific target sdk --- scripts/build-docs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 8cfe089553..0820988844 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -788,6 +788,7 @@ const main = async () => { const vFiles = await Promise.all(docs.map(async (doc) => { if (doc.sdk === undefined) return null; // skip core docs + if (doc.sdk.includes(targetSdk) === false) return null; // skip docs that are not for the target sdk const vfile = await markdownProcessor() // filter out content that is only available to other sdk's From 02b0ad859ad7f391ce59c69899a7f440f7f17e3f Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 25 Feb 2025 03:41:31 +0800 Subject: [PATCH 027/114] Remove default values from manifests to cut down json file size --- scripts/build-docs.ts | 46 ++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 0820988844..1c8d272f32 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -91,6 +91,10 @@ const tag = z.enum(["(Beta)", "(Community)"]) type Tag = z.infer +const MANIFEST_WRAP_DEFAULT = true +const MANIFEST_COLLAPSE_DEFAULT = false +const MANIFEST_HIDE_TITLE_DEFAULT = false + type ManifestItem = { title: string href: string @@ -105,7 +109,7 @@ const manifestItem: z.ZodType = z.object({ title: z.string(), href: z.string(), tag: tag.optional(), - wrap: z.boolean().default(true), + wrap: z.boolean().default(MANIFEST_WRAP_DEFAULT), icon: icon.optional(), target: z.enum(["_blank"]).optional(), sdk: z.array(sdk).optional() @@ -125,11 +129,11 @@ type ManifestGroup = { const manifestGroup: z.ZodType = z.object({ title: z.string(), items: z.lazy(() => manifestSchema), - collapse: z.boolean().default(false), + collapse: z.boolean().default(MANIFEST_COLLAPSE_DEFAULT), tag: tag.optional(), - wrap: z.boolean().default(true), + wrap: z.boolean().default(MANIFEST_WRAP_DEFAULT), icon: icon.optional(), - hideTitle: z.boolean().default(false), + hideTitle: z.boolean().default(MANIFEST_HIDE_TITLE_DEFAULT), sdk: z.array(sdk).optional() }).strict() @@ -612,7 +616,7 @@ const main = async () => { return markdownFile }))).filter((item): item is NonNullable => item !== null) - console.info('✔️ Loaded in guides') + console.info(`✔️ Loaded in ${docs.length} guides`) // Goes through and grabs the sdk scoping out of the manifest const sdkScopedManifest = await traverseTree(userManifest, @@ -744,7 +748,7 @@ const main = async () => { return vfile }) - Promise.all(coreVFiles).then(() => console.info('✔️ Wrote out core docs')) + Promise.all(coreVFiles).then((docs) => console.info(`✔️ Wrote out ${docs.length} core docs`)) const sdkSpecificVFiles = Promise.all(VALID_SDKS.map(async (targetSdk) => { @@ -757,7 +761,7 @@ const main = async () => { title: item.title, href: item.href, tag: item.tag, - wrap: item.wrap, + wrap: item.wrap === MANIFEST_WRAP_DEFAULT ? undefined : item.wrap, icon: item.icon, target: item.target } as const @@ -770,7 +774,7 @@ const main = async () => { title: item.title, href: scopeHrefToSDK(item.href, targetSdk), tag: item.tag, - wrap: item.wrap, + wrap: item.wrap === MANIFEST_WRAP_DEFAULT ? undefined : item.wrap, icon: item.icon, target: item.target } as const @@ -778,11 +782,27 @@ const main = async () => { // @ts-expect-error - This traverseTree function might just be the death of me async ({ sdk, ...group }) => { - if (sdk === undefined) return group + if (sdk === undefined) return { + title: group.title, + collapse: group.collapse === MANIFEST_COLLAPSE_DEFAULT ? undefined : group.collapse, + tag: group.tag, + wrap: group.wrap === MANIFEST_WRAP_DEFAULT ? undefined : group.wrap, + icon: group.icon, + hideTitle: group.hideTitle === MANIFEST_HIDE_TITLE_DEFAULT ? undefined : group.hideTitle, + items: group.items, + } if (sdk.includes(targetSdk) === false) return null - return group + return { + title: group.title, + collapse: group.collapse === MANIFEST_COLLAPSE_DEFAULT ? undefined : group.collapse, + tag: group.tag, + wrap: group.wrap === MANIFEST_WRAP_DEFAULT ? undefined : group.wrap, + icon: group.icon, + hideTitle: group.hideTitle === MANIFEST_HIDE_TITLE_DEFAULT ? undefined : group.hideTitle, + items: group.items, + } } ) @@ -853,12 +873,14 @@ const main = async () => { await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) - return vFiles + return { targetSdk, vFiles } })) + sdkSpecificVFiles.then((sdk) => sdk.forEach(({ targetSdk, vFiles }) => console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific guides`))) + const [awaitedCoreVFiles, awaitedSdkSpecificVFiles] = await Promise.all([Promise.all(coreVFiles), sdkSpecificVFiles]) - const flatSdkSpecificVFiles = awaitedSdkSpecificVFiles.flat() + const flatSdkSpecificVFiles = awaitedSdkSpecificVFiles.flatMap(({ vFiles }) => vFiles) const output = reporter([ ...awaitedCoreVFiles.filter((item): item is NonNullable => item !== null), From fbca9c74129f4310958a1d6d69e8baaeb6f18995 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 25 Feb 2025 05:53:49 +0800 Subject: [PATCH 028/114] For , scope to :sdk: for the component to then swap out for the users active SDK --- scripts/build-docs.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 1c8d272f32..84b4258a23 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -287,7 +287,7 @@ const traverseTree = async < return result.map(group => group.filter((item): item is NonNullable => item !== null)) as unknown as OutTree; }; -const scopeHrefToSDK = (href: string, targetSDK: SDK) => { +const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { // This is external so can't change it if (href.startsWith('/docs') === false) return href @@ -705,13 +705,11 @@ const main = async () => { attributes: [ mdastBuilder('mdxJsxAttribute', { name: 'href', - value: url + value: scopeHrefToSDK(url, ':sdk:') }), mdastBuilder('mdxJsxAttribute', { name: 'sdks', - // value: `['${guide.sdk.join("', '")}']` value: mdastBuilder('mdxJsxAttributeValueExpression', { - // value: `["${guide.sdk.join('", "')}"]` value: JSON.stringify(guide.sdk) }) }) From f36ae584ee854c56310f695d0a020440ba78b513 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 25 Feb 2025 20:29:12 +0800 Subject: [PATCH 029/114] Add comment --- scripts/build-docs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 84b4258a23..54dcc40602 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -14,6 +14,7 @@ // - [x] Generates a manifest that is specific to each SDK // - [x] Duplicates out the sdk specific docs to their respective folders // - [x] stripping filtered out content +// - [x] Removes .mdx from the end of docs markdown links import fs from 'node:fs/promises' import path from 'node:path' From 66e99d9199dc1e9f7a20257981906173bc2f4a06 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 26 Feb 2025 03:11:25 +0800 Subject: [PATCH 030/114] Create a dev mode for the dev script --- package-lock.json | 31 ++++++++++++++++++ package.json | 4 ++- scripts/build-docs.ts | 75 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63ce1c52a4..d92cfd28bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", + "chokidar": "^4.0.3", "concurrently": "^8.2.2", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", @@ -780,6 +781,36 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chokidar/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", diff --git a/package.json b/package.json index f9f63ef894..37ac1f12d5 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,13 @@ "lint:check-links": "node ./scripts/check-links.mjs", "lint:formatting": "prettier . --check", "lint:check-quickstarts": "node ./scripts/check-quickstarts.mjs", - "build": "tsx ./scripts/build-docs.ts" + "build": "tsx ./scripts/build-docs.ts", + "dev": "tsx ./scripts/build-docs.ts --watch" }, "devDependencies": { "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", + "chokidar": "^4.0.3", "concurrently": "^8.2.2", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 54dcc40602..efbd0ebb41 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -33,6 +33,7 @@ import readdirp from 'readdirp' import { z } from "zod" import { fromError } from 'zod-validation-error'; import { Node } from 'unist' +import chok from 'chokidar' const BASE_PATH = process.cwd() const DOCS_FOLDER_RELATIVE = './docs' @@ -569,7 +570,12 @@ const parseInMarkdownFile = async (href: string, partials: { } } -const main = async () => { + +const createBlankStore = () => ({ + markdownFiles: new Map>>() +}) + +const build = async (store: ReturnType) => { await ensureDirectory(DIST_PATH) const userManifest = await readManifest() @@ -610,8 +616,17 @@ const main = async () => { const inManifest = guidesInManifest.has(href) - // we aren't awaiting here so we can move on while IO processes - const markdownFile = await parseInMarkdownFile(href, partials, inManifest) + let markdownFile: Awaited>; + + const cachedMarkdownFile = store.markdownFiles.get(href) + + if (cachedMarkdownFile) { + markdownFile = structuredClone(cachedMarkdownFile) + } else { + markdownFile = await parseInMarkdownFile(href, partials, inManifest) + + store.markdownFiles.set(href, structuredClone(markdownFile)) + } guides.set(href, markdownFile) @@ -890,6 +905,60 @@ const main = async () => { if (output !== "") { console.info(output) } +} + +const watchAndRebuild = (store: ReturnType) => { + + const watcher = chok.watch( + [ + DOCS_FOLDER, + ], + { + alwaysStat: true, + ignored: (filePath, stats) => { + if (stats === undefined) return false + if (stats.isDirectory()) return false + + const relativePath = path.relative(DOCS_FOLDER, filePath) + + const isManifest = relativePath === 'manifest.json' + const isMarkdown = relativePath.endsWith('.mdx') + + return !(isManifest || isMarkdown) + }, + ignoreInitial: true, + } + ) + + watcher.on("all", async (event, filePath) => { + + console.info(`File ${filePath} changed`, { event }) + + const href = removeMdxSuffix(`/${path.relative(BASE_PATH, filePath)}`) + + store.markdownFiles.delete(href) + + await build(store) + + }) + +} + +const main = async () => { + + const store = createBlankStore() + + await build(store) + + const args = process.argv.slice(2) + const watchFlag = args.includes('--watch') + + if (watchFlag) { + + console.info(`Watching for changes...`) + + watchAndRebuild(store) + } } From 1a60e95214043d4a83989c140fa6b2df578d8d2b Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 27 Feb 2025 06:18:13 +0800 Subject: [PATCH 031/114] (wip) improve the validation to ensure can't filter to sdk that not available based on manifest --- scripts/build-docs.ts | 160 +++++++++++++++++++++++++++++------------- 1 file changed, 113 insertions(+), 47 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index efbd0ebb41..b623c1673b 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -6,6 +6,8 @@ // - [x] Validates the sdk filtering in the manifest // - [x] Validates the sdk filtering in the frontmatter // - [x] Validates the sdk filtering in the component +// - [x] Checks that the sdk is available in the manifest +// - [x] Checks that the sdk is available in the frontmatter // - [x] Embeds the includes in the markdown files // - [x] Updates the links in the content if they point to the sdk specific docs @@ -249,30 +251,36 @@ const removeMdxSuffix = (filePath: string) => { type BlankTree }> = Array>; const traverseTree = async < - Tree extends BlankTree, - InItem extends Extract, - InGroup extends Extract }>, + Tree extends { items: BlankTree }, + InItem extends Extract, + InGroup extends Extract }>, OutItem extends { href: string }, OutGroup extends { items: BlankTree }, OutTree extends BlankTree >( tree: Tree, - itemCallback: (item: InItem) => Promise = async (item) => item, - groupCallback: (group: InGroup) => Promise = async (group) => group, + itemCallback: (item: InItem, tree: Tree) => Promise = async (item) => item, + groupCallback: (group: InGroup, tree: Tree) => Promise = async (group) => group, errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, ): Promise => { - const result = await Promise.all(tree.map(async (group) => { + const result = await Promise.all(tree.items.map(async (group) => { return await Promise.all(group.map(async (item) => { try { if ('href' in item) { - return await itemCallback(item); + return await itemCallback(item, tree); } if ('items' in item && Array.isArray(item.items)) { - return await groupCallback({ - ...item, - items: (await traverseTree(item.items, itemCallback, groupCallback, errorCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) - }); + const newGroup = await groupCallback(item, tree); + + if (newGroup === null) return null; + + const newItems = (await traverseTree(newGroup, itemCallback, groupCallback, errorCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) + + return { + ...newGroup, + items: newItems + } } return item as OutItem; @@ -289,6 +297,28 @@ const traverseTree = async < return result.map(group => group.filter((item): item is NonNullable => item !== null)) as unknown as OutTree; }; +function flattenTree< + Tree extends BlankTree, + InItem extends Extract, + InGroup extends Extract }> +>(tree: Tree): InItem[] { + const result: InItem[] = []; + + for (const group of tree) { + for (const itemOrGroup of group) { + if ("href" in itemOrGroup) { + // It's an item + result.push(itemOrGroup); + } else if ("items" in itemOrGroup && Array.isArray(itemOrGroup.items)) { + // It's a group with its own sub-tree, flatten it + result.push(...flattenTree(itemOrGroup.items)); + } + } + } + + return result; +} + const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { // This is external so can't change it @@ -314,7 +344,7 @@ const extractComponentPropValueFromNode = ( ): string | undefined => { // Check if it's an MDX component - if (node.type !== "mdxJsxFlowElement") { + if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement") { return undefined; } @@ -486,7 +516,7 @@ const parseInMarkdownFile = async (href: string, partials: { const partialContentVFile = markdownProcessor() .use(() => (tree, vfile) => { mdastVisit(tree, - node => node.type === "mdxJsxFlowElement" && "name" in node && node.name === "Include", + node => (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && "name" in node && node.name === "Include", () => { vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) } @@ -525,33 +555,6 @@ const parseInMarkdownFile = async (href: string, partials: { } ) }) - // Validate the components - .use(() => (tree, vfile) => { - - mdastVisit(tree, - (node) => { - const sdk = extractComponentPropValueFromNode(node, vfile, "If", "sdk") - - if (sdk === undefined) return; - - const sdksFilter = extractSDKsFromIfProp(node, vfile, sdk) - - if (sdksFilter === undefined) return - if (frontmatter?.sdk === undefined) return; - - sdksFilter.forEach(sdk => { - if (frontmatter?.sdk === undefined) return; - - const available = frontmatter.sdk.includes(sdk) - - if (available === false) { - vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${frontmatter.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) - } - - }) - } - ) - }) .process({ path: `${href}.mdx`, value: fileContent @@ -591,7 +594,7 @@ const build = async (store: ReturnType) => { const guidesInManifest = new Set() // Grab all the docs links in the manifest - await traverseTree(userManifest, + await traverseTree({ items: userManifest }, async (item) => { if (!item.href?.startsWith('/docs/')) return item if (item.target !== undefined) return item @@ -635,8 +638,8 @@ const build = async (store: ReturnType) => { console.info(`✔️ Loaded in ${docs.length} guides`) // Goes through and grabs the sdk scoping out of the manifest - const sdkScopedManifest = await traverseTree(userManifest, - async (item) => { + const sdkScopedManifest = await traverseTree({ items: userManifest }, + async (item, tree) => { if (!item.href?.startsWith('/docs/')) return item if (item.target !== undefined) return item @@ -650,17 +653,33 @@ const build = async (store: ReturnType) => { throw new Error(`Guide "${item.title}" in manifest.json not found in the docs folder at ${item.href}.mdx`) } + const sdk = guide.sdk ?? tree.sdk + + if (guide.sdk !== undefined) { + if (guide.sdk.every(sdk => tree.sdk?.includes(sdk)) === false) { + throw new Error(`Guide "${item.title}" is attempting to use ${JSON.stringify(guide.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`) + } + } + return { ...item, - sdk: guide.sdk + sdk, + frontmatterIncludesManifestSDKs: guide.frontmatter.sdk?.includes(sdk) ?? false } }, - async (group) => { + async (group, tree) => { + const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter((sdk): sdk is SDK => sdk !== undefined) const { items, ...details } = group - if (itemsSDKs.length === 0) return { ...details, items } + if (details.sdk !== undefined) { + if (details.sdk.every(sdk => tree.sdk?.includes(sdk)) === false) { + throw new Error(`Group "${details.title}" is attempting to use ${JSON.stringify(details.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`) + } + } + + if (itemsSDKs.length === 0) return { ...details, sdk: details.sdk ?? tree.sdk, items } return { ...details, @@ -675,6 +694,8 @@ const build = async (store: ReturnType) => { ) console.info('✔️ Applied manifest sdk scoping') + const flatSDKScopedManifest = flattenTree(sdkScopedManifest) + // It would definitely be preferable we didn't need to do this markdown processing twice // But because we need a full list / hashmap of all the existing docs, we can't // Unless maybe we do some kind of lazy loading of the docs, but this would add complexity @@ -737,6 +758,51 @@ const build = async (store: ReturnType) => { } ) }) + // Validate the components + .use(() => (tree, vfile) => { + + mdastVisit(tree, + (node) => { + const sdk = extractComponentPropValueFromNode(node, vfile, "If", "sdk") + + if (sdk === undefined) return; + + const sdksFilter = extractSDKsFromIfProp(node, vfile, sdk) + + if (sdksFilter === undefined) return + + const manifestItems = flatSDKScopedManifest.filter((item) => item.href === doc.href) + + const availableSDKs = manifestItems.flatMap((item) => item.sdk).filter(Boolean) + + // The doc doesn't exist in the manifest so we are skipping it + if (manifestItems.length === 0) return; + + sdksFilter.forEach(sdk => { + (() => { + if (doc.sdk === undefined) return; + + const available = doc.sdk.includes(sdk) + + if (available === false) { + vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${doc.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) + } + })(); + + (() => { + // The doc is generic so we are skipping it + if (availableSDKs.length === 0) return; + + const available = availableSDKs.includes(sdk) + + if (available === false) { + vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, node.position) + } + })(); + }) + } + ) + }) .process(doc.vfile) const distFilePath = `${doc.href.replace("/docs/", "")}.mdx` @@ -767,7 +833,7 @@ const build = async (store: ReturnType) => { const sdkSpecificVFiles = Promise.all(VALID_SDKS.map(async (targetSdk) => { // Goes through and removes any items that are not scoped to the target sdk - const navigation = await traverseTree(sdkScopedManifest, + const navigation = await traverseTree({ items: sdkScopedManifest }, async ({ sdk, ...item }) => { // This means its generic, not scoped to a specific sdk, so we keep it From 9e708b0ceb7791747f26941a1c6c30a51393fc3d Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 27 Feb 2025 21:11:56 +0800 Subject: [PATCH 032/114] Fix up the types --- scripts/build-docs.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index b623c1673b..648f12bce6 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -275,6 +275,7 @@ const traverseTree = async < if (newGroup === null) return null; + // @ts-expect-error - OutGroup should always contain "items" property, so this is safe const newItems = (await traverseTree(newGroup, itemCallback, groupCallback, errorCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) return { @@ -638,7 +639,7 @@ const build = async (store: ReturnType) => { console.info(`✔️ Loaded in ${docs.length} guides`) // Goes through and grabs the sdk scoping out of the manifest - const sdkScopedManifest = await traverseTree({ items: userManifest }, + const sdkScopedManifest = await traverseTree({ items: userManifest, sdk: VALID_SDKS }, async (item, tree) => { if (!item.href?.startsWith('/docs/')) return item @@ -663,8 +664,7 @@ const build = async (store: ReturnType) => { return { ...item, - sdk, - frontmatterIncludesManifestSDKs: guide.frontmatter.sdk?.includes(sdk) ?? false + sdk } }, async (group, tree) => { @@ -679,13 +679,13 @@ const build = async (store: ReturnType) => { } } - if (itemsSDKs.length === 0) return { ...details, sdk: details.sdk ?? tree.sdk, items } + if (itemsSDKs.length === 0) return { ...details, sdk: details.sdk ?? tree.sdk, items } as ManifestGroup return { ...details, sdk: Array.from(new Set([...details.sdk ?? [], ...itemsSDKs])) ?? [], items - } + } as ManifestGroup }, (item, error) => { console.error('↳', item.title) From e7ec2b41b4341e2bbb9a2c200cbcac827621042c Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 28 Feb 2025 00:20:53 +0800 Subject: [PATCH 033/114] Setup testing for the build script --- package-lock.json | 1384 ++++++++++++++++++++++++++++++++++-- package.json | 4 +- scripts/build-docs.test.ts | 116 +++ scripts/build-docs.ts | 295 +++++--- 4 files changed, 1632 insertions(+), 167 deletions(-) create mode 100644 scripts/build-docs.test.ts diff --git a/package-lock.json b/package-lock.json index d92cfd28bc..684f943950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", + "vitest": "^3.0.7", "yaml": "^2.7.0", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" @@ -426,6 +427,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", @@ -584,8 +602,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -598,6 +615,272 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sindresorhus/slugify": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", @@ -650,10 +933,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.3", @@ -701,45 +985,168 @@ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", "dev": true }, - "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "node_modules/@vitest/expect": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@vitest/mocker": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@vitest/pretty-format": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", "dev": true, - "engines": { - "node": ">=8" + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "node_modules/@vitest/runner": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "dequal": "^2.0.3" - } + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } }, "node_modules/axobject-query": { "version": "4.1.0", @@ -761,6 +1168,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -771,6 +1188,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/character-entities-html4": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", @@ -781,6 +1215,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -982,12 +1426,13 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1021,6 +1466,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1055,6 +1510,13 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", @@ -1131,11 +1593,20 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "^1.0.0" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1255,12 +1726,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -2511,10 +2989,47 @@ ] }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } }, "node_modules/periscopic": { "version": "3.1.0", @@ -2528,6 +3043,13 @@ "is-reference": "^3.0.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2540,6 +3062,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prettier": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", @@ -2718,6 +3269,45 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rollup": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", + "fsevents": "~2.3.2" + } + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -2751,12 +3341,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "peer": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -2767,6 +3364,20 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2852,6 +3463,50 @@ "node": ">=16" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -3226,6 +3881,637 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/vitest": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.7", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index d0fbae9538..20f396a859 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "lint:check-quickstarts": "node ./scripts/check-quickstarts.mjs", "lint:check-frontmatter": "node ./scripts/check-frontmatter.mjs", "build": "tsx ./scripts/build-docs.ts", - "dev": "tsx ./scripts/build-docs.ts --watch" + "dev": "tsx ./scripts/build-docs.ts --watch", + "test": "vitest" }, "devDependencies": { "@sindresorhus/slugify": "^2.2.1", @@ -34,6 +35,7 @@ "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", + "vitest": "^3.0.7", "yaml": "^2.7.0", "zod": "^3.24.2", "zod-validation-error": "^3.4.0" diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts new file mode 100644 index 0000000000..40060e0afc --- /dev/null +++ b/scripts/build-docs.test.ts @@ -0,0 +1,116 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import os from 'node:os' + +import { expect, test } from 'vitest' +import { build, createBlankStore } from './build-docs' + +async function createTempFiles(files: { + path: string; + content: string; +}[]) { + // Create temp folder with unique name + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'clerk-docs-test-')) + + // Create all files + for (const file of files) { + // Ensure the directory exists + const filePath = path.join(tempDir, file.path) + const dirPath = path.dirname(filePath) + + await fs.mkdir(dirPath, { recursive: true }) + + // Write the file + await fs.writeFile(filePath, file.content) + } + + // Return the temp directory and cleanup function + return { + files: files.reduce((acc, file) => { + acc[file.path] = file.content + return acc + }, {} as Record), + tempDir, + cleanup: async () => { + try { + await fs.rm(tempDir, { recursive: true, force: true }) + } catch (error) { + console.warn(`Warning: Failed to clean up temp folder ${tempDir}:`, error) + } + }, + pathJoin: (...paths: string[]) => path.join(tempDir, ...paths) + } +} + +async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +async function readFile(filePath: string): Promise { + return normalizeString(await fs.readFile(filePath, 'utf-8')) +} + +function normalizeString(str: string): string { + return str.replace(/\r\n/g, '\n').trim(); +} + +test('Basic build test with simple files', async () => { + // Create temp environment with minimal files array + const { files, tempDir, cleanup, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +# Simple Test Page + +Testing with a simple page.` + } + ]) + + const config = { + basePath: tempDir, + docsRelativePath: './docs', + docsFolder: pathJoin('./docs'), + manifestFilePath: pathJoin('./docs/manifest.json'), + partialsPath: './_partials', + distPath: pathJoin('./dist'), + ignorePaths: ["/docs/_partials"], + validSdks: ["nextjs", "react"], + manifestOptions: { + wrapDefault: true, + collapseDefault: false, + hideTitleDefault: false + } + } + + await build(createBlankStore(), config) + + expect(await fileExists(pathJoin('./dist/simple-test.mdx'))).toBe(true) + expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(files['./docs/simple-test.mdx']) + + expect(await fileExists(pathJoin('./dist/nextjs/manifest.json'))).toBe(true) + expect(await readFile(pathJoin('./dist/nextjs/manifest.json'))).toBe(JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + })) + + expect(await fileExists(pathJoin('./dist/react/manifest.json'))).toBe(true) + expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + })) + + await cleanup() +}) + diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 648f12bce6..a8124c7890 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -37,27 +37,21 @@ import { fromError } from 'zod-validation-error'; import { Node } from 'unist' import chok from 'chokidar' -const BASE_PATH = process.cwd() -const DOCS_FOLDER_RELATIVE = './docs' -const DOCS_FOLDER = path.join(BASE_PATH, DOCS_FOLDER_RELATIVE) -const MANIFEST_FILE_PATH = path.join(DOCS_FOLDER, './manifest.json') -const PARTIALS_PATH = './_partials' -const DIST_PATH = path.join(BASE_PATH, './dist') -// const CLERK_PATH = path.join(BASE_PATH, "../clerk") -const IGNORE = [ - "/docs/core-1", - '/pricing', - '/docs/reference/backend-api', - '/docs/reference/frontend-api', - '/support', - '/discord', - '/contact', - '/contact/sales', - '/contact/support', - '/blog', - '/changelog/2024-04-19', - "/docs/_partials" -] +export type BuildConfig = { + basePath: string; + docsRelativePath: string; + docsFolder: string; + manifestFilePath: string; + partialsPath: string; + distPath: string; + ignorePaths: string[]; + validSdks: readonly string[]; + manifestOptions: { + wrapDefault: boolean; + collapseDefault: boolean; + hideTitleDefault: boolean; + }; +} const VALID_SDKS = [ "nextjs", @@ -95,9 +89,41 @@ const tag = z.enum(["(Beta)", "(Community)"]) type Tag = z.infer -const MANIFEST_WRAP_DEFAULT = true -const MANIFEST_COLLAPSE_DEFAULT = false -const MANIFEST_HIDE_TITLE_DEFAULT = false +// The default config the script will run under when using npm run build +const createDefaultConfig = (): BuildConfig => { + const basePath = process.cwd(); + const docsRelativePath = './docs'; + const docsFolder = path.join(basePath, docsRelativePath); + + return { + basePath, + docsRelativePath, + docsFolder, + manifestFilePath: path.join(docsFolder, './manifest.json'), + partialsPath: './_partials', + distPath: path.join(basePath, './dist'), + ignorePaths: [ + "/docs/core-1", + '/pricing', + '/docs/reference/backend-api', + '/docs/reference/frontend-api', + '/support', + '/discord', + '/contact', + '/contact/sales', + '/contact/support', + '/blog', + '/changelog/2024-04-19', + "/docs/_partials" + ], + validSdks: VALID_SDKS, + manifestOptions: { + wrapDefault: true, + collapseDefault: false, + hideTitleDefault: false + } + }; +}; type ManifestItem = { title: string @@ -109,16 +135,6 @@ type ManifestItem = { sdk?: SDK[] } -const manifestItem: z.ZodType = z.object({ - title: z.string(), - href: z.string(), - tag: tag.optional(), - wrap: z.boolean().default(MANIFEST_WRAP_DEFAULT), - icon: icon.optional(), - target: z.enum(["_blank"]).optional(), - sdk: z.array(sdk).optional() -}).strict() - type ManifestGroup = { title: string items: Manifest @@ -130,27 +146,46 @@ type ManifestGroup = { sdk?: SDK[] } -const manifestGroup: z.ZodType = z.object({ - title: z.string(), - items: z.lazy(() => manifestSchema), - collapse: z.boolean().default(MANIFEST_COLLAPSE_DEFAULT), - tag: tag.optional(), - wrap: z.boolean().default(MANIFEST_WRAP_DEFAULT), - icon: icon.optional(), - hideTitle: z.boolean().default(MANIFEST_HIDE_TITLE_DEFAULT), - sdk: z.array(sdk).optional() -}).strict() - type Manifest = (ManifestItem | ManifestGroup)[][] -const manifestSchema: z.ZodType = z.array( - z.array( - z.union([ - manifestItem, - manifestGroup - ]) +// Create manifest schema based on config +const createManifestSchema = (config: BuildConfig) => { + const manifestItem: z.ZodType = z.object({ + title: z.string(), + href: z.string(), + tag: tag.optional(), + wrap: z.boolean().default(config.manifestOptions.wrapDefault), + icon: icon.optional(), + target: z.enum(["_blank"]).optional(), + sdk: z.array(sdk).optional() + }).strict() + + const manifestGroup: z.ZodType = z.object({ + title: z.string(), + items: z.lazy(() => manifestSchema), + collapse: z.boolean().default(config.manifestOptions.collapseDefault), + tag: tag.optional(), + wrap: z.boolean().default(config.manifestOptions.wrapDefault), + icon: icon.optional(), + hideTitle: z.boolean().default(config.manifestOptions.hideTitleDefault), + sdk: z.array(sdk).optional() + }).strict() + + const manifestSchema: z.ZodType = z.array( + z.array( + z.union([ + manifestItem, + manifestGroup + ]) + ) ) -) + + return { + manifestItem, + manifestGroup, + manifestSchema + } +} const pleaseReport = "(this is a bug with the build script, please report)" @@ -162,8 +197,9 @@ const isValidSdks = (sdks: string[]): sdks is SDK[] => { return sdks.every(isValidSdk) } -const readManifest = async (): Promise => { - const unsafe_manifest = await fs.readFile(MANIFEST_FILE_PATH, { "encoding": "utf-8" }) +const readManifest = (config: BuildConfig) => async (): Promise => { + const { manifestSchema } = createManifestSchema(config) + const unsafe_manifest = await fs.readFile(config.manifestFilePath, { "encoding": "utf-8" }) const manifest = await manifestSchema.safeParseAsync(JSON.parse(unsafe_manifest).navigation) @@ -174,8 +210,8 @@ const readManifest = async (): Promise => { throw new Error(`Failed to parse manifest: ${fromError(manifest.error)}`) } -const readMarkdownFile = async (docPath: string) => { - const filePath = path.join(BASE_PATH, docPath) +const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { + const filePath = path.join(config.basePath, docPath) try { const fileContent = await fs.readFile(filePath, { "encoding": "utf-8" }) @@ -185,25 +221,28 @@ const readMarkdownFile = async (docPath: string) => { } } -const readDocsFolder = () => { - return readdirp.promise(DOCS_FOLDER, { +const readDocsFolder = (config: BuildConfig) => async () => { + return readdirp.promise(config.docsFolder, { type: 'files', - fileFilter: (entry) => IGNORE.some((ignoreItem) => `/docs/${entry.path}`.startsWith(ignoreItem)) === false && entry.path.endsWith('.mdx') + fileFilter: (entry) => config.ignorePaths.some((ignoreItem) => + `/docs/${entry.path}`.startsWith(ignoreItem)) === false && entry.path.endsWith('.mdx') }) } -const readPartialsFolder = () => { - return readdirp.promise(path.join(DOCS_FOLDER, './_partials'), { +const readPartialsFolder = (config: BuildConfig) => async () => { + return readdirp.promise(path.join(config.docsFolder, config.partialsPath), { type: 'files', fileFilter: '*.mdx', }) } -const readPartialsMarkdown = (paths: string[]) => { +const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => { + const readFile = readMarkdownFile(config); + return Promise.all(paths.map(async (markdownPath) => { - const fullPath = path.join(DOCS_FOLDER_RELATIVE, PARTIALS_PATH, markdownPath) + const fullPath = path.join(config.docsRelativePath, config.partialsPath, markdownPath) - const [error, content] = await readMarkdownFile(fullPath) + const [error, content] = await readFile(fullPath) if (error) { throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) @@ -223,22 +262,24 @@ const markdownProcessor = remark() type VFile = Awaited> -const ensureDirectory = async (path: string): Promise => { +const ensureDirectory = (config: BuildConfig) => async (dirPath: string): Promise => { try { - await fs.access(path) + await fs.access(dirPath) } catch { - await fs.mkdir(path, { recursive: true }) + await fs.mkdir(dirPath, { recursive: true }) } } -const writeDistFile = async (filePath: string, contents: string) => { - const fullPath = path.join(DIST_PATH, filePath) - await ensureDirectory(path.dirname(fullPath)) +const writeDistFile = (config: BuildConfig) => async (filePath: string, contents: string) => { + const ensureDir = ensureDirectory(config); + const fullPath = path.join(config.distPath, filePath) + await ensureDir(path.dirname(fullPath)) await fs.writeFile(fullPath, contents, { "encoding": "utf-8" }) } -const writeSDKFile = async (sdk: SDK, filePath: string, contents: string) => { - await writeDistFile(path.join(sdk, filePath), contents) +const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, contents: string) => { + const writeFile = writeDistFile(config); + await writeFile(path.join(sdk, filePath), contents) } const removeMdxSuffix = (filePath: string) => { @@ -423,14 +464,15 @@ const extractSDKsFromIfProp = (node: Node, vfile: VFile | undefined, sdkProp: st vfile?.message(`sdk "${sdkProp}" in is not a valid SDK`, node.position) } } - } -const parseInMarkdownFile = async (href: string, partials: { - path: string; - content: string; -}[], inManifest: boolean) => { - const [error, fileContent] = await readMarkdownFile(`${href}.mdx`) +const parseInMarkdownFile = (config: BuildConfig) => async ( + href: string, + partials: { path: string; content: string; }[], + inManifest: boolean, +) => { + const readFile = readMarkdownFile(config); + const [error, fileContent] = await readFile(`${href}.mdx`) if (error !== null) { throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { cause: error }) @@ -574,24 +616,38 @@ const parseInMarkdownFile = async (href: string, partials: { } } - -const createBlankStore = () => ({ - markdownFiles: new Map>>() +export const createBlankStore = () => ({ + markdownFiles: new Map>>>() }) -const build = async (store: ReturnType) => { - await ensureDirectory(DIST_PATH) - - const userManifest = await readManifest() +export const build = async ( + store: ReturnType, + config: BuildConfig +) => { + // Apply currying to create functions pre-configured with config + const ensureDir = ensureDirectory(config); + const getManifest = readManifest(config); + const getDocsFolder = readDocsFolder(config); + const getPartialsFolder = readPartialsFolder(config); + const getPartialsMarkdown = readPartialsMarkdown(config); + const parseMarkdownFile = parseInMarkdownFile(config); + const writeFile = writeDistFile(config); + const writeSdkFile = writeSDKFile(config); + + await ensureDir(config.distPath) + + const userManifest = await getManifest() console.info('✔️ Read Manifest') - const docsFiles = await readDocsFolder() + const docsFiles = await getDocsFolder() console.info('✔️ Read Docs Folder') - const partials = await readPartialsMarkdown((await readPartialsFolder()).map(item => item.path)) + const partials = await getPartialsMarkdown( + (await getPartialsFolder()).map(item => item.path) + ) console.info('✔️ Read Partials') - const guides = new Map>>() + const guides = new Map>>() const guidesInManifest = new Set() // Grab all the docs links in the manifest @@ -600,7 +656,7 @@ const build = async (store: ReturnType) => { if (!item.href?.startsWith('/docs/')) return item if (item.target !== undefined) return item - const ignore = IGNORE.some((ignoreItem) => item.href.startsWith(ignoreItem)) + const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) if (ignore === true) return item guidesInManifest.add(item.href) @@ -620,14 +676,14 @@ const build = async (store: ReturnType) => { const inManifest = guidesInManifest.has(href) - let markdownFile: Awaited>; + let markdownFile: Awaited>; const cachedMarkdownFile = store.markdownFiles.get(href) if (cachedMarkdownFile) { markdownFile = structuredClone(cachedMarkdownFile) } else { - markdownFile = await parseInMarkdownFile(href, partials, inManifest) + markdownFile = await parseMarkdownFile(href, partials, inManifest) store.markdownFiles.set(href, structuredClone(markdownFile)) } @@ -645,7 +701,7 @@ const build = async (store: ReturnType) => { if (!item.href?.startsWith('/docs/')) return item if (item.target !== undefined) return item - const ignore = IGNORE.some((ignoreItem) => item.href.startsWith(ignoreItem)) + const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) if (ignore === true) return item // even thou we are not processing them, we still need to keep them const guide = guides.get(item.href) @@ -716,7 +772,7 @@ const build = async (store: ReturnType) => { const [url, hash] = (node.url as string).split("#") - const ignore = IGNORE.some((ignoreItem) => url.startsWith(ignoreItem)) + const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) if (ignore === true) return node; const guide = guides.get(url) @@ -814,7 +870,7 @@ const build = async (store: ReturnType) => { if (doc.sdk !== undefined) { // This is a sdk specific guide, so we want to put a landing page here to redirect the user to a guide customised to their sdk. - await writeDistFile( + await writeFile( distFilePath, // It's possible we will want to / need to put some frontmatter here `` @@ -823,7 +879,7 @@ const build = async (store: ReturnType) => { return vfile } - await writeDistFile(distFilePath, String(vfile)) + await writeFile(distFilePath, String(vfile)) return vfile }) @@ -841,7 +897,7 @@ const build = async (store: ReturnType) => { title: item.title, href: item.href, tag: item.tag, - wrap: item.wrap === MANIFEST_WRAP_DEFAULT ? undefined : item.wrap, + wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, icon: item.icon, target: item.target } as const @@ -854,21 +910,20 @@ const build = async (store: ReturnType) => { title: item.title, href: scopeHrefToSDK(item.href, targetSdk), tag: item.tag, - wrap: item.wrap === MANIFEST_WRAP_DEFAULT ? undefined : item.wrap, + wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, icon: item.icon, target: item.target } as const }, // @ts-expect-error - This traverseTree function might just be the death of me async ({ sdk, ...group }) => { - if (sdk === undefined) return { title: group.title, - collapse: group.collapse === MANIFEST_COLLAPSE_DEFAULT ? undefined : group.collapse, + collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, tag: group.tag, - wrap: group.wrap === MANIFEST_WRAP_DEFAULT ? undefined : group.wrap, + wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, icon: group.icon, - hideTitle: group.hideTitle === MANIFEST_HIDE_TITLE_DEFAULT ? undefined : group.hideTitle, + hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, items: group.items, } @@ -876,11 +931,11 @@ const build = async (store: ReturnType) => { return { title: group.title, - collapse: group.collapse === MANIFEST_COLLAPSE_DEFAULT ? undefined : group.collapse, + collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, tag: group.tag, - wrap: group.wrap === MANIFEST_WRAP_DEFAULT ? undefined : group.wrap, + wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, icon: group.icon, - hideTitle: group.hideTitle === MANIFEST_HIDE_TITLE_DEFAULT ? undefined : group.hideTitle, + hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, items: group.items, } } @@ -946,12 +1001,12 @@ const build = async (store: ReturnType) => { ...doc.vfile, messages: [] // reset the messages, otherwise they will be duplicated }) - await writeSDKFile(targetSdk, `${doc.href.replace("/docs/", "")}.mdx`, String(vfile)) + await writeSdkFile(targetSdk, `${doc.href.replace("/docs/", "")}.mdx`, String(vfile)) return vfile })) - await writeSDKFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) + await writeSdkFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) return { targetSdk, vFiles } })) @@ -973,11 +1028,13 @@ const build = async (store: ReturnType) => { } } -const watchAndRebuild = (store: ReturnType) => { - +const watchAndRebuild = ( + store: ReturnType, + config: BuildConfig +) => { const watcher = chok.watch( [ - DOCS_FOLDER, + config.docsFolder, ], { alwaysStat: true, @@ -985,7 +1042,7 @@ const watchAndRebuild = (store: ReturnType) => { if (stats === undefined) return false if (stats.isDirectory()) return false - const relativePath = path.relative(DOCS_FOLDER, filePath) + const relativePath = path.relative(config.docsFolder, filePath) const isManifest = relativePath === 'manifest.json' const isMarkdown = relativePath.endsWith('.mdx') @@ -1000,21 +1057,22 @@ const watchAndRebuild = (store: ReturnType) => { console.info(`File ${filePath} changed`, { event }) - const href = removeMdxSuffix(`/${path.relative(BASE_PATH, filePath)}`) + const href = removeMdxSuffix(`/${path.relative(config.basePath, filePath)}`) store.markdownFiles.delete(href) - await build(store) + await build(store, config) }) } const main = async () => { + // Create default configuration + const config = createDefaultConfig(); + const store = createBlankStore(); - const store = createBlankStore() - - await build(store) + await build(store, config); const args = process.argv.slice(2) const watchFlag = args.includes('--watch') @@ -1023,9 +1081,12 @@ const main = async () => { console.info(`Watching for changes...`) - watchAndRebuild(store) + watchAndRebuild(store, config); } } -main() \ No newline at end of file +// Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts +if (require.main === module) { + main(); +} \ No newline at end of file From 316cdd994e4c584591f335202895788396b84775 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 28 Feb 2025 00:34:45 +0800 Subject: [PATCH 034/114] fix test --- scripts/build-docs.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index a8124c7890..a105d66d83 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -695,7 +695,7 @@ export const build = async ( console.info(`✔️ Loaded in ${docs.length} guides`) // Goes through and grabs the sdk scoping out of the manifest - const sdkScopedManifest = await traverseTree({ items: userManifest, sdk: VALID_SDKS }, + const sdkScopedManifest = await traverseTree({ items: userManifest, sdk: undefined as undefined | SDK[] }, async (item, tree) => { if (!item.href?.startsWith('/docs/')) return item @@ -750,6 +750,8 @@ export const build = async ( ) console.info('✔️ Applied manifest sdk scoping') + writeFile('m.json', JSON.stringify(sdkScopedManifest, null, 2)) + const flatSDKScopedManifest = flattenTree(sdkScopedManifest) // It would definitely be preferable we didn't need to do this markdown processing twice From 8964d906123fb74c0c66b8834bee39a364664007 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 28 Feb 2025 01:49:40 +0800 Subject: [PATCH 035/114] Write a second test --- package-lock.json | 401 +++++++++++++++++++++++++++++++++++-- package.json | 1 + scripts/build-docs.test.ts | 168 +++++++++++++--- scripts/build-docs.ts | 174 +++++++++------- 4 files changed, 614 insertions(+), 130 deletions(-) diff --git a/package-lock.json b/package-lock.json index 684f943950..b7df2e739b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/node": "^22.13.2", "chokidar": "^4.0.3", "concurrently": "^8.2.2", + "glob": "^11.0.1", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", "prettier-plugin-nginx": "^1.0.3", @@ -563,6 +564,109 @@ "node": ">=18" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1128,6 +1232,22 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -1168,6 +1288,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1328,21 +1465,6 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/concurrently/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/concurrently/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1395,6 +1517,21 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -1626,6 +1763,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -1672,6 +1826,30 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1703,6 +1881,29 @@ "@types/estree": "*" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -1733,6 +1934,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -2988,6 +3199,32 @@ } ] }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3014,6 +3251,40 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3332,6 +3603,29 @@ "suf-log": "^2.5.3" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shell-quote": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", @@ -3348,6 +3642,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3392,6 +3699,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", @@ -3428,6 +3751,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/suf-log": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", @@ -4495,6 +4832,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -4529,19 +4882,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/y18n": { diff --git a/package.json b/package.json index 20f396a859..679a0724b4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@types/node": "^22.13.2", "chokidar": "^4.0.3", "concurrently": "^8.2.2", + "glob": "^11.0.1", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", "prettier-plugin-nginx": "^1.0.3", diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 40060e0afc..5ae64ca85a 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -1,9 +1,11 @@ import fs from 'node:fs/promises' import path from 'node:path' import os from 'node:os' +import {glob} from 'glob'; -import { expect, test } from 'vitest' -import { build, createBlankStore } from './build-docs' + +import { expect, onTestFinished, test } from 'vitest' +import { build, createBlankStore, createConfig } from './build-docs' async function createTempFiles(files: { path: string; @@ -24,20 +26,18 @@ async function createTempFiles(files: { await fs.writeFile(filePath, file.content) } + onTestFinished(async () => { + try { + await fs.rm(tempDir, { recursive: true, force: true }) + } catch (error) { + console.warn(`Warning: Failed to clean up temp folder ${tempDir}:`, error) + throw error + } + }) + // Return the temp directory and cleanup function return { - files: files.reduce((acc, file) => { - acc[file.path] = file.content - return acc - }, {} as Record), tempDir, - cleanup: async () => { - try { - await fs.rm(tempDir, { recursive: true, force: true }) - } catch (error) { - console.warn(`Warning: Failed to clean up temp folder ${tempDir}:`, error) - } - }, pathJoin: (...paths: string[]) => path.join(tempDir, ...paths) } } @@ -59,9 +59,29 @@ function normalizeString(str: string): string { return str.replace(/\r\n/g, '\n').trim(); } +function treeDir(baseDir: string) { + return glob('**/*', { + cwd: baseDir, + nodir: true // Only return files, not directories + }); +} + +const baseConfig = { + docsPath: './docs', + manifestPath: './docs/manifest.json', + partialsPath: './_partials', + distPath: './dist', + ignorePaths: ["/docs/_partials"], + manifestOptions: { + wrapDefault: true, + collapseDefault: false, + hideTitleDefault: false + } +} + test('Basic build test with simple files', async () => { // Create temp environment with minimal files array - const { files, tempDir, cleanup, pathJoin } = await createTempFiles([ + const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ @@ -80,26 +100,20 @@ Testing with a simple page.` } ]) - const config = { + await build(createBlankStore(), createConfig({ + ...baseConfig, basePath: tempDir, - docsRelativePath: './docs', - docsFolder: pathJoin('./docs'), - manifestFilePath: pathJoin('./docs/manifest.json'), - partialsPath: './_partials', - distPath: pathJoin('./dist'), - ignorePaths: ["/docs/_partials"], validSdks: ["nextjs", "react"], - manifestOptions: { - wrapDefault: true, - collapseDefault: false, - hideTitleDefault: false - } - } - - await build(createBlankStore(), config) + })) expect(await fileExists(pathJoin('./dist/simple-test.mdx'))).toBe(true) - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(files['./docs/simple-test.mdx']) + expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(`--- +title: Simple Test +--- + +# Simple Test Page + +Testing with a simple page.`) expect(await fileExists(pathJoin('./dist/nextjs/manifest.json'))).toBe(true) expect(await readFile(pathJoin('./dist/nextjs/manifest.json'))).toBe(JSON.stringify({ @@ -111,6 +125,98 @@ Testing with a simple page.` navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] })) - await cleanup() }) +test('Two Docs, each grouped by a different SDK', async () => { + // Create temp environment with minimal files array + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: "React", + sdk: ["react"], + items: [ + [ + { title: "Quickstart", href: "/docs/quickstart/react" } + ] + ] + }, + { + title: "Vue", + sdk: ["vue"], + items: [ + [ + { title: "Quickstart", href: "/docs/quickstart/vue" } + ] + ] + } + ], + ] + }) + }, + { + path: './docs/quickstart/react.mdx', + content: `--- +title: Quickstart +--- + +# React Quickstart` + }, + { + path: './docs/quickstart/vue.mdx', + content: `--- +title: Quickstart +--- + +# Vue Quickstart` + } + ]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "vue"] + })) + + expect(await fileExists(pathJoin('./dist/react/manifest.json'))).toBe(true) + expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ + navigation: [ + [ + { + title: "React", + items: [ + [ + { title: "Quickstart", href: "/docs/quickstart/react" } + ] + ] + }, + ], + ] + })) + expect(await treeDir(pathJoin('./dist'))).toEqual([ + 'vue/manifest.json', + 'react/manifest.json', + 'quickstart/vue.mdx', + 'quickstart/react.mdx', + ]) + + expect(await fileExists(pathJoin('./dist/vue/manifest.json'))).toBe(true) + expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe(JSON.stringify({ + navigation: [ + [ + { + title: "Vue", + items: [ + [ + { title: "Quickstart", href: "/docs/quickstart/vue" } + ] + ] + }, + ], + ] + })) + +}) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index a105d66d83..da830b35f4 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -37,22 +37,6 @@ import { fromError } from 'zod-validation-error'; import { Node } from 'unist' import chok from 'chokidar' -export type BuildConfig = { - basePath: string; - docsRelativePath: string; - docsFolder: string; - manifestFilePath: string; - partialsPath: string; - distPath: string; - ignorePaths: string[]; - validSdks: readonly string[]; - manifestOptions: { - wrapDefault: boolean; - collapseDefault: boolean; - hideTitleDefault: boolean; - }; -} - const VALID_SDKS = [ "nextjs", "react", @@ -89,42 +73,6 @@ const tag = z.enum(["(Beta)", "(Community)"]) type Tag = z.infer -// The default config the script will run under when using npm run build -const createDefaultConfig = (): BuildConfig => { - const basePath = process.cwd(); - const docsRelativePath = './docs'; - const docsFolder = path.join(basePath, docsRelativePath); - - return { - basePath, - docsRelativePath, - docsFolder, - manifestFilePath: path.join(docsFolder, './manifest.json'), - partialsPath: './_partials', - distPath: path.join(basePath, './dist'), - ignorePaths: [ - "/docs/core-1", - '/pricing', - '/docs/reference/backend-api', - '/docs/reference/frontend-api', - '/support', - '/discord', - '/contact', - '/contact/sales', - '/contact/support', - '/blog', - '/changelog/2024-04-19', - "/docs/_partials" - ], - validSdks: VALID_SDKS, - manifestOptions: { - wrapDefault: true, - collapseDefault: false, - hideTitleDefault: false - } - }; -}; - type ManifestItem = { title: string href: string @@ -189,12 +137,12 @@ const createManifestSchema = (config: BuildConfig) => { const pleaseReport = "(this is a bug with the build script, please report)" -const isValidSdk = (sdk: string): sdk is SDK => { - return VALID_SDKS.includes(sdk as SDK) +const isValidSdk = (config: BuildConfig) => (sdk: string): sdk is SDK => { + return config.validSdks.includes(sdk as SDK) } -const isValidSdks = (sdks: string[]): sdks is SDK[] => { - return sdks.every(isValidSdk) +const isValidSdks = (config: BuildConfig) => (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk(config)) } const readManifest = (config: BuildConfig) => async (): Promise => { @@ -222,7 +170,7 @@ const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { } const readDocsFolder = (config: BuildConfig) => async () => { - return readdirp.promise(config.docsFolder, { + return readdirp.promise(config.docsPath, { type: 'files', fileFilter: (entry) => config.ignorePaths.some((ignoreItem) => `/docs/${entry.path}`.startsWith(ignoreItem)) === false && entry.path.endsWith('.mdx') @@ -230,7 +178,7 @@ const readDocsFolder = (config: BuildConfig) => async () => { } const readPartialsFolder = (config: BuildConfig) => async () => { - return readdirp.promise(path.join(config.docsFolder, config.partialsPath), { + return readdirp.promise(path.join(config.docsPath, config.partialsRelativePath), { type: 'files', fileFilter: '*.mdx', }) @@ -240,7 +188,7 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => const readFile = readMarkdownFile(config); return Promise.all(paths.map(async (markdownPath) => { - const fullPath = path.join(config.docsRelativePath, config.partialsPath, markdownPath) + const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, markdownPath) const [error, content] = await readFile(fullPath) @@ -448,17 +396,17 @@ const extractComponentPropValueFromNode = ( return undefined; } -const extractSDKsFromIfProp = (node: Node, vfile: VFile | undefined, sdkProp: string) => { +const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile | undefined, sdkProp: string) => { if (sdkProp.includes('", "') || sdkProp.includes("', '")) { const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) - if (isValidSdks(sdks)) { + if (isValidSdks(config)(sdks)) { return sdks } else { - const invalidSDKs = sdks.filter(sdk => !isValidSdk(sdk)) + const invalidSDKs = sdks.filter(sdk => !isValidSdk(config)(sdk)) vfile?.message(`sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, node.position) } } else { - if (isValidSdk(sdkProp)) { + if (isValidSdk(config)(sdkProp)) { return [sdkProp] } else { vfile?.message(`sdk "${sdkProp}" in is not a valid SDK`, node.position) @@ -506,8 +454,8 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') - if (frontmatterSDKs !== undefined && isValidSdks(frontmatterSDKs) === false) { - const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(sdk) === false) + if (frontmatterSDKs !== undefined && isValidSdks(config)(frontmatterSDKs) === false) { + const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(config)(sdk) === false) vfile.fail(`Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(VALID_SDKS)}`, node.position) return; } @@ -712,7 +660,7 @@ export const build = async ( const sdk = guide.sdk ?? tree.sdk - if (guide.sdk !== undefined) { + if (guide.sdk !== undefined && tree.sdk !== undefined) { if (guide.sdk.every(sdk => tree.sdk?.includes(sdk)) === false) { throw new Error(`Guide "${item.title}" is attempting to use ${JSON.stringify(guide.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`) } @@ -729,7 +677,7 @@ export const build = async ( const { items, ...details } = group - if (details.sdk !== undefined) { + if (details.sdk !== undefined && tree.sdk !== undefined) { if (details.sdk.every(sdk => tree.sdk?.includes(sdk)) === false) { throw new Error(`Group "${details.title}" is attempting to use ${JSON.stringify(details.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`) } @@ -750,8 +698,6 @@ export const build = async ( ) console.info('✔️ Applied manifest sdk scoping') - writeFile('m.json', JSON.stringify(sdkScopedManifest, null, 2)) - const flatSDKScopedManifest = flattenTree(sdkScopedManifest) // It would definitely be preferable we didn't need to do this markdown processing twice @@ -825,7 +771,7 @@ export const build = async ( if (sdk === undefined) return; - const sdksFilter = extractSDKsFromIfProp(node, vfile, sdk) + const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk) if (sdksFilter === undefined) return @@ -865,7 +811,7 @@ export const build = async ( const distFilePath = `${doc.href.replace("/docs/", "")}.mdx` - if (isValidSdk(distFilePath.split('/')[0])) { + if (isValidSdk(config)(distFilePath.split('/')[0])) { throw new Error(`Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`) } @@ -888,7 +834,7 @@ export const build = async ( Promise.all(coreVFiles).then((docs) => console.info(`✔️ Wrote out ${docs.length} core docs`)) - const sdkSpecificVFiles = Promise.all(VALID_SDKS.map(async (targetSdk) => { + const sdkSpecificVFiles = Promise.all(config.validSdks.map(async (targetSdk) => { // Goes through and removes any items that are not scoped to the target sdk const navigation = await traverseTree({ items: sdkScopedManifest }, @@ -961,7 +907,7 @@ export const build = async ( if (sdk === undefined) return true - const sdksFilter = extractSDKsFromIfProp(node, undefined, sdk) + const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk) if (sdksFilter === undefined) return true @@ -1036,7 +982,7 @@ const watchAndRebuild = ( ) => { const watcher = chok.watch( [ - config.docsFolder, + config.docsPath, ], { alwaysStat: true, @@ -1044,7 +990,7 @@ const watchAndRebuild = ( if (stats === undefined) return false if (stats.isDirectory()) return false - const relativePath = path.relative(config.docsFolder, filePath) + const relativePath = path.relative(config.docsPath, filePath) const isManifest = relativePath === 'manifest.json' const isMarkdown = relativePath.endsWith('.mdx') @@ -1069,9 +1015,83 @@ const watchAndRebuild = ( } +type BuildConfigOptions = { + basePath: string; + validSdks: readonly SDK[]; + docsPath: string; + manifestPath: string; + partialsPath: string; + distPath: string; + ignorePaths: string[]; + manifestOptions: { + wrapDefault: boolean; + collapseDefault: boolean; + hideTitleDefault: boolean; + }; +} + +type BuildConfig = ReturnType + +export function createConfig(config: BuildConfigOptions) { + const resolve = (relativePath: string) => { + return path.isAbsolute(relativePath) ? relativePath : path.join(config.basePath, relativePath) + } + + return { + basePath: config.basePath, + validSdks: config.validSdks, + + docsRelativePath: config.docsPath, + docsPath: resolve(config.docsPath), + + manifestRelativePath: config.manifestPath, + manifestFilePath: resolve(config.manifestPath), + + distRelativePath: config.distPath, + distPath: resolve(config.distPath), + + partialsRelativePath: config.partialsPath, + partialsPath: resolve(config.partialsPath), + + ignorePaths: config.ignorePaths, + manifestOptions: config.manifestOptions ?? { + wrapDefault: true, + collapseDefault: false, + hideTitleDefault: false + }, + } +} + const main = async () => { - // Create default configuration - const config = createDefaultConfig(); + + const config = createConfig({ + basePath: process.cwd(), + docsPath: './docs', + manifestPath: './docs/manifest.json', + partialsPath: './_partials', + distPath: './dist', + ignorePaths: [ + "/docs/core-1", + '/pricing', + '/docs/reference/backend-api', + '/docs/reference/frontend-api', + '/support', + '/discord', + '/contact', + '/contact/sales', + '/contact/support', + '/blog', + '/changelog/2024-04-19', + "/docs/_partials" + ], + validSdks: VALID_SDKS, + manifestOptions: { + wrapDefault: true, + collapseDefault: false, + hideTitleDefault: false + } + }) + const store = createBlankStore(); await build(store, config); From 611da4b81c2ad3c6ee17fea12feecc907c6941c0 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 28 Feb 2025 02:01:56 +0800 Subject: [PATCH 036/114] more tests --- scripts/build-docs.test.ts | 86 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 5ae64ca85a..0a9d8fdfd4 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -220,3 +220,89 @@ title: Quickstart })) }) + +test('sdk in frontmatter filters the docs', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react +--- + +# Simple Test Page + +Testing with a simple page.` + }]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/react/simple-test" }]] + })) + + expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- +title: Simple Test +sdk: react +--- + +# Simple Test Page + +Testing with a simple page.`) + + expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(``) + + expect(await treeDir(pathJoin('./dist'))).toEqual([ + 'simple-test.mdx', + 'react/simple-test.mdx', + 'react/manifest.json', + ]) +}) + +test('3 sdks in frontmatter generates 3 variants', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react, vue, astro +--- + +# Simple Test Page + +Testing with a simple page.` + } + ]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "vue", "astro"] + })) + + expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/react/simple-test" }]] + })) + expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe(JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/vue/simple-test" }]] + })) + expect(await readFile(pathJoin('./dist/astro/manifest.json'))).toBe(JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/astro/simple-test" }]] + })) +}) \ No newline at end of file From cb3f17db1f745f156e4ef5687d55c8f32f5c39d7 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 28 Feb 2025 05:20:13 +0800 Subject: [PATCH 037/114] more tests --- .gitignore | 1 + scripts/build-docs.test.ts | 228 ++++++++++++++++++++++++++++++++----- scripts/build-docs.ts | 2 +- 3 files changed, 202 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 176f1bda5a..73ec6f8efc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # testing /coverage +.temp-test/ # next.js /.next/ diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 0a9d8fdfd4..4a98919d76 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -4,15 +4,47 @@ import os from 'node:os' import {glob} from 'glob'; -import { expect, onTestFinished, test } from 'vitest' +import { expect, onTestFinished, test, vi } from 'vitest' import { build, createBlankStore, createConfig } from './build-docs' -async function createTempFiles(files: { - path: string; - content: string; -}[]) { +const tempConfig = { + // Set to true to use local repo temp directory instead of system temp + useLocalTemp: false, + + // Local temp directory path (relative to project root) + localTempPath: './.temp-test', + + // Whether to preserve temp directories after tests + // (helpful for debugging, but requires manual cleanup) + preserveTemp: false +} + +async function createTempFiles( + files: { path: string; content: string }[], + options?: { + prefix?: string; // Prefix for the temp directory name + preserveTemp?: boolean; // Override global preserveTemp setting + useLocalTemp?: boolean; // Override global useLocalTemp setting + } +) { + const prefix = options?.prefix || 'clerk-docs-test-' + const preserve = options?.preserveTemp ?? tempConfig.preserveTemp + const useLocalTemp = options?.useLocalTemp ?? tempConfig.useLocalTemp + + // Determine base directory for temp files + let baseDir: string + + if (useLocalTemp) { + // Use local directory in the repo + baseDir = tempConfig.localTempPath + await fs.mkdir(baseDir, { recursive: true }) + } else { + // Use system temp directory + baseDir = os.tmpdir() + } + // Create temp folder with unique name - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'clerk-docs-test-')) + const tempDir = await fs.mkdtemp(path.join(baseDir, prefix)) // Create all files for (const file of files) { @@ -26,19 +58,37 @@ async function createTempFiles(files: { await fs.writeFile(filePath, file.content) } - onTestFinished(async () => { - try { - await fs.rm(tempDir, { recursive: true, force: true }) - } catch (error) { - console.warn(`Warning: Failed to clean up temp folder ${tempDir}:`, error) - throw error - } - }) + // Register cleanup unless preserveTemp is true + if (!preserve) { + onTestFinished(async () => { + try { + await fs.rm(tempDir, { recursive: true, force: true }) + } catch (error) { + console.warn(`Warning: Failed to clean up temp folder ${tempDir}:`, error) + } + }) + } else { + // Log the location for manual inspection + console.log(`Preserving temp directory for inspection: ${tempDir}`) + } - // Return the temp directory and cleanup function + // Return useful helpers return { tempDir, - pathJoin: (...paths: string[]) => path.join(tempDir, ...paths) + pathJoin: (...paths: string[]) => path.join(tempDir, ...paths), + + // Get a list of all files in the temp directory + listFiles: async () => { + return glob('**/*', { + cwd: tempDir, + nodir: true + }) + }, + + // Read file contents + readFile: async (filePath: string): Promise => { + return fs.readFile(path.join(tempDir, filePath), 'utf-8') + } } } @@ -196,12 +246,6 @@ title: Quickstart ], ] })) - expect(await treeDir(pathJoin('./dist'))).toEqual([ - 'vue/manifest.json', - 'react/manifest.json', - 'quickstart/vue.mdx', - 'quickstart/react.mdx', - ]) expect(await fileExists(pathJoin('./dist/vue/manifest.json'))).toBe(true) expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe(JSON.stringify({ @@ -219,6 +263,14 @@ title: Quickstart ] })) + const distFiles = await treeDir(pathJoin('./dist')) + + expect(distFiles.length).toBe(4) + expect(distFiles).toContain('vue/manifest.json') + expect(distFiles).toContain('react/manifest.json') + expect(distFiles).toContain('quickstart/vue.mdx') + expect(distFiles).toContain('quickstart/react.mdx') + }) test('sdk in frontmatter filters the docs', async () => { @@ -262,11 +314,12 @@ Testing with a simple page.`) expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(``) - expect(await treeDir(pathJoin('./dist'))).toEqual([ - 'simple-test.mdx', - 'react/simple-test.mdx', - 'react/manifest.json', - ]) + const distFiles = await treeDir(pathJoin('./dist')) + + expect(distFiles.length).toBe(3) + expect(distFiles).toContain('simple-test.mdx') + expect(distFiles).toContain('react/simple-test.mdx') + expect(distFiles).toContain('react/manifest.json') }) test('3 sdks in frontmatter generates 3 variants', async () => { @@ -305,4 +358,123 @@ Testing with a simple page.` expect(await readFile(pathJoin('./dist/astro/manifest.json'))).toBe(JSON.stringify({ navigation: [[{ title: "Simple Test", href: "/docs/astro/simple-test" }]] })) -}) \ No newline at end of file + + const distFiles = await treeDir(pathJoin('./dist')) + + expect(distFiles.length).toBe(7) + expect(distFiles).toContain('simple-test.mdx') + expect(distFiles).toContain('react/simple-test.mdx') + expect(distFiles).toContain('react/manifest.json') + expect(distFiles).toContain('vue/simple-test.mdx') + expect(distFiles).toContain('vue/manifest.json') + expect(distFiles).toContain('astro/simple-test.mdx') + expect(distFiles).toContain('astro/manifest.json') +}) + +test(' content filtered out when sdk is in frontmatter', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react, expo +--- + +# Simple Test Page + + + React Content + + +Testing with a simple page.` + } + ]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "expo"] + })) + + expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('React Content') + + expect(await readFile(pathJoin('./dist/expo/simple-test.mdx'))).not.toContain('React Content') +}) + +test('Invalid SDK in frontmatter fails the build', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react, expo, coffeescript +--- + +# Simple Test Page + +Testing with a simple page.` + } + ]) + + const promise = build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "expo"] + })) + + await expect(promise).rejects.toThrow(`Invalid SDK ["coffeescript"], the valid SDKs are ["react","expo"]`) +}) + +test('Invalid SDK in fails the build', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react, expo +--- + +# Simple Test Page + + + astro Content + + +Testing with a simple page.` + } + ]) + + const logSpy = vi.spyOn(console, 'info') + + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "expo"] + })) + + + expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx +8:1-10:6 warning sdk \"astro\" in is not a valid SDK + +⚠ 1 warning`) +}) + diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index da830b35f4..6aca0ef68f 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -456,7 +456,7 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( if (frontmatterSDKs !== undefined && isValidSdks(config)(frontmatterSDKs) === false) { const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(config)(sdk) === false) - vfile.fail(`Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(VALID_SDKS)}`, node.position) + vfile.fail(`Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(config.validSdks)}`, node.position) return; } From 7163efd439f5108000f71d16101be39f0454963b Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 28 Feb 2025 06:04:02 +0800 Subject: [PATCH 038/114] More tests :) --- scripts/build-docs.test.ts | 292 +++++++++++++++++++++++++++++++++++-- 1 file changed, 278 insertions(+), 14 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 4a98919d76..f1892eebbf 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -1,19 +1,19 @@ import fs from 'node:fs/promises' import path from 'node:path' import os from 'node:os' -import {glob} from 'glob'; +import { glob } from 'glob'; -import { expect, onTestFinished, test, vi } from 'vitest' +import { describe, expect, onTestFinished, test, vi } from 'vitest' import { build, createBlankStore, createConfig } from './build-docs' const tempConfig = { // Set to true to use local repo temp directory instead of system temp useLocalTemp: false, - + // Local temp directory path (relative to project root) localTempPath: './.temp-test', - + // Whether to preserve temp directories after tests // (helpful for debugging, but requires manual cleanup) preserveTemp: false @@ -21,7 +21,7 @@ const tempConfig = { async function createTempFiles( files: { path: string; content: string }[], - options?: { + options?: { prefix?: string; // Prefix for the temp directory name preserveTemp?: boolean; // Override global preserveTemp setting useLocalTemp?: boolean; // Override global useLocalTemp setting @@ -30,10 +30,10 @@ async function createTempFiles( const prefix = options?.prefix || 'clerk-docs-test-' const preserve = options?.preserveTemp ?? tempConfig.preserveTemp const useLocalTemp = options?.useLocalTemp ?? tempConfig.useLocalTemp - + // Determine base directory for temp files let baseDir: string - + if (useLocalTemp) { // Use local directory in the repo baseDir = tempConfig.localTempPath @@ -42,7 +42,7 @@ async function createTempFiles( // Use system temp directory baseDir = os.tmpdir() } - + // Create temp folder with unique name const tempDir = await fs.mkdtemp(path.join(baseDir, prefix)) @@ -76,15 +76,15 @@ async function createTempFiles( return { tempDir, pathJoin: (...paths: string[]) => path.join(tempDir, ...paths), - + // Get a list of all files in the temp directory listFiles: async () => { - return glob('**/*', { + return glob('**/*', { cwd: tempDir, - nodir: true + nodir: true }) }, - + // Read file contents readFile: async (filePath: string): Promise => { return fs.readFile(path.join(tempDir, filePath), 'utf-8') @@ -110,7 +110,7 @@ function normalizeString(str: string): string { } function treeDir(baseDir: string) { - return glob('**/*', { + return glob('**/*', { cwd: baseDir, nodir: true // Only return files, not directories }); @@ -291,7 +291,7 @@ sdk: react # Simple Test Page Testing with a simple page.` - }]) + }]) await build(createBlankStore(), createConfig({ ...baseConfig, @@ -478,3 +478,267 @@ Testing with a simple page.` ⚠ 1 warning`) }) +describe('Includes and Partials', () => { + test(' Component embeds content in to guide', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/_partials/test-partial.mdx', + content: `Test Partial Content` + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + + + +# Simple Test Page` + } + ]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toContain('Test Partial Content') + }) + + test('Invalid partial src fails the build', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + + + +# Simple Test Page` + } + ]) + + const logSpy = vi.spyOn(console, 'info') + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx +5:1-5:41 warning Partial /docs/_partials/test-partial.mdx not found + +⚠ 1 warning`) + }) + + test('Fail if partial is within a partial', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/_partials/test-partial-1.mdx', + content: `` + }, + { + path: './docs/_partials/test-partial-2.mdx', + content: `Test Partial Content` + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + + + +# Simple Test Page` + } + ]) + + const promise = build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + await expect(promise).rejects.toThrow(`Partials inside of partials is not yet supported`) + }) +}) + +describe('Link Validation and Processing', () => { + test('Fail if link is to non-existent page', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +[Non Existent Page](/docs/non-existent-page) + +# Simple Test Page` + } + ]) + + const logSpy = vi.spyOn(console, 'info') + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx +5:1-5:45 warning Guide /docs/non-existent-page not found + +⚠ 1 warning`) + }) + + test('Warn if link is to existent page but with invalid hash', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +[Simple Test](/docs/simple-test#non-existent-hash) + +# Simple Test Page` + } + ]) + + const logSpy = vi.spyOn(console, 'info') + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx +5:1-5:51 warning Hash "non-existent-hash" not found in /docs/simple-test + +⚠ 1 warning`) + }) + + + test('Pick up on id in heading for hash alias', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Simple Test", href: "/docs/simple-test" }, + { title: "Headings", href: "/docs/headings" } + ]] + }) + }, + { + path: './docs/headings.mdx', + content: `--- +title: Headings +--- + +# test {{ id: 'my-heading' }}` + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +[Headings](/docs/headings#my-heading)` + } + ]) + + const logSpy = vi.spyOn(console, 'info') + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx +5:1-5:38 warning Hash "my-heading" not found in /docs/headings + +⚠ 1 warning`) + }) + + + test('Swap out links for when a link points to an sdk generated guide', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "SDK Filtered Page", href: "/docs/sdk-filtered-page" }, { title: "Core Page", href: "/docs/core-page" }]] + }) + }, + { + path: './docs/sdk-filtered-page.mdx', + content: `--- +title: SDK Filtered Page +sdk: react, nextjs +--- + +SDK filtered page` + }, + { + path: './docs/core-page.mdx', + content: `--- +title: Core Page +--- + +# Core page + +[SDK Filtered Page](/docs/sdk-filtered-page) +` + } + ]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "nextjs"] + })) + + expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain(` + SDK Filtered Page +`) + expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain(` + SDK Filtered Page +`) + }) +}) \ No newline at end of file From 451e2605fb179eaf8e09c7de22d52e745aca4a91 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 1 Mar 2025 00:17:15 +0800 Subject: [PATCH 039/114] tests :D --- package.json | 2 +- scripts/build-docs.test.ts | 608 ++++++++++++++++++++++++++++++++++--- scripts/build-docs.ts | 14 +- 3 files changed, 581 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 679a0724b4..2f832e2f5e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint:check-frontmatter": "node ./scripts/check-frontmatter.mjs", "build": "tsx ./scripts/build-docs.ts", "dev": "tsx ./scripts/build-docs.ts --watch", - "test": "vitest" + "test": "vitest --silent" }, "devDependencies": { "@sindresorhus/slugify": "^2.2.1", diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index f1892eebbf..4d3663399e 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -462,20 +462,13 @@ Testing with a simple page.` } ]) - const logSpy = vi.spyOn(console, 'info') - - - await build(createBlankStore(), createConfig({ + const output = await build(createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, validSdks: ["react", "expo"] })) - - expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx -8:1-10:6 warning sdk \"astro\" in is not a valid SDK - -⚠ 1 warning`) + expect(output).toContain(`warning sdk \"astro\" in is not a valid SDK`) }) describe('Includes and Partials', () => { @@ -532,18 +525,13 @@ title: Simple Test } ]) - const logSpy = vi.spyOn(console, 'info') - - await build(createBlankStore(), createConfig({ + const output = await build(createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, validSdks: ["react"] })) - expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx -5:1-5:41 warning Partial /docs/_partials/test-partial.mdx not found - -⚠ 1 warning`) + expect(output).toContain(`warning Partial /docs/_partials/test-partial.mdx not found`) }) test('Fail if partial is within a partial', async () => { @@ -582,6 +570,35 @@ title: Simple Test await expect(promise).rejects.toThrow(`Partials inside of partials is not yet supported`) }) + + test(`Warning if src doesn't start with "_partials/"`, async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + + + +# Simple Test Page` + } + ]) + + const output = await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + expect(output).toContain(`warning prop "src" must start with "_partials/"`) + }) }) describe('Link Validation and Processing', () => { @@ -605,18 +622,50 @@ title: Simple Test } ]) - const logSpy = vi.spyOn(console, 'info') - - await build(createBlankStore(), createConfig({ + const output = await build(createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, validSdks: ["react"] })) - expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx -5:1-5:45 warning Guide /docs/non-existent-page not found + expect(output).toContain(`warning Guide /docs/non-existent-page not found`) + }) + + test('Validate link between two pages is valid', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] + }) + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +[Core Page](/docs/core-page) + +# Simple Test Page` + }, + { + path: './docs/core-page.mdx', + content: `--- +title: Core Page +--- + +# Core Page` + } + ]) -⚠ 1 warning`) + const output = await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })) + + expect(output).not.toContain(`warning Guide /docs/core-page not found`) }) test('Warn if link is to existent page but with invalid hash', async () => { @@ -639,22 +688,18 @@ title: Simple Test } ]) - const logSpy = vi.spyOn(console, 'info') - await build(createBlankStore(), createConfig({ + const output = await build(createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, validSdks: ["react"] })) - expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx -5:1-5:51 warning Hash "non-existent-hash" not found in /docs/simple-test - -⚠ 1 warning`) + expect(output).toContain(`warning Hash "non-existent-hash" not found in /docs/simple-test`) }) - - test('Pick up on id in heading for hash alias', async () => { + // skipping for now as it fails and needs to be fixed + test.skip('Pick up on id in heading for hash alias', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', @@ -683,18 +728,13 @@ title: Simple Test } ]) - const logSpy = vi.spyOn(console, 'info') - - await build(createBlankStore(), createConfig({ + const output = await build(createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, validSdks: ["react"] })) - expect(logSpy).toHaveBeenCalledWith(`/docs/simple-test.mdx -5:1-5:38 warning Hash "my-heading" not found in /docs/headings - -⚠ 1 warning`) + expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) }) @@ -741,4 +781,496 @@ title: Core Page SDK Filtered Page `) }) -}) \ No newline at end of file +}) + +describe('SDK Filtering', () => { + + test('should handle SDK filtering with deeply nested manifest structures', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [{ + title: "Top Level", + items: [ + [{ + title: "Mid Level", + sdk: ["react", "nextjs"], + items: [ + [{ + title: "Deep Level", + sdk: ["nextjs"], + items: [ + [{ title: "Deeply Nested Page", href: "/docs/deeply-nested-nextjs" }] + ] + },{ + title: "Deep Level", + sdk: ["react"], + items: [ + [{ title: "Deeply Nested Page", href: "/docs/deeply-nested-react" }] + ] + }] + ] + }] + ] + }] + ] + }) + }, + { + path: './docs/deeply-nested-nextjs.mdx', + content: `--- +title: Deeply Nested Page +sdk: nextjs +--- + +Content for Next.js users.` + }, + { + path: './docs/deeply-nested-react.mdx', + content: `--- +title: Deeply Nested Page +sdk: react +--- + +Content for React users.` + } + ]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "nextjs", "javascript-frontend"] + })) + + // Page should be available in nextjs (from manifest deep nesting) + expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-react.mdx'))).toBe(false) + expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toContain("Content for Next.js users.") + expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).not.toContain("Content for React users.") + + // Page should be available in react (from parent manifest item) + expect(await fileExists(pathJoin('./dist/react/deeply-nested-react.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/react/deeply-nested-nextjs.mdx'))).toBe(false) + expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).toContain("Content for React users.") + expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).not.toContain("Content for Next.js users.") + + // Page should NOT be available in javascript-frontend (filtered out by manifest) + expect(await fileExists(pathJoin('./dist/javascript-frontend/deeply-nested-nextjs.mdx'))).toBe(false) + expect(await fileExists(pathJoin('./dist/javascript-frontend/deeply-nested-react.mdx'))).toBe(false) + }); + + test('should correctly process multiple blocks with different SDKs in a single document', async () => { + const { tempDir, pathJoin }= await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { + title: "Multiple SDK Blocks", + href: "/multiple-sdk-blocks" + } + ]] + }) + }, + { + path: './docs/multiple-sdk-blocks.mdx', + content: `--- +title: Multiple SDK Blocks +sdk: react, nextjs, javascript-frontend +--- + +# Multiple SDK Blocks + + + This content is for React users only. + + + + This content is for Next.js users only. + + + + This content is for JavaScript Frontend users only. + + +Common content for all SDKs.` + } + ]); + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "nextjs", "javascript-frontend"] + })); + + // Check React version + expect(await fileExists(pathJoin('./dist/react/multiple-sdk-blocks.mdx'))).toBe(true); + const reactContent = await readFile(pathJoin('./dist/react/multiple-sdk-blocks.mdx')); + expect(reactContent).toContain("This content is for React users only."); + expect(reactContent).not.toContain("This content is for Next.js users only."); + expect(reactContent).not.toContain("This content is for JavaScript Frontend users only."); + expect(reactContent).toContain("Common content for all SDKs."); + + // Check Next.js version + expect(await fileExists(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx'))).toBe(true); + const nextjsContent = await readFile(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx')); + expect(nextjsContent).not.toContain("This content is for React users only."); + expect(nextjsContent).toContain("This content is for Next.js users only."); + expect(nextjsContent).not.toContain("This content is for JavaScript Frontend users only."); + expect(nextjsContent).toContain("Common content for all SDKs."); + + // Check JavaScript Frontend version + expect(await fileExists(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx'))).toBe(true); + const jsContent = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx')); + expect(jsContent).not.toContain("This content is for React users only."); + expect(jsContent).not.toContain("This content is for Next.js users only."); + expect(jsContent).toContain("This content is for JavaScript Frontend users only."); + expect(jsContent).toContain("Common content for all SDKs."); + }); + + test('should handle nested components correctly', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [{ + title: "Parent Group", + sdk: ["react", "nextjs"], + items: [ + [{ title: "Nested SDK Page", href: "/docs/nested-sdk-page" }] + ] + }] + ] + }) + }, + { + path: './docs/nested-sdk-page.mdx', + content: `--- +title: Nested SDK Page +sdk: react, nextjs +--- + +# Nested SDK Filtering + + + This content is for React users. + + + This is nested content specifically for Next.js users who are also using React. + + + +Common content for all SDKs.` + } + ]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "nextjs"] + })) + + // Check React output has only React content + const reactOutput = await readFile(pathJoin('./dist/react/nested-sdk-page.mdx')) + expect(reactOutput).toContain("This content is for React users.") + expect(reactOutput).not.toContain("This is nested content specifically for Next.js users") + + // Check Next.js output has both React and Next.js content + const nextjsOutput = await readFile(pathJoin('./dist/nextjs/nested-sdk-page.mdx')) + expect(nextjsOutput).toContain("This content is for React users.") + expect(nextjsOutput).toContain("This is nested content specifically for Next.js users") + + }); + + test('should support components with array syntax for multiple SDKs', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { + title: "Multiple SDK Test", + href: "/docs/multiple-sdk-test" + } + ]] + }) + }, + { + path: './docs/multiple-sdk-test.mdx', + content: `--- +title: Multiple SDK Test +sdk: react, nextjs, javascript-frontend +--- + +# Multiple SDK Test + + + This content is for React and Next.js users. + + + + This content is for JavaScript Frontend users. + + +Common content for all SDKs.` + } + ]) + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "nextjs", "javascript-frontend"] + })) + + // Check React output has React content but not JavaScript Frontend content + const reactOutput = await readFile(pathJoin('./dist/react/multiple-sdk-test.mdx')) + expect(reactOutput).toContain("This content is for React and Next.js users.") + expect(reactOutput).not.toContain("This content is for JavaScript Frontend users.") + + // Check Next.js output has Next.js content but not JavaScript Frontend content + const nextjsOutput = await readFile(pathJoin('./dist/nextjs/multiple-sdk-test.mdx')) + expect(nextjsOutput).toContain("This content is for React and Next.js users.") + expect(nextjsOutput).not.toContain("This content is for JavaScript Frontend users.") + + // Check JavaScript Frontend output has JavaScript Frontend content but not React/Next.js content + const jsOutput = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-test.mdx')) + expect(jsOutput).toContain("This content is for JavaScript Frontend users.") + expect(jsOutput).not.toContain("This content is for React and Next.js users.") + }); +}); + +describe('Manifest Handling', () => { + test('should apply manifest options (wrapDefault, collapseDefault, hideTitleDefault) correctly', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Group One", items: [[{ title: "Item One", href: "/docs/item-one" }]], wrap: true, collapse: true, hideTitle: false }, + { title: "Group Two", items: [[{ title: "Item Two", href: "/docs/item-two" }]], wrap: true, collapse: false, hideTitle: true }, + { title: "Group Three", items: [[{ title: "Item Three", href: "/docs/item-three" }]], wrap: false, collapse: true, hideTitle: false }, + { title: "Group Four", items: [[{ title: "Item Four", href: "/docs/item-four" }]], wrap: false, collapse: false, hideTitle: true }, + ]] + }) + }, + { path: "./docs/item-one.mdx", content: `---\ntitle: Item One\n---\nItem One` }, + { path: "./docs/item-two.mdx", content: `---\ntitle: Item Two\n---\nItem Two` }, + { path: "./docs/item-three.mdx", content: `---\ntitle: Item Three\n---\nItem Three` }, + { path: "./docs/item-four.mdx", content: `---\ntitle: Item Four\n---\nItem Four` }, + ]) + + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["nextjs"], + manifestOptions: { + wrapDefault: false, + collapseDefault: false, + hideTitleDefault: false + } + })) + + const manifest = JSON.parse(await readFile(pathJoin('./dist/nextjs/manifest.json'))) + const groups = manifest.navigation[0] + + expect(groups[0].wrap).toBe(true) + expect(groups[0].collapse).toBe(true) + expect(groups[0].hideTitle).toBe(undefined) + + expect(groups[1].wrap).toBe(true) + expect(groups[1].collapse).toBe(undefined) + expect(groups[1].hideTitle).toBe(true) + + expect(groups[2].wrap).toBe(undefined) + expect(groups[2].collapse).toBe(true) + expect(groups[2].hideTitle).toBe(undefined) + + expect(groups[3].wrap).toBe(undefined) + expect(groups[3].collapse).toBe(undefined) + expect(groups[3].hideTitle).toBe(true) + + }); + + test('should properly inherit SDK filtering from parent groups to child items', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { + title: "SDK Group", + sdk: ["nextjs", "react"], + items: [[ + { + title: "Sub Group", + items: [[ + { title: "SDK Item", href: "/docs/sdk-item" }, + { title: "Nested Group", items: [[{ title: "Nested Item", href: "/docs/nested-item" }]] } + ]] + } + ]] + }, + { + title: "Generic Group", + items: [[ + { + title: "Sub Group", + items: [[ + { title: "Generic Item", href: "/docs/generic-item" } + ]] + } + ]] + }, + { + title: "Vue Group", + sdk: ["vue"], + items: [[ + { + title: "Sub Group", + items: [[{ title: "Vue Item", href: "/docs/vue-item" }]] + } + ]] + } + ]] + }) + }, + { + path: "./docs/sdk-item.mdx", + content: `---\ntitle: SDK Item\n---\nSDK specific content` + }, + { + path: "./docs/nested-item.mdx", + content: `---\ntitle: Nested Item\n---\nNested SDK specific content` + }, + { + path: "./docs/generic-item.mdx", + content: `---\ntitle: Generic Item\n---\nGeneric content` + }, + { + path: "./docs/vue-item.mdx", + content: `---\ntitle: Vue Item\n---\nVue specific content` + } + ]); + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["nextjs", "react", "vue"], + })); + + // Check nextjs manifest + const nextjsManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/nextjs/manifest.json'), 'utf-8')); + const nextjsGroups = nextjsManifest.navigation[0]; + + expect(nextjsGroups[0].items[0][0].items[0][0].title).toBe("SDK Item"); + expect(nextjsGroups[0].items[0][0].items[0][1].title).toBe("Nested Group"); + expect(nextjsGroups[1].items[0][0].items[0][0].title).toBe("Generic Item"); + expect(nextjsGroups[2]).toBe(undefined); + + + // Check react manifest + const reactManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/react/manifest.json'), 'utf-8')); + const reactGroups = reactManifest.navigation[0]; + + expect(reactGroups[0].items[0][0].items[0][0].title).toBe("SDK Item"); + expect(reactGroups[0].items[0][0].items[0][1].title).toBe("Nested Group"); + expect(reactGroups[1].items[0][0].items[0][0].title).toBe("Generic Item"); + expect(reactGroups[2]).toBe(undefined); + + + // Check vue manifest + const vueManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/vue/manifest.json'), 'utf-8')); + const vueGroups = vueManifest.navigation[0]; + + expect(vueGroups[0].items[0][0].items[0][0].title).toBe("Generic Item"); + expect(vueGroups[1].items[0][0].items[0][0].title).toBe("Vue Item"); + expect(vueGroups[2]).toBe(undefined); + }); + +}); + +// describe('Path and File Handling', () => { +// test('should ignore paths specified in ignorePaths during processing', async () => { +// // Test implementation +// }); + +// test('should detect file path conflicts when a core doc path matches an SDK path', async () => { +// // Test implementation +// }); + +// test('should remove .mdx suffix from markdown links', async () => { +// // Test implementation +// }); +// }); + +// describe('Edge Cases', () => { +// test('should handle empty manifest or empty docs directory gracefully', async () => { +// // Test implementation +// }); + +// test('should process very large docs/manifests efficiently', async () => { +// // Test implementation +// }); + +// test('should report errors for malformed frontmatter', async () => { +// // Test implementation +// }); + +// test('should handle invalid YAML in frontmatter appropriately', async () => { +// // Test implementation +// }); + +// test('should require and validate mandatory frontmatter fields', async () => { +// // Test implementation +// }); + +// test('should properly handle special characters in paths and links', async () => { +// // Test implementation +// }); +// }); + +// describe('File Watching', () => { +// test('should correctly detect file changes in watch mode', async () => { +// // Test implementation +// }); + +// test('should rebuild only affected files when possible', async () => { +// // Test implementation +// }); + +// test('should maintain performance with frequent file changes', async () => { +// // Test implementation +// }); +// }); + +// describe('Error Reporting', () => { +// test('should produce clear and informative error messages for validation failures', async () => { +// // Test implementation +// }); + +// test('should handle errors when a referenced document exists but is invalid', async () => { +// // Test implementation +// }); + +// test('should complete build workflow when errors are present in some files', async () => { +// // Test implementation +// }); +// }); + +// describe('Advanced Features', () => { +// test('should correctly handle links with anchors to specific sections of documents', async () => { +// // Test implementation +// }); + +// test('should process target="_blank" links in manifest correctly', async () => { +// // Test implementation +// }); + +// test('should generate appropriate landing pages for SDK-specific docs', async () => { +// // Test implementation +// }); +// }); \ No newline at end of file diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 6aca0ef68f..543390e2c1 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -397,16 +397,20 @@ const extractComponentPropValueFromNode = ( } const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile | undefined, sdkProp: string) => { - if (sdkProp.includes('", "') || sdkProp.includes("', '")) { + + const isValidItem = isValidSdk(config) + const isValidItems = isValidSdks(config) + + if (sdkProp.includes('", "') || sdkProp.includes("', '") || sdkProp.includes('["') || sdkProp.includes('"]')) { const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) - if (isValidSdks(config)(sdks)) { + if (isValidItems(sdks)) { return sdks } else { - const invalidSDKs = sdks.filter(sdk => !isValidSdk(config)(sdk)) + const invalidSDKs = sdks.filter(sdk => !isValidItem(sdk)) vfile?.message(`sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, node.position) } } else { - if (isValidSdk(config)(sdkProp)) { + if (isValidItem(sdkProp)) { return [sdkProp] } else { vfile?.message(`sdk "${sdkProp}" in is not a valid SDK`, node.position) @@ -974,6 +978,8 @@ export const build = async ( if (output !== "") { console.info(output) } + + return output } const watchAndRebuild = ( From f8bff1ea35ec3a8c3a71dd05474f4d7389b3e853 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 4 Mar 2025 23:36:55 +0800 Subject: [PATCH 040/114] finish off the tests --- scripts/build-docs.test.ts | 578 +++++++++++++++++++++++++++++++------ scripts/build-docs.ts | 36 ++- 2 files changed, 512 insertions(+), 102 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 4d3663399e..f21cf8e05b 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -4,7 +4,7 @@ import os from 'node:os' import { glob } from 'glob'; -import { describe, expect, onTestFinished, test, vi } from 'vitest' +import { describe, expect, onTestFinished, test } from 'vitest' import { build, createBlankStore, createConfig } from './build-docs' const tempConfig = { @@ -698,8 +698,7 @@ title: Simple Test expect(output).toContain(`warning Hash "non-existent-hash" not found in /docs/simple-test`) }) - // skipping for now as it fails and needs to be fixed - test.skip('Pick up on id in heading for hash alias', async () => { + test('Pick up on id in heading for hash alias', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', @@ -774,12 +773,7 @@ title: Core Page validSdks: ["react", "nextjs"] })) - expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain(` - SDK Filtered Page -`) - expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain(` - SDK Filtered Page -`) + expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain(`SDK Filtered Page`) }) }) @@ -1193,84 +1187,488 @@ describe('Manifest Handling', () => { }); -// describe('Path and File Handling', () => { -// test('should ignore paths specified in ignorePaths during processing', async () => { -// // Test implementation -// }); - -// test('should detect file path conflicts when a core doc path matches an SDK path', async () => { -// // Test implementation -// }); - -// test('should remove .mdx suffix from markdown links', async () => { -// // Test implementation -// }); -// }); - -// describe('Edge Cases', () => { -// test('should handle empty manifest or empty docs directory gracefully', async () => { -// // Test implementation -// }); - -// test('should process very large docs/manifests efficiently', async () => { -// // Test implementation -// }); - -// test('should report errors for malformed frontmatter', async () => { -// // Test implementation -// }); - -// test('should handle invalid YAML in frontmatter appropriately', async () => { -// // Test implementation -// }); - -// test('should require and validate mandatory frontmatter fields', async () => { -// // Test implementation -// }); - -// test('should properly handle special characters in paths and links', async () => { -// // Test implementation -// }); -// }); - -// describe('File Watching', () => { -// test('should correctly detect file changes in watch mode', async () => { -// // Test implementation -// }); - -// test('should rebuild only affected files when possible', async () => { -// // Test implementation -// }); - -// test('should maintain performance with frequent file changes', async () => { -// // Test implementation -// }); -// }); - -// describe('Error Reporting', () => { -// test('should produce clear and informative error messages for validation failures', async () => { -// // Test implementation -// }); - -// test('should handle errors when a referenced document exists but is invalid', async () => { -// // Test implementation -// }); - -// test('should complete build workflow when errors are present in some files', async () => { -// // Test implementation -// }); -// }); - -// describe('Advanced Features', () => { -// test('should correctly handle links with anchors to specific sections of documents', async () => { -// // Test implementation -// }); - -// test('should process target="_blank" links in manifest correctly', async () => { -// // Test implementation -// }); - -// test('should generate appropriate landing pages for SDK-specific docs', async () => { -// // Test implementation -// }); -// }); \ No newline at end of file +describe('Path and File Handling', () => { + test('should ignore paths specified in ignorePaths during processing', async () => { + const { tempDir, pathJoin, listFiles } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Regular Guide", href: "/docs/regular-guide" }, + { title: "Ignored Guide", href: "/docs/ignored/ignored-guide" } + ]] + }) + }, + { + path: './docs/regular-guide.mdx', + content: `--- +title: Regular Guide +--- + +# Regular Guide Content` + }, + { + path: './docs/ignored/ignored-guide.mdx', + content: `--- +title: Ignored Guide +--- + +# Ignored Guide Content` + } + ]); + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"], + ignorePaths: ["/docs/ignored"] + })); + + // Check that only the regular guide was processed + const distFiles = (await listFiles()).filter(file => file.startsWith('dist/')) + + expect(distFiles).toContain('dist/regular-guide.mdx'); + expect(distFiles).toContain('dist/react/manifest.json'); + expect(distFiles).not.toContain('dist/ignored/ignored-guide.mdx'); + + // Verify that the manifest was filtered correctly + expect(JSON.parse(await readFile(pathJoin('./dist/react/manifest.json')))).toEqual({ + navigation: [[ + { + title: "Regular Guide", + href: "/docs/regular-guide" + }, + { + title: "Ignored Guide", + href: "/docs/ignored/ignored-guide" + } + ]] + }) + }); + + test('should detect file path conflicts when a core doc path matches an SDK path', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "React Guide", href: "/docs/react/conflict" }]] + }) + }, + { + path: './docs/react/conflict.mdx', + content: `--- +title: React Guide +--- + +# This will cause a conflict because it's in a path that starts with "react"` + } + ]); + + // This should throw an error because the file path starts with an SDK name + const promise = build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + await expect(promise).rejects.toThrow('Attempting to write out a core doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict'); + }); + + test('should remove .mdx suffix from markdown links', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Source Page", href: "/docs/source-page" }, + { title: "Target Page", href: "/docs/target-page" } + ]] + }) + }, + { + path: './docs/source-page.mdx', + content: `--- +title: Source Page +--- + +# Source Page + +[Link to Target with .mdx](/docs/target-page.mdx) +[Link to Target without .mdx](/docs/target-page)` + }, + { + path: './docs/target-page.mdx', + content: `--- +title: Target Page +--- + +# Target Page Content` + } + ]); + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + // Both links should be processed to remove .mdx + const sourcePageContent = await readFile(pathJoin('./dist/source-page.mdx')); + + // The link should have .mdx removed + expect(sourcePageContent).toContain('[Link to Target with .mdx](/docs/target-page)'); + expect(sourcePageContent).toContain('[Link to Target without .mdx](/docs/target-page)'); + expect(sourcePageContent).not.toContain('/docs/target-page.mdx'); + }); +}); + +describe('Edge Cases', () => { + + test('should report errors for malformed frontmatter', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Malformed Frontmatter", href: "/docs/malformed-frontmatter" }]] + }) + }, + { + path: './docs/malformed-frontmatter.mdx', + content: `--- +title: Malformed Frontmatter +description: \`This frontmatter has an unbalanced quote +--- + +# Content with malformed frontmatter` + } + ]); + + // This should throw a parsing error + const promise = build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + await expect(promise).rejects.toThrow("Plain value cannot start with reserved character"); + }); + + test('should require and validate mandatory frontmatter fields', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Missing Title", href: "/docs/missing-title" }]] + }) + }, + { + path: './docs/missing-title.mdx', + content: `--- +description: This frontmatter is missing the required title field +--- + +# Content with missing title in frontmatter` + } + ]); + + // This should throw an error about missing title + const promise = build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + await expect(promise).rejects.toThrow('Frontmatter must have a "title" property'); + }); + + test('should fail on special characters in paths', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Space in url", href: "/docs/space in url" }, + ]] + }) + }, + { + path: './docs/space in url.mdx', + content: `---\ntitle: Space in url\n---` + } + ]); + + const promise = build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + await expect(promise).rejects.toThrow('Href "/docs/space in url" contains characters that will be encoded by the browser, please remove them') + }); +}); + +describe('Error Reporting', () => { + test('should produce clear and informative error messages for validation failures', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "Validation Error", href: "/docs/validation-error" }]] + }) + }, + { + path: './docs/validation-error.mdx', + content: `--- +title: Validation Error +sdk: react, invalid-sdk +--- + +# Validation Error Page + +This page has an invalid SDK in frontmatter.` + } + ]); + + // This should throw an error with specific message about invalid SDK + const promise = build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + await expect(promise).rejects.toThrow('Invalid SDK ["invalid-sdk"], the valid SDKs are ["react"]'); + }); + + test('should handle errors when a referenced document exists but is invalid', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Valid Document", href: "/docs/valid-document" }, + { title: "Invalid Reference", href: "/docs/invalid-reference" } + ]] + }) + }, + { + path: './docs/valid-document.mdx', + content: `--- +title: Valid Document +--- + +# Valid Document + +[Link to Invalid Reference](/docs/invalid-reference#non-existent-header)` + }, + { + path: './docs/invalid-reference.mdx', + content: `--- +title: Invalid Reference +--- + +# Invalid Reference + +This document doesn't have the referenced header.` + } + ]); + + // Should complete with warnings + const output = await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + // Should report warning about missing hash + expect(output).toContain('warning Hash "non-existent-header" not found in /docs/invalid-reference'); + }); + + test('should complete build workflow when errors are present in some files', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Valid Document", href: "/docs/valid-document" }, + { title: "Document with Warnings", href: "/docs/document-with-warnings" } + ]] + }) + }, + { + path: './docs/valid-document.mdx', + content: `--- +title: Valid Document +--- + +# Valid Document + +This is a completely valid document.` + }, + { + path: './docs/document-with-warnings.mdx', + content: `--- +title: Document with Warnings +--- + +# Document with Warnings + +[Broken Link](/docs/non-existent-document) + + + This content has an invalid SDK. +` + } + ]); + + // Should complete with warnings + const output = await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + // Check that the build completed and valid files were created + expect(await fileExists(pathJoin('./dist/valid-document.mdx'))).toBe(true); + expect(await fileExists(pathJoin('./dist/document-with-warnings.mdx'))).toBe(true); + + // Check that warnings were reported + expect(output).toContain('warning Guide /docs/non-existent-document not found'); + expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK'); + }); +}); + +describe('Advanced Features', () => { + test('should correctly handle links with anchors to specific sections of documents', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Source Document", href: "/docs/source-document" }, + { title: "Target Document", href: "/docs/target-document" } + ]] + }) + }, + { + path: './docs/source-document.mdx', + content: `--- +title: Source Document +--- + +# Source Document + +[Link to Section 1](/docs/target-document#section-1) +[Link to Section 2](/docs/target-document#section-2) +[Link to Invalid Section](/docs/target-document#invalid-section)` + }, + { + path: './docs/target-document.mdx', + content: `--- +title: Target Document +--- + +# Target Document + +## Section 1 + +Content for section 1. + +## Section 2 + +Content for section 2.` + } + ]); + + const output = await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + // Valid links should work without warnings + expect(output).not.toContain('warning Hash "section-1" not found'); + expect(output).not.toContain('warning Hash "section-2" not found'); + + // Invalid link should produce a warning + expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document'); + }); + + test('should process target="_blank" links in manifest correctly', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[ + { title: "Normal Link", href: "/docs/normal-link" }, + { title: "External Link", href: "https://example.com", target: "_blank" } + ]] + }) + }, + { + path: './docs/normal-link.mdx', + content: `--- +title: Normal Link +--- + +# Normal Link + +This is a normal document.` + } + ]); + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react"] + })); + + // Check that the manifest contains the target="_blank" attribute + const reactManifest = JSON.parse(await readFile(pathJoin('./dist/react/manifest.json'))); + expect(reactManifest) + .toEqual({ + navigation: [[ + { title: "Normal Link", href: "/docs/normal-link" }, + { title: "External Link", href: "https://example.com", target: "_blank" } + ]] + }) + }); + + test('should generate appropriate landing pages for SDK-specific docs', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: "SDK Document", href: "/docs/sdk-document" }]] + }) + }, + { + path: './docs/sdk-document.mdx', + content: `--- +title: SDK Document +sdk: react, nextjs +--- + +# SDK Document + +This document is available for React and Next.js.` + } + ]); + + await build(createBlankStore(), createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ["react", "nextjs"] + })); + + // Check that SDK-specific versions were created + expect(await fileExists(pathJoin('./dist/react/sdk-document.mdx'))).toBe(true); + expect(await fileExists(pathJoin('./dist/nextjs/sdk-document.mdx'))).toBe(true); + + // Check that a landing page was created at the original URL + expect(await fileExists(pathJoin('./dist/sdk-document.mdx'))).toBe(true); + + // Verify landing page content + const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')); + expect(landingPage).toBe(''); + }); +}); \ No newline at end of file diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 543390e2c1..3ea75672d6 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -446,6 +446,10 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( if (inManifest === false) { vfile.message("This guide is not in the manifest.json, but will still be publicly accessible and other guides can link to it") } + + if (href !== encodeURI(href)) { + vfile.fail(`Href "${href}" contains characters that will be encoded by the browser, please remove them`) + } }) .use(() => (tree, vfile) => { mdastVisit(tree, @@ -545,8 +549,18 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( mdastVisit(tree, node => node.type === "heading", node => { - const slug = slugify(toString(node).trim()) - headingsHashs.push(slug) + + // @ts-expect-error - If the heading has a id in it, this will pick it up + // eg # test {{ id: 'my-heading' }} + // This is for remapping the hash to the custom id + const id = node?.children?.[1]?.data?.estree?.body?.[0]?.expression?.properties?.[0]?.value?.value as string | undefined + + if (id !== undefined) { + headingsHashs.push(id) + } else { + const slug = slugify(toString(node).trim()) + headingsHashs.push(slug) + } } ) }) @@ -707,7 +721,7 @@ export const build = async ( // It would definitely be preferable we didn't need to do this markdown processing twice // But because we need a full list / hashmap of all the existing docs, we can't // Unless maybe we do some kind of lazy loading of the docs, but this would add complexity - const coreVFiles = docs.map(async (doc) => { + const coreVFiles = await Promise.all(docs.map(async (doc) => { const vfile = await markdownProcessor() // Validate links between guides are valid .use(() => (tree: Node, vfile: VFile) => { @@ -745,7 +759,7 @@ export const build = async ( if (guide.sdk !== undefined) { // we are going to swap it for the sdk link component to give the users a great experience - return mdastBuilder('mdxJsxFlowElement', { + return mdastBuilder('mdxJsxTextElement', { name: 'SDKLink', attributes: [ mdastBuilder('mdxJsxAttribute', { @@ -834,11 +848,11 @@ export const build = async ( await writeFile(distFilePath, String(vfile)) return vfile - }) + })) - Promise.all(coreVFiles).then((docs) => console.info(`✔️ Wrote out ${docs.length} core docs`)) + console.info(`✔️ Wrote out ${docs.length} core docs`) - const sdkSpecificVFiles = Promise.all(config.validSdks.map(async (targetSdk) => { + const sdkSpecificVFiles = await Promise.all(config.validSdks.map(async (targetSdk) => { // Goes through and removes any items that are not scoped to the target sdk const navigation = await traverseTree({ items: sdkScopedManifest }, @@ -963,14 +977,12 @@ export const build = async ( return { targetSdk, vFiles } })) - sdkSpecificVFiles.then((sdk) => sdk.forEach(({ targetSdk, vFiles }) => console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific guides`))) - - const [awaitedCoreVFiles, awaitedSdkSpecificVFiles] = await Promise.all([Promise.all(coreVFiles), sdkSpecificVFiles]) + sdkSpecificVFiles.forEach(({ targetSdk, vFiles }) => console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific guides`)) - const flatSdkSpecificVFiles = awaitedSdkSpecificVFiles.flatMap(({ vFiles }) => vFiles) + const flatSdkSpecificVFiles = sdkSpecificVFiles.flatMap(({ vFiles }) => vFiles) const output = reporter([ - ...awaitedCoreVFiles.filter((item): item is NonNullable => item !== null), + ...coreVFiles.filter((item): item is NonNullable => item !== null), ...flatSdkSpecificVFiles.filter((item): item is NonNullable => item !== null) ], { quiet: true }) From 002239c582e109ccd343cbeffbd1967919f4009a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 5 Mar 2025 22:49:17 +0800 Subject: [PATCH 041/114] Cut down the build script to just do validation --- scripts/build-docs.test.ts | 920 +------------------------------------ scripts/build-docs.ts | 265 +---------- 2 files changed, 19 insertions(+), 1166 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index f21cf8e05b..6c45399e5c 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -3,7 +3,6 @@ import path from 'node:path' import os from 'node:os' import { glob } from 'glob'; - import { describe, expect, onTestFinished, test } from 'vitest' import { build, createBlankStore, createConfig } from './build-docs' @@ -120,7 +119,6 @@ const baseConfig = { docsPath: './docs', manifestPath: './docs/manifest.json', partialsPath: './_partials', - distPath: './dist', ignorePaths: ["/docs/_partials"], manifestOptions: { wrapDefault: true, @@ -150,262 +148,15 @@ Testing with a simple page.` } ]) - await build(createBlankStore(), createConfig({ + const output = await build(createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, validSdks: ["nextjs", "react"], })) - expect(await fileExists(pathJoin('./dist/simple-test.mdx'))).toBe(true) - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(`--- -title: Simple Test ---- - -# Simple Test Page - -Testing with a simple page.`) - - expect(await fileExists(pathJoin('./dist/nextjs/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/nextjs/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - })) - - expect(await fileExists(pathJoin('./dist/react/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - })) - + expect(output).toBe("") }) -test('Two Docs, each grouped by a different SDK', async () => { - // Create temp environment with minimal files array - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: "React", - sdk: ["react"], - items: [ - [ - { title: "Quickstart", href: "/docs/quickstart/react" } - ] - ] - }, - { - title: "Vue", - sdk: ["vue"], - items: [ - [ - { title: "Quickstart", href: "/docs/quickstart/vue" } - ] - ] - } - ], - ] - }) - }, - { - path: './docs/quickstart/react.mdx', - content: `--- -title: Quickstart ---- - -# React Quickstart` - }, - { - path: './docs/quickstart/vue.mdx', - content: `--- -title: Quickstart ---- - -# Vue Quickstart` - } - ]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "vue"] - })) - - expect(await fileExists(pathJoin('./dist/react/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ - navigation: [ - [ - { - title: "React", - items: [ - [ - { title: "Quickstart", href: "/docs/quickstart/react" } - ] - ] - }, - ], - ] - })) - - expect(await fileExists(pathJoin('./dist/vue/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe(JSON.stringify({ - navigation: [ - [ - { - title: "Vue", - items: [ - [ - { title: "Quickstart", href: "/docs/quickstart/vue" } - ] - ] - }, - ], - ] - })) - - const distFiles = await treeDir(pathJoin('./dist')) - - expect(distFiles.length).toBe(4) - expect(distFiles).toContain('vue/manifest.json') - expect(distFiles).toContain('react/manifest.json') - expect(distFiles).toContain('quickstart/vue.mdx') - expect(distFiles).toContain('quickstart/react.mdx') - -}) - -test('sdk in frontmatter filters the docs', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react ---- - -# Simple Test Page - -Testing with a simple page.` - }]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) - - expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/react/simple-test" }]] - })) - - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- -title: Simple Test -sdk: react ---- - -# Simple Test Page - -Testing with a simple page.`) - - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(``) - - const distFiles = await treeDir(pathJoin('./dist')) - - expect(distFiles.length).toBe(3) - expect(distFiles).toContain('simple-test.mdx') - expect(distFiles).toContain('react/simple-test.mdx') - expect(distFiles).toContain('react/manifest.json') -}) - -test('3 sdks in frontmatter generates 3 variants', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, vue, astro ---- - -# Simple Test Page - -Testing with a simple page.` - } - ]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "vue", "astro"] - })) - - expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/react/simple-test" }]] - })) - expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/vue/simple-test" }]] - })) - expect(await readFile(pathJoin('./dist/astro/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/astro/simple-test" }]] - })) - - const distFiles = await treeDir(pathJoin('./dist')) - - expect(distFiles.length).toBe(7) - expect(distFiles).toContain('simple-test.mdx') - expect(distFiles).toContain('react/simple-test.mdx') - expect(distFiles).toContain('react/manifest.json') - expect(distFiles).toContain('vue/simple-test.mdx') - expect(distFiles).toContain('vue/manifest.json') - expect(distFiles).toContain('astro/simple-test.mdx') - expect(distFiles).toContain('astro/manifest.json') -}) - -test(' content filtered out when sdk is in frontmatter', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, expo ---- - -# Simple Test Page - - - React Content - - -Testing with a simple page.` - } - ]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "expo"] - })) - - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('React Content') - - expect(await readFile(pathJoin('./dist/expo/simple-test.mdx'))).not.toContain('React Content') -}) test('Invalid SDK in frontmatter fails the build', async () => { const { tempDir, pathJoin } = await createTempFiles([ @@ -472,38 +223,6 @@ Testing with a simple page.` }) describe('Includes and Partials', () => { - test(' Component embeds content in to guide', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) - }, - { - path: './docs/_partials/test-partial.mdx', - content: `Test Partial Content` - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test ---- - - - -# Simple Test Page` - } - ]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) - - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toContain('Test Partial Content') - }) test('Invalid partial src fails the build', async () => { const { tempDir } = await createTempFiles([ @@ -736,515 +455,9 @@ title: Simple Test expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) }) - - test('Swap out links for when a link points to an sdk generated guide', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: "SDK Filtered Page", href: "/docs/sdk-filtered-page" }, { title: "Core Page", href: "/docs/core-page" }]] - }) - }, - { - path: './docs/sdk-filtered-page.mdx', - content: `--- -title: SDK Filtered Page -sdk: react, nextjs ---- - -SDK filtered page` - }, - { - path: './docs/core-page.mdx', - content: `--- -title: Core Page ---- - -# Core page - -[SDK Filtered Page](/docs/sdk-filtered-page) -` - } - ]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs"] - })) - - expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain(`SDK Filtered Page`) - }) }) -describe('SDK Filtering', () => { - - test('should handle SDK filtering with deeply nested manifest structures', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [{ - title: "Top Level", - items: [ - [{ - title: "Mid Level", - sdk: ["react", "nextjs"], - items: [ - [{ - title: "Deep Level", - sdk: ["nextjs"], - items: [ - [{ title: "Deeply Nested Page", href: "/docs/deeply-nested-nextjs" }] - ] - },{ - title: "Deep Level", - sdk: ["react"], - items: [ - [{ title: "Deeply Nested Page", href: "/docs/deeply-nested-react" }] - ] - }] - ] - }] - ] - }] - ] - }) - }, - { - path: './docs/deeply-nested-nextjs.mdx', - content: `--- -title: Deeply Nested Page -sdk: nextjs ---- - -Content for Next.js users.` - }, - { - path: './docs/deeply-nested-react.mdx', - content: `--- -title: Deeply Nested Page -sdk: react ---- - -Content for React users.` - } - ]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs", "javascript-frontend"] - })) - - // Page should be available in nextjs (from manifest deep nesting) - expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-react.mdx'))).toBe(false) - expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toContain("Content for Next.js users.") - expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).not.toContain("Content for React users.") - - // Page should be available in react (from parent manifest item) - expect(await fileExists(pathJoin('./dist/react/deeply-nested-react.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/react/deeply-nested-nextjs.mdx'))).toBe(false) - expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).toContain("Content for React users.") - expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).not.toContain("Content for Next.js users.") - - // Page should NOT be available in javascript-frontend (filtered out by manifest) - expect(await fileExists(pathJoin('./dist/javascript-frontend/deeply-nested-nextjs.mdx'))).toBe(false) - expect(await fileExists(pathJoin('./dist/javascript-frontend/deeply-nested-react.mdx'))).toBe(false) - }); - - test('should correctly process multiple blocks with different SDKs in a single document', async () => { - const { tempDir, pathJoin }= await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[ - { - title: "Multiple SDK Blocks", - href: "/multiple-sdk-blocks" - } - ]] - }) - }, - { - path: './docs/multiple-sdk-blocks.mdx', - content: `--- -title: Multiple SDK Blocks -sdk: react, nextjs, javascript-frontend ---- - -# Multiple SDK Blocks - - - This content is for React users only. - - - - This content is for Next.js users only. - - - - This content is for JavaScript Frontend users only. - - -Common content for all SDKs.` - } - ]); - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs", "javascript-frontend"] - })); - - // Check React version - expect(await fileExists(pathJoin('./dist/react/multiple-sdk-blocks.mdx'))).toBe(true); - const reactContent = await readFile(pathJoin('./dist/react/multiple-sdk-blocks.mdx')); - expect(reactContent).toContain("This content is for React users only."); - expect(reactContent).not.toContain("This content is for Next.js users only."); - expect(reactContent).not.toContain("This content is for JavaScript Frontend users only."); - expect(reactContent).toContain("Common content for all SDKs."); - - // Check Next.js version - expect(await fileExists(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx'))).toBe(true); - const nextjsContent = await readFile(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx')); - expect(nextjsContent).not.toContain("This content is for React users only."); - expect(nextjsContent).toContain("This content is for Next.js users only."); - expect(nextjsContent).not.toContain("This content is for JavaScript Frontend users only."); - expect(nextjsContent).toContain("Common content for all SDKs."); - - // Check JavaScript Frontend version - expect(await fileExists(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx'))).toBe(true); - const jsContent = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx')); - expect(jsContent).not.toContain("This content is for React users only."); - expect(jsContent).not.toContain("This content is for Next.js users only."); - expect(jsContent).toContain("This content is for JavaScript Frontend users only."); - expect(jsContent).toContain("Common content for all SDKs."); - }); - - test('should handle nested components correctly', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [{ - title: "Parent Group", - sdk: ["react", "nextjs"], - items: [ - [{ title: "Nested SDK Page", href: "/docs/nested-sdk-page" }] - ] - }] - ] - }) - }, - { - path: './docs/nested-sdk-page.mdx', - content: `--- -title: Nested SDK Page -sdk: react, nextjs ---- - -# Nested SDK Filtering - - - This content is for React users. - - - This is nested content specifically for Next.js users who are also using React. - - - -Common content for all SDKs.` - } - ]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs"] - })) - - // Check React output has only React content - const reactOutput = await readFile(pathJoin('./dist/react/nested-sdk-page.mdx')) - expect(reactOutput).toContain("This content is for React users.") - expect(reactOutput).not.toContain("This is nested content specifically for Next.js users") - - // Check Next.js output has both React and Next.js content - const nextjsOutput = await readFile(pathJoin('./dist/nextjs/nested-sdk-page.mdx')) - expect(nextjsOutput).toContain("This content is for React users.") - expect(nextjsOutput).toContain("This is nested content specifically for Next.js users") - - }); - - test('should support components with array syntax for multiple SDKs', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[ - { - title: "Multiple SDK Test", - href: "/docs/multiple-sdk-test" - } - ]] - }) - }, - { - path: './docs/multiple-sdk-test.mdx', - content: `--- -title: Multiple SDK Test -sdk: react, nextjs, javascript-frontend ---- - -# Multiple SDK Test - - - This content is for React and Next.js users. - - - - This content is for JavaScript Frontend users. - - -Common content for all SDKs.` - } - ]) - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs", "javascript-frontend"] - })) - - // Check React output has React content but not JavaScript Frontend content - const reactOutput = await readFile(pathJoin('./dist/react/multiple-sdk-test.mdx')) - expect(reactOutput).toContain("This content is for React and Next.js users.") - expect(reactOutput).not.toContain("This content is for JavaScript Frontend users.") - - // Check Next.js output has Next.js content but not JavaScript Frontend content - const nextjsOutput = await readFile(pathJoin('./dist/nextjs/multiple-sdk-test.mdx')) - expect(nextjsOutput).toContain("This content is for React and Next.js users.") - expect(nextjsOutput).not.toContain("This content is for JavaScript Frontend users.") - - // Check JavaScript Frontend output has JavaScript Frontend content but not React/Next.js content - const jsOutput = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-test.mdx')) - expect(jsOutput).toContain("This content is for JavaScript Frontend users.") - expect(jsOutput).not.toContain("This content is for React and Next.js users.") - }); -}); - -describe('Manifest Handling', () => { - test('should apply manifest options (wrapDefault, collapseDefault, hideTitleDefault) correctly', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[ - { title: "Group One", items: [[{ title: "Item One", href: "/docs/item-one" }]], wrap: true, collapse: true, hideTitle: false }, - { title: "Group Two", items: [[{ title: "Item Two", href: "/docs/item-two" }]], wrap: true, collapse: false, hideTitle: true }, - { title: "Group Three", items: [[{ title: "Item Three", href: "/docs/item-three" }]], wrap: false, collapse: true, hideTitle: false }, - { title: "Group Four", items: [[{ title: "Item Four", href: "/docs/item-four" }]], wrap: false, collapse: false, hideTitle: true }, - ]] - }) - }, - { path: "./docs/item-one.mdx", content: `---\ntitle: Item One\n---\nItem One` }, - { path: "./docs/item-two.mdx", content: `---\ntitle: Item Two\n---\nItem Two` }, - { path: "./docs/item-three.mdx", content: `---\ntitle: Item Three\n---\nItem Three` }, - { path: "./docs/item-four.mdx", content: `---\ntitle: Item Four\n---\nItem Four` }, - ]) - - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["nextjs"], - manifestOptions: { - wrapDefault: false, - collapseDefault: false, - hideTitleDefault: false - } - })) - - const manifest = JSON.parse(await readFile(pathJoin('./dist/nextjs/manifest.json'))) - const groups = manifest.navigation[0] - - expect(groups[0].wrap).toBe(true) - expect(groups[0].collapse).toBe(true) - expect(groups[0].hideTitle).toBe(undefined) - - expect(groups[1].wrap).toBe(true) - expect(groups[1].collapse).toBe(undefined) - expect(groups[1].hideTitle).toBe(true) - - expect(groups[2].wrap).toBe(undefined) - expect(groups[2].collapse).toBe(true) - expect(groups[2].hideTitle).toBe(undefined) - - expect(groups[3].wrap).toBe(undefined) - expect(groups[3].collapse).toBe(undefined) - expect(groups[3].hideTitle).toBe(true) - - }); - - test('should properly inherit SDK filtering from parent groups to child items', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[ - { - title: "SDK Group", - sdk: ["nextjs", "react"], - items: [[ - { - title: "Sub Group", - items: [[ - { title: "SDK Item", href: "/docs/sdk-item" }, - { title: "Nested Group", items: [[{ title: "Nested Item", href: "/docs/nested-item" }]] } - ]] - } - ]] - }, - { - title: "Generic Group", - items: [[ - { - title: "Sub Group", - items: [[ - { title: "Generic Item", href: "/docs/generic-item" } - ]] - } - ]] - }, - { - title: "Vue Group", - sdk: ["vue"], - items: [[ - { - title: "Sub Group", - items: [[{ title: "Vue Item", href: "/docs/vue-item" }]] - } - ]] - } - ]] - }) - }, - { - path: "./docs/sdk-item.mdx", - content: `---\ntitle: SDK Item\n---\nSDK specific content` - }, - { - path: "./docs/nested-item.mdx", - content: `---\ntitle: Nested Item\n---\nNested SDK specific content` - }, - { - path: "./docs/generic-item.mdx", - content: `---\ntitle: Generic Item\n---\nGeneric content` - }, - { - path: "./docs/vue-item.mdx", - content: `---\ntitle: Vue Item\n---\nVue specific content` - } - ]); - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["nextjs", "react", "vue"], - })); - - // Check nextjs manifest - const nextjsManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/nextjs/manifest.json'), 'utf-8')); - const nextjsGroups = nextjsManifest.navigation[0]; - - expect(nextjsGroups[0].items[0][0].items[0][0].title).toBe("SDK Item"); - expect(nextjsGroups[0].items[0][0].items[0][1].title).toBe("Nested Group"); - expect(nextjsGroups[1].items[0][0].items[0][0].title).toBe("Generic Item"); - expect(nextjsGroups[2]).toBe(undefined); - - - // Check react manifest - const reactManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/react/manifest.json'), 'utf-8')); - const reactGroups = reactManifest.navigation[0]; - - expect(reactGroups[0].items[0][0].items[0][0].title).toBe("SDK Item"); - expect(reactGroups[0].items[0][0].items[0][1].title).toBe("Nested Group"); - expect(reactGroups[1].items[0][0].items[0][0].title).toBe("Generic Item"); - expect(reactGroups[2]).toBe(undefined); - - - // Check vue manifest - const vueManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/vue/manifest.json'), 'utf-8')); - const vueGroups = vueManifest.navigation[0]; - - expect(vueGroups[0].items[0][0].items[0][0].title).toBe("Generic Item"); - expect(vueGroups[1].items[0][0].items[0][0].title).toBe("Vue Item"); - expect(vueGroups[2]).toBe(undefined); - }); - -}); - describe('Path and File Handling', () => { - test('should ignore paths specified in ignorePaths during processing', async () => { - const { tempDir, pathJoin, listFiles } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[ - { title: "Regular Guide", href: "/docs/regular-guide" }, - { title: "Ignored Guide", href: "/docs/ignored/ignored-guide" } - ]] - }) - }, - { - path: './docs/regular-guide.mdx', - content: `--- -title: Regular Guide ---- - -# Regular Guide Content` - }, - { - path: './docs/ignored/ignored-guide.mdx', - content: `--- -title: Ignored Guide ---- - -# Ignored Guide Content` - } - ]); - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"], - ignorePaths: ["/docs/ignored"] - })); - - // Check that only the regular guide was processed - const distFiles = (await listFiles()).filter(file => file.startsWith('dist/')) - - expect(distFiles).toContain('dist/regular-guide.mdx'); - expect(distFiles).toContain('dist/react/manifest.json'); - expect(distFiles).not.toContain('dist/ignored/ignored-guide.mdx'); - - // Verify that the manifest was filtered correctly - expect(JSON.parse(await readFile(pathJoin('./dist/react/manifest.json')))).toEqual({ - navigation: [[ - { - title: "Regular Guide", - href: "/docs/regular-guide" - }, - { - title: "Ignored Guide", - href: "/docs/ignored/ignored-guide" - } - ]] - }) - }); test('should detect file path conflicts when a core doc path matches an SDK path', async () => { const { tempDir } = await createTempFiles([ @@ -1273,53 +486,6 @@ title: React Guide await expect(promise).rejects.toThrow('Attempting to write out a core doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict'); }); - - test('should remove .mdx suffix from markdown links', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[ - { title: "Source Page", href: "/docs/source-page" }, - { title: "Target Page", href: "/docs/target-page" } - ]] - }) - }, - { - path: './docs/source-page.mdx', - content: `--- -title: Source Page ---- - -# Source Page - -[Link to Target with .mdx](/docs/target-page.mdx) -[Link to Target without .mdx](/docs/target-page)` - }, - { - path: './docs/target-page.mdx', - content: `--- -title: Target Page ---- - -# Target Page Content` - } - ]); - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - // Both links should be processed to remove .mdx - const sourcePageContent = await readFile(pathJoin('./dist/source-page.mdx')); - - // The link should have .mdx removed - expect(sourcePageContent).toContain('[Link to Target with .mdx](/docs/target-page)'); - expect(sourcePageContent).toContain('[Link to Target without .mdx](/docs/target-page)'); - expect(sourcePageContent).not.toContain('/docs/target-page.mdx'); - }); }); describe('Edge Cases', () => { @@ -1527,10 +693,6 @@ title: Document with Warnings validSdks: ["react"] })); - // Check that the build completed and valid files were created - expect(await fileExists(pathJoin('./dist/valid-document.mdx'))).toBe(true); - expect(await fileExists(pathJoin('./dist/document-with-warnings.mdx'))).toBe(true); - // Check that warnings were reported expect(output).toContain('warning Guide /docs/non-existent-document not found'); expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK'); @@ -1593,82 +755,4 @@ Content for section 2.` expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document'); }); - test('should process target="_blank" links in manifest correctly', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[ - { title: "Normal Link", href: "/docs/normal-link" }, - { title: "External Link", href: "https://example.com", target: "_blank" } - ]] - }) - }, - { - path: './docs/normal-link.mdx', - content: `--- -title: Normal Link ---- - -# Normal Link - -This is a normal document.` - } - ]); - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - // Check that the manifest contains the target="_blank" attribute - const reactManifest = JSON.parse(await readFile(pathJoin('./dist/react/manifest.json'))); - expect(reactManifest) - .toEqual({ - navigation: [[ - { title: "Normal Link", href: "/docs/normal-link" }, - { title: "External Link", href: "https://example.com", target: "_blank" } - ]] - }) - }); - - test('should generate appropriate landing pages for SDK-specific docs', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: "SDK Document", href: "/docs/sdk-document" }]] - }) - }, - { - path: './docs/sdk-document.mdx', - content: `--- -title: SDK Document -sdk: react, nextjs ---- - -# SDK Document - -This document is available for React and Next.js.` - } - ]); - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs"] - })); - - // Check that SDK-specific versions were created - expect(await fileExists(pathJoin('./dist/react/sdk-document.mdx'))).toBe(true); - expect(await fileExists(pathJoin('./dist/nextjs/sdk-document.mdx'))).toBe(true); - - // Check that a landing page was created at the original URL - expect(await fileExists(pathJoin('./dist/sdk-document.mdx'))).toBe(true); - - // Verify landing page content - const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')); - expect(landingPage).toBe(''); - }); }); \ No newline at end of file diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 3ea75672d6..79c25e4a16 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -9,23 +9,11 @@ // - [x] Checks that the sdk is available in the manifest // - [x] Checks that the sdk is available in the frontmatter -// - [x] Embeds the includes in the markdown files -// - [x] Updates the links in the content if they point to the sdk specific docs -// - [x] Copies over "core" docs to the dist folder -// - [x] Generates "landing" pages for the sdk specific docs at the original url -// - [x] Generates a manifest that is specific to each SDK -// - [x] Duplicates out the sdk specific docs to their respective folders -// - [x] stripping filtered out content -// - [x] Removes .mdx from the end of docs markdown links - import fs from 'node:fs/promises' import path from 'node:path' import remarkMdx from 'remark-mdx' import { remark } from 'remark' import { visit as mdastVisit } from 'unist-util-visit' -import { filter as mdastFilter } from 'unist-util-filter' -import { map as mdastMap } from 'unist-util-map' -import { u as mdastBuilder } from 'unist-builder' import remarkFrontmatter from 'remark-frontmatter' import yaml from "yaml" import { slugifyWithCounter } from '@sindresorhus/slugify' @@ -210,26 +198,6 @@ const markdownProcessor = remark() type VFile = Awaited> -const ensureDirectory = (config: BuildConfig) => async (dirPath: string): Promise => { - try { - await fs.access(dirPath) - } catch { - await fs.mkdir(dirPath, { recursive: true }) - } -} - -const writeDistFile = (config: BuildConfig) => async (filePath: string, contents: string) => { - const ensureDir = ensureDirectory(config); - const fullPath = path.join(config.distPath, filePath) - await ensureDir(path.dirname(fullPath)) - await fs.writeFile(fullPath, contents, { "encoding": "utf-8" }) -} - -const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, contents: string) => { - const writeFile = writeDistFile(config); - await writeFile(path.join(sdk, filePath), contents) -} - const removeMdxSuffix = (filePath: string) => { if (filePath.endsWith('.mdx')) { return filePath.slice(0, -4) @@ -487,30 +455,27 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( } }) - // Validate and embed the + // Validate the .use(() => (tree, vfile) => { - return mdastMap(tree, + return mdastVisit(tree, node => { const partialSrc = extractComponentPropValueFromNode(node, vfile, "Include", "src") - if (partialSrc === undefined) { - return node - } + if (partialSrc === undefined) return; if (partialSrc.startsWith('_partials/') === false) { vfile.message(` prop "src" must start with "_partials/"`, node.position) - return node + return; } const partial = partials.find((partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`) if (partial === undefined) { vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) - return node + return; } - let partialNode: Node | null = null const partialContentVFile = markdownProcessor() .use(() => (tree, vfile) => { @@ -520,8 +485,6 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) } ) - - partialNode = tree }) .processSync({ path: partial.path, @@ -534,13 +497,6 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( console.error(partialContentReport) } - if (partialNode === null) { - vfile.fail(`Failed to parse the content of ${partial.path}`, node.position) - return node - } - - return Object.assign(node, partialNode) - } ) }) @@ -591,16 +547,11 @@ export const build = async ( config: BuildConfig ) => { // Apply currying to create functions pre-configured with config - const ensureDir = ensureDirectory(config); const getManifest = readManifest(config); const getDocsFolder = readDocsFolder(config); const getPartialsFolder = readPartialsFolder(config); const getPartialsMarkdown = readPartialsMarkdown(config); const parseMarkdownFile = parseInMarkdownFile(config); - const writeFile = writeDistFile(config); - const writeSdkFile = writeSDKFile(config); - - await ensureDir(config.distPath) const userManifest = await getManifest() console.info('✔️ Read Manifest') @@ -725,27 +676,27 @@ export const build = async ( const vfile = await markdownProcessor() // Validate links between guides are valid .use(() => (tree: Node, vfile: VFile) => { - return mdastMap(tree, + return mdastVisit(tree, node => { - if (node.type !== "link") return node - if (!("url" in node)) return node - if (typeof node.url !== "string") return node - if (!node.url.startsWith("/docs/")) return node - if (!("children" in node)) return node + if (node.type !== "link") return; + if (!("url" in node)) return; + if (typeof node.url !== "string") return; + if (!node.url.startsWith("/docs/")) return; + if (!("children" in node)) return; node.url = removeMdxSuffix(node.url) const [url, hash] = (node.url as string).split("#") const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return node; + if (ignore === true) return; const guide = guides.get(url) if (guide === undefined) { vfile.message(`Guide ${url} not found`, node.position) - return node; + return; } if (hash !== undefined) { @@ -755,28 +706,6 @@ export const build = async ( vfile.message(`Hash "${hash}" not found in ${url}`, node.position) } } - - if (guide.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:') - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(guide.sdk) - }) - }) - ] - }) - } - - return node; } ) }) @@ -833,19 +762,6 @@ export const build = async ( throw new Error(`Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`) } - if (doc.sdk !== undefined) { - // This is a sdk specific guide, so we want to put a landing page here to redirect the user to a guide customised to their sdk. - - await writeFile( - distFilePath, - // It's possible we will want to / need to put some frontmatter here - `` - ) - - return vfile - } - - await writeFile(distFilePath, String(vfile)) return vfile })) @@ -853,113 +769,24 @@ export const build = async ( console.info(`✔️ Wrote out ${docs.length} core docs`) const sdkSpecificVFiles = await Promise.all(config.validSdks.map(async (targetSdk) => { - - // Goes through and removes any items that are not scoped to the target sdk - const navigation = await traverseTree({ items: sdkScopedManifest }, - async ({ sdk, ...item }) => { - - // This means its generic, not scoped to a specific sdk, so we keep it - if (sdk === undefined) return { - title: item.title, - href: item.href, - tag: item.tag, - wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, - icon: item.icon, - target: item.target - } as const - - // This item is not scoped to the target sdk, so we remove it - if (sdk.includes(targetSdk) === false) return null - - // This is a scoped item and its scoped to our target sdk - return { - title: item.title, - href: scopeHrefToSDK(item.href, targetSdk), - tag: item.tag, - wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, - icon: item.icon, - target: item.target - } as const - }, - // @ts-expect-error - This traverseTree function might just be the death of me - async ({ sdk, ...group }) => { - if (sdk === undefined) return { - title: group.title, - collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, - tag: group.tag, - wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, - icon: group.icon, - hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, - items: group.items, - } - - if (sdk.includes(targetSdk) === false) return null - - return { - title: group.title, - collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, - tag: group.tag, - wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, - icon: group.icon, - hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, - items: group.items, - } - } - ) - const vFiles = await Promise.all(docs.map(async (doc) => { if (doc.sdk === undefined) return null; // skip core docs if (doc.sdk.includes(targetSdk) === false) return null; // skip docs that are not for the target sdk const vfile = await markdownProcessor() - // filter out content that is only available to other sdk's - .use(() => (tree, vfile) => { - return mdastFilter(tree, - node => { - - // We aren't passing the vfile here as the as the warning - // should have already been reported above when we initially - // parsed the file - - const sdk = extractComponentPropValueFromNode(node, undefined, "If", "sdk") - - if (sdk === undefined) return true - - const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk) - - if (sdksFilter === undefined) return true - - if (sdksFilter.includes(targetSdk)) { - return true - } - - return false - - } - ) - }) // scope urls so they point to the current sdk .use(() => (tree, vfile) => { - return mdastMap(tree, + return mdastVisit(tree, node => { - if (node.type !== "link") return node + if (node.type !== "link") return; if (!("url" in node)) { vfile.fail(`Link node does not have a url property ${pleaseReport}`, node.position) - return node + return; } if (typeof node.url !== "string") { vfile.fail(`Link node url must be a string ${pleaseReport}`, node.position) - return node + return; } - if (!node.url.startsWith("/docs/")) { - return node - } - - const guide = guides.get(node.url) - - if (guide === undefined) { } - - return node } ) }) @@ -967,13 +794,9 @@ export const build = async ( ...doc.vfile, messages: [] // reset the messages, otherwise they will be duplicated }) - await writeSdkFile(targetSdk, `${doc.href.replace("/docs/", "")}.mdx`, String(vfile)) - return vfile })) - await writeSdkFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) - return { targetSdk, vFiles } })) @@ -994,52 +817,12 @@ export const build = async ( return output } -const watchAndRebuild = ( - store: ReturnType, - config: BuildConfig -) => { - const watcher = chok.watch( - [ - config.docsPath, - ], - { - alwaysStat: true, - ignored: (filePath, stats) => { - if (stats === undefined) return false - if (stats.isDirectory()) return false - - const relativePath = path.relative(config.docsPath, filePath) - - const isManifest = relativePath === 'manifest.json' - const isMarkdown = relativePath.endsWith('.mdx') - - return !(isManifest || isMarkdown) - }, - ignoreInitial: true, - } - ) - - watcher.on("all", async (event, filePath) => { - - console.info(`File ${filePath} changed`, { event }) - - const href = removeMdxSuffix(`/${path.relative(config.basePath, filePath)}`) - - store.markdownFiles.delete(href) - - await build(store, config) - - }) - -} - type BuildConfigOptions = { basePath: string; validSdks: readonly SDK[]; docsPath: string; manifestPath: string; partialsPath: string; - distPath: string; ignorePaths: string[]; manifestOptions: { wrapDefault: boolean; @@ -1065,9 +848,6 @@ export function createConfig(config: BuildConfigOptions) { manifestRelativePath: config.manifestPath, manifestFilePath: resolve(config.manifestPath), - distRelativePath: config.distPath, - distPath: resolve(config.distPath), - partialsRelativePath: config.partialsPath, partialsPath: resolve(config.partialsPath), @@ -1087,7 +867,6 @@ const main = async () => { docsPath: './docs', manifestPath: './docs/manifest.json', partialsPath: './_partials', - distPath: './dist', ignorePaths: [ "/docs/core-1", '/pricing', @@ -1114,16 +893,6 @@ const main = async () => { await build(store, config); - const args = process.argv.slice(2) - const watchFlag = args.includes('--watch') - - if (watchFlag) { - - console.info(`Watching for changes...`) - - watchAndRebuild(store, config); - } - } // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts From 66c68e56d27d8e0d45fe63979dcc9863cae29f5f Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 5 Mar 2025 23:03:23 +0800 Subject: [PATCH 042/114] fix up types --- scripts/build-docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 79c25e4a16..61d5bb2b09 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -370,7 +370,7 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile const isValidItems = isValidSdks(config) if (sdkProp.includes('", "') || sdkProp.includes("', '") || sdkProp.includes('["') || sdkProp.includes('"]')) { - const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) + const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) as string[] if (isValidItems(sdks)) { return sdks } else { From 3deaae7445255a6ba2ee378ebaa92179f6fc2f1c Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 5 Mar 2025 23:07:05 +0800 Subject: [PATCH 043/114] run prettier --- scripts/build-docs.test.ts | 541 ++++++++++++---------- scripts/build-docs.ts | 918 ++++++++++++++++++++----------------- 2 files changed, 788 insertions(+), 671 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 6c45399e5c..03c517445e 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import os from 'node:os' -import { glob } from 'glob'; +import { glob } from 'glob' import { describe, expect, onTestFinished, test } from 'vitest' import { build, createBlankStore, createConfig } from './build-docs' @@ -15,16 +15,16 @@ const tempConfig = { // Whether to preserve temp directories after tests // (helpful for debugging, but requires manual cleanup) - preserveTemp: false + preserveTemp: false, } async function createTempFiles( files: { path: string; content: string }[], options?: { - prefix?: string; // Prefix for the temp directory name - preserveTemp?: boolean; // Override global preserveTemp setting - useLocalTemp?: boolean; // Override global useLocalTemp setting - } + prefix?: string // Prefix for the temp directory name + preserveTemp?: boolean // Override global preserveTemp setting + useLocalTemp?: boolean // Override global useLocalTemp setting + }, ) { const prefix = options?.prefix || 'clerk-docs-test-' const preserve = options?.preserveTemp ?? tempConfig.preserveTemp @@ -80,14 +80,14 @@ async function createTempFiles( listFiles: async () => { return glob('**/*', { cwd: tempDir, - nodir: true + nodir: true, }) }, // Read file contents readFile: async (filePath: string): Promise => { return fs.readFile(path.join(tempDir, filePath), 'utf-8') - } + }, } } @@ -105,26 +105,26 @@ async function readFile(filePath: string): Promise { } function normalizeString(str: string): string { - return str.replace(/\r\n/g, '\n').trim(); + return str.replace(/\r\n/g, '\n').trim() } function treeDir(baseDir: string) { return glob('**/*', { cwd: baseDir, - nodir: true // Only return files, not directories - }); + nodir: true, // Only return files, not directories + }) } const baseConfig = { docsPath: './docs', manifestPath: './docs/manifest.json', partialsPath: './_partials', - ignorePaths: ["/docs/_partials"], + ignorePaths: ['/docs/_partials'], manifestOptions: { wrapDefault: true, collapseDefault: false, - hideTitleDefault: false - } + hideTitleDefault: false, + }, } test('Basic build test with simple files', async () => { @@ -133,8 +133,8 @@ test('Basic build test with simple files', async () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -144,27 +144,29 @@ title: Simple Test # Simple Test Page -Testing with a simple page.` - } +Testing with a simple page.`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["nextjs", "react"], - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react'], + }), + ) - expect(output).toBe("") + expect(output).toBe('') }) - test('Invalid SDK in frontmatter fails the build', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -175,15 +177,18 @@ sdk: react, expo, coffeescript # Simple Test Page -Testing with a simple page.` - } +Testing with a simple page.`, + }, ]) - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "expo"] - })) + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'expo'], + }), + ) await expect(promise).rejects.toThrow(`Invalid SDK ["coffeescript"], the valid SDKs are ["react","expo"]`) }) @@ -193,8 +198,8 @@ test('Invalid SDK in fails the build', async () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -209,28 +214,30 @@ sdk: react, expo astro Content -Testing with a simple page.` - } +Testing with a simple page.`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "expo"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'expo'], + }), + ) expect(output).toContain(`warning sdk \"astro\" in is not a valid SDK`) }) describe('Includes and Partials', () => { - test('Invalid partial src fails the build', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -240,15 +247,18 @@ title: Simple Test -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).toContain(`warning Partial /docs/_partials/test-partial.mdx not found`) }) @@ -258,16 +268,16 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/_partials/test-partial-1.mdx', - content: `` + content: ``, }, { path: './docs/_partials/test-partial-2.mdx', - content: `Test Partial Content` + content: `Test Partial Content`, }, { path: './docs/simple-test.mdx', @@ -277,15 +287,18 @@ title: Simple Test -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) await expect(promise).rejects.toThrow(`Partials inside of partials is not yet supported`) }) @@ -295,8 +308,8 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -306,15 +319,18 @@ title: Simple Test -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).toContain(`warning prop "src" must start with "_partials/"`) }) @@ -326,8 +342,8 @@ describe('Link Validation and Processing', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -337,15 +353,18 @@ title: Simple Test [Non Existent Page](/docs/non-existent-page) -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).toContain(`warning Guide /docs/non-existent-page not found`) }) @@ -355,8 +374,8 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -366,7 +385,7 @@ title: Simple Test [Core Page](/docs/core-page) -# Simple Test Page` +# Simple Test Page`, }, { path: './docs/core-page.mdx', @@ -374,15 +393,18 @@ title: Simple Test title: Core Page --- -# Core Page` - } +# Core Page`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).not.toContain(`warning Guide /docs/core-page not found`) }) @@ -392,8 +414,8 @@ title: Core Page { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -403,16 +425,18 @@ title: Simple Test [Simple Test](/docs/simple-test#non-existent-hash) -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).toContain(`warning Hash "non-existent-hash" not found in /docs/simple-test`) }) @@ -422,11 +446,13 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Simple Test", href: "/docs/simple-test" }, - { title: "Headings", href: "/docs/headings" } - ]] - }) + navigation: [ + [ + { title: 'Simple Test', href: '/docs/simple-test' }, + { title: 'Headings', href: '/docs/headings' }, + ], + ], + }), }, { path: './docs/headings.mdx', @@ -434,7 +460,7 @@ title: Simple Test title: Headings --- -# test {{ id: 'my-heading' }}` +# test {{ id: 'my-heading' }}`, }, { path: './docs/simple-test.mdx', @@ -442,30 +468,31 @@ title: Headings title: Simple Test --- -[Headings](/docs/headings#my-heading)` - } +[Headings](/docs/headings#my-heading)`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) }) - }) describe('Path and File Handling', () => { - test('should detect file path conflicts when a core doc path matches an SDK path', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "React Guide", href: "/docs/react/conflict" }]] - }) + navigation: [[{ title: 'React Guide', href: '/docs/react/conflict' }]], + }), }, { path: './docs/react/conflict.mdx', @@ -473,30 +500,34 @@ describe('Path and File Handling', () => { title: React Guide --- -# This will cause a conflict because it's in a path that starts with "react"` - } - ]); +# This will cause a conflict because it's in a path that starts with "react"`, + }, + ]) // This should throw an error because the file path starts with an SDK name - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - await expect(promise).rejects.toThrow('Attempting to write out a core doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict'); - }); -}); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Attempting to write out a core doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict', + ) + }) +}) describe('Edge Cases', () => { - test('should report errors for malformed frontmatter', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Malformed Frontmatter", href: "/docs/malformed-frontmatter" }]] - }) + navigation: [[{ title: 'Malformed Frontmatter', href: '/docs/malformed-frontmatter' }]], + }), }, { path: './docs/malformed-frontmatter.mdx', @@ -505,27 +536,30 @@ title: Malformed Frontmatter description: \`This frontmatter has an unbalanced quote --- -# Content with malformed frontmatter` - } - ]); +# Content with malformed frontmatter`, + }, + ]) // This should throw a parsing error - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - await expect(promise).rejects.toThrow("Plain value cannot start with reserved character"); - }); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow('Plain value cannot start with reserved character') + }) test('should require and validate mandatory frontmatter fields', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Missing Title", href: "/docs/missing-title" }]] - }) + navigation: [[{ title: 'Missing Title', href: '/docs/missing-title' }]], + }), }, { path: './docs/missing-title.mdx', @@ -533,45 +567,51 @@ description: \`This frontmatter has an unbalanced quote description: This frontmatter is missing the required title field --- -# Content with missing title in frontmatter` - } - ]); +# Content with missing title in frontmatter`, + }, + ]) // This should throw an error about missing title - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - await expect(promise).rejects.toThrow('Frontmatter must have a "title" property'); - }); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow('Frontmatter must have a "title" property') + }) test('should fail on special characters in paths', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Space in url", href: "/docs/space in url" }, - ]] - }) + navigation: [[{ title: 'Space in url', href: '/docs/space in url' }]], + }), }, { path: './docs/space in url.mdx', - content: `---\ntitle: Space in url\n---` - } - ]); - - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + content: `---\ntitle: Space in url\n---`, + }, + ]) - await expect(promise).rejects.toThrow('Href "/docs/space in url" contains characters that will be encoded by the browser, please remove them') - }); -}); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Href "/docs/space in url" contains characters that will be encoded by the browser, please remove them', + ) + }) +}) describe('Error Reporting', () => { test('should produce clear and informative error messages for validation failures', async () => { @@ -579,8 +619,8 @@ describe('Error Reporting', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Validation Error", href: "/docs/validation-error" }]] - }) + navigation: [[{ title: 'Validation Error', href: '/docs/validation-error' }]], + }), }, { path: './docs/validation-error.mdx', @@ -591,30 +631,35 @@ sdk: react, invalid-sdk # Validation Error Page -This page has an invalid SDK in frontmatter.` - } - ]); +This page has an invalid SDK in frontmatter.`, + }, + ]) // This should throw an error with specific message about invalid SDK - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - await expect(promise).rejects.toThrow('Invalid SDK ["invalid-sdk"], the valid SDKs are ["react"]'); - }); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow('Invalid SDK ["invalid-sdk"], the valid SDKs are ["react"]') + }) test('should handle errors when a referenced document exists but is invalid', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Valid Document", href: "/docs/valid-document" }, - { title: "Invalid Reference", href: "/docs/invalid-reference" } - ]] - }) + navigation: [ + [ + { title: 'Valid Document', href: '/docs/valid-document' }, + { title: 'Invalid Reference', href: '/docs/invalid-reference' }, + ], + ], + }), }, { path: './docs/valid-document.mdx', @@ -624,7 +669,7 @@ title: Valid Document # Valid Document -[Link to Invalid Reference](/docs/invalid-reference#non-existent-header)` +[Link to Invalid Reference](/docs/invalid-reference#non-existent-header)`, }, { path: './docs/invalid-reference.mdx', @@ -634,31 +679,36 @@ title: Invalid Reference # Invalid Reference -This document doesn't have the referenced header.` - } - ]); +This document doesn't have the referenced header.`, + }, + ]) // Should complete with warnings - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) // Should report warning about missing hash - expect(output).toContain('warning Hash "non-existent-header" not found in /docs/invalid-reference'); - }); + expect(output).toContain('warning Hash "non-existent-header" not found in /docs/invalid-reference') + }) test('should complete build workflow when errors are present in some files', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Valid Document", href: "/docs/valid-document" }, - { title: "Document with Warnings", href: "/docs/document-with-warnings" } - ]] - }) + navigation: [ + [ + { title: 'Valid Document', href: '/docs/valid-document' }, + { title: 'Document with Warnings', href: '/docs/document-with-warnings' }, + ], + ], + }), }, { path: './docs/valid-document.mdx', @@ -668,7 +718,7 @@ title: Valid Document # Valid Document -This is a completely valid document.` +This is a completely valid document.`, }, { path: './docs/document-with-warnings.mdx', @@ -682,22 +732,25 @@ title: Document with Warnings This content has an invalid SDK. -` - } - ]); +`, + }, + ]) // Should complete with warnings - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) // Check that warnings were reported - expect(output).toContain('warning Guide /docs/non-existent-document not found'); - expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK'); - }); -}); + expect(output).toContain('warning Guide /docs/non-existent-document not found') + expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK') + }) +}) describe('Advanced Features', () => { test('should correctly handle links with anchors to specific sections of documents', async () => { @@ -705,11 +758,13 @@ describe('Advanced Features', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Source Document", href: "/docs/source-document" }, - { title: "Target Document", href: "/docs/target-document" } - ]] - }) + navigation: [ + [ + { title: 'Source Document', href: '/docs/source-document' }, + { title: 'Target Document', href: '/docs/target-document' }, + ], + ], + }), }, { path: './docs/source-document.mdx', @@ -721,7 +776,7 @@ title: Source Document [Link to Section 1](/docs/target-document#section-1) [Link to Section 2](/docs/target-document#section-2) -[Link to Invalid Section](/docs/target-document#invalid-section)` +[Link to Invalid Section](/docs/target-document#invalid-section)`, }, { path: './docs/target-document.mdx', @@ -737,22 +792,24 @@ Content for section 1. ## Section 2 -Content for section 2.` - } - ]); +Content for section 2.`, + }, + ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) // Valid links should work without warnings - expect(output).not.toContain('warning Hash "section-1" not found'); - expect(output).not.toContain('warning Hash "section-2" not found'); - - // Invalid link should produce a warning - expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document'); - }); + expect(output).not.toContain('warning Hash "section-1" not found') + expect(output).not.toContain('warning Hash "section-2" not found') -}); \ No newline at end of file + // Invalid link should produce a warning + expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document') + }) +}) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 61d5bb2b09..7ae3f4ad97 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -15,49 +15,103 @@ import remarkMdx from 'remark-mdx' import { remark } from 'remark' import { visit as mdastVisit } from 'unist-util-visit' import remarkFrontmatter from 'remark-frontmatter' -import yaml from "yaml" +import yaml from 'yaml' import { slugifyWithCounter } from '@sindresorhus/slugify' import { toString } from 'mdast-util-to-string' import reporter from 'vfile-reporter' import readdirp from 'readdirp' -import { z } from "zod" -import { fromError } from 'zod-validation-error'; +import { z } from 'zod' +import { fromError } from 'zod-validation-error' import { Node } from 'unist' import chok from 'chokidar' const VALID_SDKS = [ - "nextjs", - "react", - "javascript-frontend", - "chrome-extension", - "expo", - "ios", - "nodejs", - "expressjs", - "fastify", - "react-router", - "remix", - "tanstack-start", - "go", - "astro", - "nuxt", - "vue", - "ruby", - "python", - "javascript-backend", - "sdk-development", - "community-sdk" + 'nextjs', + 'react', + 'javascript-frontend', + 'chrome-extension', + 'expo', + 'ios', + 'nodejs', + 'expressjs', + 'fastify', + 'react-router', + 'remix', + 'tanstack-start', + 'go', + 'astro', + 'nuxt', + 'vue', + 'ruby', + 'python', + 'javascript-backend', + 'sdk-development', + 'community-sdk', ] as const -type SDK = typeof VALID_SDKS[number] +type SDK = (typeof VALID_SDKS)[number] const sdk = z.enum(VALID_SDKS) -const icon = z.enum(["apple", "application-2", "arrow-up-circle", "astro", "angular", "block", "bolt", "book", "box", "c-sharp", "chart", "checkmark-circle", "chrome", "clerk", "code-bracket", "cog-6-teeth", "door", "elysia", "expressjs", "globe", "go", "home", "hono", "javascript", "koa", "link", "linkedin", "lock", "nextjs", "nodejs", "plug", "plus-circle", "python", "react", "redwood", "remix", "react-router", "rocket", "route", "ruby", "rust", "speedometer", "stacked-rectangle", "solid", "svelte", "tanstack", "user-circle", "user-dotted-circle", "vue", "x", "expo", "nuxt", "fastify"]) +const icon = z.enum([ + 'apple', + 'application-2', + 'arrow-up-circle', + 'astro', + 'angular', + 'block', + 'bolt', + 'book', + 'box', + 'c-sharp', + 'chart', + 'checkmark-circle', + 'chrome', + 'clerk', + 'code-bracket', + 'cog-6-teeth', + 'door', + 'elysia', + 'expressjs', + 'globe', + 'go', + 'home', + 'hono', + 'javascript', + 'koa', + 'link', + 'linkedin', + 'lock', + 'nextjs', + 'nodejs', + 'plug', + 'plus-circle', + 'python', + 'react', + 'redwood', + 'remix', + 'react-router', + 'rocket', + 'route', + 'ruby', + 'rust', + 'speedometer', + 'stacked-rectangle', + 'solid', + 'svelte', + 'tanstack', + 'user-circle', + 'user-dotted-circle', + 'vue', + 'x', + 'expo', + 'nuxt', + 'fastify', +]) type Icon = z.infer -const tag = z.enum(["(Beta)", "(Community)"]) +const tag = z.enum(['(Beta)', '(Community)']) type Tag = z.infer @@ -86,56 +140,57 @@ type Manifest = (ManifestItem | ManifestGroup)[][] // Create manifest schema based on config const createManifestSchema = (config: BuildConfig) => { - const manifestItem: z.ZodType = z.object({ - title: z.string(), - href: z.string(), - tag: tag.optional(), - wrap: z.boolean().default(config.manifestOptions.wrapDefault), - icon: icon.optional(), - target: z.enum(["_blank"]).optional(), - sdk: z.array(sdk).optional() - }).strict() - - const manifestGroup: z.ZodType = z.object({ - title: z.string(), - items: z.lazy(() => manifestSchema), - collapse: z.boolean().default(config.manifestOptions.collapseDefault), - tag: tag.optional(), - wrap: z.boolean().default(config.manifestOptions.wrapDefault), - icon: icon.optional(), - hideTitle: z.boolean().default(config.manifestOptions.hideTitleDefault), - sdk: z.array(sdk).optional() - }).strict() - - const manifestSchema: z.ZodType = z.array( - z.array( - z.union([ - manifestItem, - manifestGroup - ]) - ) - ) + const manifestItem: z.ZodType = z + .object({ + title: z.string(), + href: z.string(), + tag: tag.optional(), + wrap: z.boolean().default(config.manifestOptions.wrapDefault), + icon: icon.optional(), + target: z.enum(['_blank']).optional(), + sdk: z.array(sdk).optional(), + }) + .strict() + + const manifestGroup: z.ZodType = z + .object({ + title: z.string(), + items: z.lazy(() => manifestSchema), + collapse: z.boolean().default(config.manifestOptions.collapseDefault), + tag: tag.optional(), + wrap: z.boolean().default(config.manifestOptions.wrapDefault), + icon: icon.optional(), + hideTitle: z.boolean().default(config.manifestOptions.hideTitleDefault), + sdk: z.array(sdk).optional(), + }) + .strict() + + const manifestSchema: z.ZodType = z.array(z.array(z.union([manifestItem, manifestGroup]))) return { manifestItem, manifestGroup, - manifestSchema + manifestSchema, } } -const pleaseReport = "(this is a bug with the build script, please report)" +const pleaseReport = '(this is a bug with the build script, please report)' -const isValidSdk = (config: BuildConfig) => (sdk: string): sdk is SDK => { - return config.validSdks.includes(sdk as SDK) -} +const isValidSdk = + (config: BuildConfig) => + (sdk: string): sdk is SDK => { + return config.validSdks.includes(sdk as SDK) + } -const isValidSdks = (config: BuildConfig) => (sdks: string[]): sdks is SDK[] => { - return sdks.every(isValidSdk(config)) -} +const isValidSdks = + (config: BuildConfig) => + (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk(config)) + } const readManifest = (config: BuildConfig) => async (): Promise => { const { manifestSchema } = createManifestSchema(config) - const unsafe_manifest = await fs.readFile(config.manifestFilePath, { "encoding": "utf-8" }) + const unsafe_manifest = await fs.readFile(config.manifestFilePath, { encoding: 'utf-8' }) const manifest = await manifestSchema.safeParseAsync(JSON.parse(unsafe_manifest).navigation) @@ -150,7 +205,7 @@ const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { const filePath = path.join(config.basePath, docPath) try { - const fileContent = await fs.readFile(filePath, { "encoding": "utf-8" }) + const fileContent = await fs.readFile(filePath, { encoding: 'utf-8' }) return [null, fileContent] as const } catch (error) { return [new Error(`file ${filePath} doesn't exist`, { cause: error }), null] as const @@ -160,8 +215,9 @@ const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { const readDocsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.docsPath, { type: 'files', - fileFilter: (entry) => config.ignorePaths.some((ignoreItem) => - `/docs/${entry.path}`.startsWith(ignoreItem)) === false && entry.path.endsWith('.mdx') + fileFilter: (entry) => + config.ignorePaths.some((ignoreItem) => `/docs/${entry.path}`.startsWith(ignoreItem)) === false && + entry.path.endsWith('.mdx'), }) } @@ -173,28 +229,27 @@ const readPartialsFolder = (config: BuildConfig) => async () => { } const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => { - const readFile = readMarkdownFile(config); + const readFile = readMarkdownFile(config) - return Promise.all(paths.map(async (markdownPath) => { - const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, markdownPath) + return Promise.all( + paths.map(async (markdownPath) => { + const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, markdownPath) - const [error, content] = await readFile(fullPath) + const [error, content] = await readFile(fullPath) - if (error) { - throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) - } + if (error) { + throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) + } - return { - path: markdownPath, - content, - } - })) + return { + path: markdownPath, + content, + } + }), + ) } -const markdownProcessor = remark() - .use(remarkFrontmatter) - .use(remarkMdx) - .freeze() +const markdownProcessor = remark().use(remarkFrontmatter).use(remarkMdx).freeze() type VFile = Awaited> @@ -205,80 +260,87 @@ const removeMdxSuffix = (filePath: string) => { return filePath } -type BlankTree }> = Array>; +type BlankTree }> = Array> const traverseTree = async < Tree extends { items: BlankTree }, - InItem extends Extract, - InGroup extends Extract }>, + InItem extends Extract, + InGroup extends Extract }>, OutItem extends { href: string }, OutGroup extends { items: BlankTree }, - OutTree extends BlankTree + OutTree extends BlankTree, >( tree: Tree, itemCallback: (item: InItem, tree: Tree) => Promise = async (item) => item, groupCallback: (group: InGroup, tree: Tree) => Promise = async (group) => group, errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, ): Promise => { - const result = await Promise.all(tree.items.map(async (group) => { - return await Promise.all(group.map(async (item) => { - try { - if ('href' in item) { - return await itemCallback(item, tree); - } + const result = await Promise.all( + tree.items.map(async (group) => { + return await Promise.all( + group.map(async (item) => { + try { + if ('href' in item) { + return await itemCallback(item, tree) + } - if ('items' in item && Array.isArray(item.items)) { - const newGroup = await groupCallback(item, tree); + if ('items' in item && Array.isArray(item.items)) { + const newGroup = await groupCallback(item, tree) - if (newGroup === null) return null; + if (newGroup === null) return null - // @ts-expect-error - OutGroup should always contain "items" property, so this is safe - const newItems = (await traverseTree(newGroup, itemCallback, groupCallback, errorCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) + // @ts-expect-error - OutGroup should always contain "items" property, so this is safe + const newItems = (await traverseTree(newGroup, itemCallback, groupCallback, errorCallback)).map((group) => + group.filter((item): item is NonNullable => item !== null), + ) - return { - ...newGroup, - items: newItems - } - } + return { + ...newGroup, + items: newItems, + } + } - return item as OutItem; - } catch (error) { - if (error instanceof Error && errorCallback !== undefined) { - errorCallback(item, error); - } else { - throw error - } - } - })); - })); + return item as OutItem + } catch (error) { + if (error instanceof Error && errorCallback !== undefined) { + errorCallback(item, error) + } else { + throw error + } + } + }), + ) + }), + ) - return result.map(group => group.filter((item): item is NonNullable => item !== null)) as unknown as OutTree; -}; + return result.map((group) => + group.filter((item): item is NonNullable => item !== null), + ) as unknown as OutTree +} function flattenTree< Tree extends BlankTree, InItem extends Extract, - InGroup extends Extract }> + InGroup extends Extract }>, >(tree: Tree): InItem[] { - const result: InItem[] = []; + const result: InItem[] = [] for (const group of tree) { for (const itemOrGroup of group) { - if ("href" in itemOrGroup) { + if ('href' in itemOrGroup) { // It's an item - result.push(itemOrGroup); - } else if ("items" in itemOrGroup && Array.isArray(itemOrGroup.items)) { + result.push(itemOrGroup) + } else if ('items' in itemOrGroup && Array.isArray(itemOrGroup.items)) { // It's a group with its own sub-tree, flatten it - result.push(...flattenTree(itemOrGroup.items)); + result.push(...flattenTree(itemOrGroup.items)) } } } - return result; + return result } const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { - // This is external so can't change it if (href.startsWith('/docs') === false) return href @@ -300,72 +362,53 @@ const extractComponentPropValueFromNode = ( componentName: string, propName: string, ): string | undefined => { - // Check if it's an MDX component - if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement") { - return undefined; + if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') { + return undefined } // Check if it's the correct component - if (!("name" in node)) return undefined; - if (node.name !== componentName) return undefined; + if (!('name' in node)) return undefined + if (node.name !== componentName) return undefined // Check for attributes - if (!("attributes" in node)) { - vfile?.message( - `<${componentName} /> component has no props`, - node.position - ); - return undefined; + if (!('attributes' in node)) { + vfile?.message(`<${componentName} /> component has no props`, node.position) + return undefined } if (!Array.isArray(node.attributes)) { - vfile?.message( - `<${componentName} /> node attributes is not an array ${pleaseReport}`, - node.position - ); - return undefined; + vfile?.message(`<${componentName} /> node attributes is not an array ${pleaseReport}`, node.position) + return undefined } // Find the requested prop - const propAttribute = node.attributes.find( - (attribute) => attribute.name === propName - ); + const propAttribute = node.attributes.find((attribute) => attribute.name === propName) if (propAttribute === undefined) { - vfile?.message( - `<${componentName} /> component has no "${propName}" attribute`, - node.position - ); - return undefined; + vfile?.message(`<${componentName} /> component has no "${propName}" attribute`, node.position) + return undefined } - const value = propAttribute.value; + const value = propAttribute.value if (value === undefined) { - vfile?.message( - `<${componentName} /> attribute "${propName}" has no value ${pleaseReport}`, - node.position - ); - return undefined; + vfile?.message(`<${componentName} /> attribute "${propName}" has no value ${pleaseReport}`, node.position) + return undefined } // Handle both string values and object values (like JSX expressions) - if (typeof value === "string") { - return value; - } else if (typeof value === "object" && "value" in value) { - return value.value; + if (typeof value === 'string') { + return value + } else if (typeof value === 'object' && 'value' in value) { + return value.value } - vfile?.message( - `<${componentName} /> attribute "${propName}" has an unsupported value type`, - node.position - ); - return undefined; + vfile?.message(`<${componentName} /> attribute "${propName}" has an unsupported value type`, node.position) + return undefined } const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile | undefined, sdkProp: string) => { - const isValidItem = isValidSdk(config) const isValidItems = isValidSdks(config) @@ -374,7 +417,7 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile if (isValidItems(sdks)) { return sdks } else { - const invalidSDKs = sdks.filter(sdk => !isValidItem(sdk)) + const invalidSDKs = sdks.filter((sdk) => !isValidItem(sdk)) vfile?.message(`sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, node.position) } } else { @@ -386,172 +429,176 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile } } -const parseInMarkdownFile = (config: BuildConfig) => async ( - href: string, - partials: { path: string; content: string; }[], - inManifest: boolean, -) => { - const readFile = readMarkdownFile(config); - const [error, fileContent] = await readFile(`${href}.mdx`) - - if (error !== null) { - throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { cause: error }) - } - - type Frontmatter = { - title: string; - description?: string; - sdk?: SDK[] - } +const parseInMarkdownFile = + (config: BuildConfig) => async (href: string, partials: { path: string; content: string }[], inManifest: boolean) => { + const readFile = readMarkdownFile(config) + const [error, fileContent] = await readFile(`${href}.mdx`) - let frontmatter: Frontmatter | undefined = undefined + if (error !== null) { + throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { + cause: error, + }) + } - const slugify = slugifyWithCounter() - const headingsHashs: Array = [] + type Frontmatter = { + title: string + description?: string + sdk?: SDK[] + } - const vfile = await markdownProcessor() - .use(() => (tree, vfile) => { - if (inManifest === false) { - vfile.message("This guide is not in the manifest.json, but will still be publicly accessible and other guides can link to it") - } + let frontmatter: Frontmatter | undefined = undefined - if (href !== encodeURI(href)) { - vfile.fail(`Href "${href}" contains characters that will be encoded by the browser, please remove them`) - } - }) - .use(() => (tree, vfile) => { - mdastVisit(tree, - node => node.type === 'yaml' && "value" in node, - node => { - if (!("value" in node)) return; - if (typeof node.value !== "string") return; + const slugify = slugifyWithCounter() + const headingsHashs: Array = [] - const frontmatterYaml: Record<"title" | "description" | "sdk", string | undefined> = yaml.parse(node.value) + const vfile = await markdownProcessor() + .use(() => (tree, vfile) => { + if (inManifest === false) { + vfile.message( + 'This guide is not in the manifest.json, but will still be publicly accessible and other guides can link to it', + ) + } - const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') + if (href !== encodeURI(href)) { + vfile.fail(`Href "${href}" contains characters that will be encoded by the browser, please remove them`) + } + }) + .use(() => (tree, vfile) => { + mdastVisit( + tree, + (node) => node.type === 'yaml' && 'value' in node, + (node) => { + if (!('value' in node)) return + if (typeof node.value !== 'string') return - if (frontmatterSDKs !== undefined && isValidSdks(config)(frontmatterSDKs) === false) { - const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(config)(sdk) === false) - vfile.fail(`Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(config.validSdks)}`, node.position) - return; - } + const frontmatterYaml: Record<'title' | 'description' | 'sdk', string | undefined> = yaml.parse(node.value) - if (frontmatterYaml.title === undefined) { - vfile.fail(`Frontmatter must have a "title" property`, node.position) - return; - } + const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') - frontmatter = { - title: frontmatterYaml.title, - description: frontmatterYaml.description, - sdk: frontmatterSDKs - } - } - ) + if (frontmatterSDKs !== undefined && isValidSdks(config)(frontmatterSDKs) === false) { + const invalidSDKs = frontmatterSDKs.filter((sdk) => isValidSdk(config)(sdk) === false) + vfile.fail( + `Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(config.validSdks)}`, + node.position, + ) + return + } - if (frontmatter === undefined) { - vfile.fail(`Frontmatter parsing failed for ${href}`) - return; - } + if (frontmatterYaml.title === undefined) { + vfile.fail(`Frontmatter must have a "title" property`, node.position) + return + } - }) - // Validate the - .use(() => (tree, vfile) => { - return mdastVisit(tree, - node => { + frontmatter = { + title: frontmatterYaml.title, + description: frontmatterYaml.description, + sdk: frontmatterSDKs, + } + }, + ) - const partialSrc = extractComponentPropValueFromNode(node, vfile, "Include", "src") + if (frontmatter === undefined) { + vfile.fail(`Frontmatter parsing failed for ${href}`) + return + } + }) + // Validate the + .use(() => (tree, vfile) => { + return mdastVisit(tree, (node) => { + const partialSrc = extractComponentPropValueFromNode(node, vfile, 'Include', 'src') - if (partialSrc === undefined) return; + if (partialSrc === undefined) return if (partialSrc.startsWith('_partials/') === false) { vfile.message(` prop "src" must start with "_partials/"`, node.position) - return; + return } - const partial = partials.find((partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`) + const partial = partials.find( + (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, + ) if (partial === undefined) { vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) - return; + return } - const partialContentVFile = markdownProcessor() .use(() => (tree, vfile) => { - mdastVisit(tree, - node => (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && "name" in node && node.name === "Include", + mdastVisit( + tree, + (node) => + (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && + 'name' in node && + node.name === 'Include', () => { vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) - } + }, ) }) .processSync({ path: partial.path, - value: partial.content + value: partial.content, }) const partialContentReport = reporter([partialContentVFile], { quiet: true }) - if (partialContentReport !== "") { + if (partialContentReport !== '') { console.error(partialContentReport) } + }) + }) + // extract out the headings to check hashes in links + .use(() => (tree) => { + mdastVisit( + tree, + (node) => node.type === 'heading', + (node) => { + // @ts-expect-error - If the heading has a id in it, this will pick it up + // eg # test {{ id: 'my-heading' }} + // This is for remapping the hash to the custom id + const id = node?.children?.[1]?.data?.estree?.body?.[0]?.expression?.properties?.[0]?.value?.value as + | string + | undefined + + if (id !== undefined) { + headingsHashs.push(id) + } else { + const slug = slugify(toString(node).trim()) + headingsHashs.push(slug) + } + }, + ) + }) + .process({ + path: `${href}.mdx`, + value: fileContent, + }) - } - ) - }) - // extract out the headings to check hashes in links - .use(() => (tree) => { - mdastVisit(tree, - node => node.type === "heading", - node => { - - // @ts-expect-error - If the heading has a id in it, this will pick it up - // eg # test {{ id: 'my-heading' }} - // This is for remapping the hash to the custom id - const id = node?.children?.[1]?.data?.estree?.body?.[0]?.expression?.properties?.[0]?.value?.value as string | undefined - - if (id !== undefined) { - headingsHashs.push(id) - } else { - const slug = slugify(toString(node).trim()) - headingsHashs.push(slug) - } - } - ) - }) - .process({ - path: `${href}.mdx`, - value: fileContent - }) - - if (frontmatter === undefined) { - throw new Error(`Frontmatter parsing failed for ${href}`) - } + if (frontmatter === undefined) { + throw new Error(`Frontmatter parsing failed for ${href}`) + } - return { - href, - sdk: (frontmatter as Frontmatter).sdk, - vfile, - headingsHashs, - frontmatter: frontmatter as Frontmatter + return { + href, + sdk: (frontmatter as Frontmatter).sdk, + vfile, + headingsHashs, + frontmatter: frontmatter as Frontmatter, + } } -} export const createBlankStore = () => ({ - markdownFiles: new Map>>>() + markdownFiles: new Map>>>(), }) -export const build = async ( - store: ReturnType, - config: BuildConfig -) => { +export const build = async (store: ReturnType, config: BuildConfig) => { // Apply currying to create functions pre-configured with config - const getManifest = readManifest(config); - const getDocsFolder = readDocsFolder(config); - const getPartialsFolder = readPartialsFolder(config); - const getPartialsMarkdown = readPartialsMarkdown(config); - const parseMarkdownFile = parseInMarkdownFile(config); + const getManifest = readManifest(config) + const getDocsFolder = readDocsFolder(config) + const getPartialsFolder = readPartialsFolder(config) + const getPartialsMarkdown = readPartialsMarkdown(config) + const parseMarkdownFile = parseInMarkdownFile(config) const userManifest = await getManifest() console.info('✔️ Read Manifest') @@ -559,62 +606,62 @@ export const build = async ( const docsFiles = await getDocsFolder() console.info('✔️ Read Docs Folder') - const partials = await getPartialsMarkdown( - (await getPartialsFolder()).map(item => item.path) - ) + const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) console.info('✔️ Read Partials') const guides = new Map>>() const guidesInManifest = new Set() // Grab all the docs links in the manifest - await traverseTree({ items: userManifest }, - async (item) => { - if (!item.href?.startsWith('/docs/')) return item - if (item.target !== undefined) return item + await traverseTree({ items: userManifest }, async (item) => { + if (!item.href?.startsWith('/docs/')) return item + if (item.target !== undefined) return item - const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) - if (ignore === true) return item + const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) + if (ignore === true) return item - guidesInManifest.add(item.href) + guidesInManifest.add(item.href) - return item - } - ) + return item + }) console.info('✔️ Parsed in Manifest') // Read in all the guides - const docs = (await Promise.all(docsFiles.map(async (file) => { - const href = removeMdxSuffix(`/docs/${file.path}`) + const docs = ( + await Promise.all( + docsFiles.map(async (file) => { + const href = removeMdxSuffix(`/docs/${file.path}`) - const alreadyLoaded = guides.get(href) + const alreadyLoaded = guides.get(href) - if (alreadyLoaded) return null // already processed + if (alreadyLoaded) return null // already processed - const inManifest = guidesInManifest.has(href) + const inManifest = guidesInManifest.has(href) - let markdownFile: Awaited>; + let markdownFile: Awaited> - const cachedMarkdownFile = store.markdownFiles.get(href) + const cachedMarkdownFile = store.markdownFiles.get(href) - if (cachedMarkdownFile) { - markdownFile = structuredClone(cachedMarkdownFile) - } else { - markdownFile = await parseMarkdownFile(href, partials, inManifest) + if (cachedMarkdownFile) { + markdownFile = structuredClone(cachedMarkdownFile) + } else { + markdownFile = await parseMarkdownFile(href, partials, inManifest) - store.markdownFiles.set(href, structuredClone(markdownFile)) - } + store.markdownFiles.set(href, structuredClone(markdownFile)) + } - guides.set(href, markdownFile) + guides.set(href, markdownFile) - return markdownFile - }))).filter((item): item is NonNullable => item !== null) + return markdownFile + }), + ) + ).filter((item): item is NonNullable => item !== null) console.info(`✔️ Loaded in ${docs.length} guides`) // Goes through and grabs the sdk scoping out of the manifest - const sdkScopedManifest = await traverseTree({ items: userManifest, sdk: undefined as undefined | SDK[] }, + const sdkScopedManifest = await traverseTree( + { items: userManifest, sdk: undefined as undefined | SDK[] }, async (item, tree) => { - if (!item.href?.startsWith('/docs/')) return item if (item.target !== undefined) return item @@ -630,25 +677,30 @@ export const build = async ( const sdk = guide.sdk ?? tree.sdk if (guide.sdk !== undefined && tree.sdk !== undefined) { - if (guide.sdk.every(sdk => tree.sdk?.includes(sdk)) === false) { - throw new Error(`Guide "${item.title}" is attempting to use ${JSON.stringify(guide.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`) + if (guide.sdk.every((sdk) => tree.sdk?.includes(sdk)) === false) { + throw new Error( + `Guide "${item.title}" is attempting to use ${JSON.stringify(guide.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`, + ) } } return { ...item, - sdk + sdk, } }, async (group, tree) => { - - const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter((sdk): sdk is SDK => sdk !== undefined) + const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter( + (sdk): sdk is SDK => sdk !== undefined, + ) const { items, ...details } = group if (details.sdk !== undefined && tree.sdk !== undefined) { - if (details.sdk.every(sdk => tree.sdk?.includes(sdk)) === false) { - throw new Error(`Group "${details.title}" is attempting to use ${JSON.stringify(details.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`) + if (details.sdk.every((sdk) => tree.sdk?.includes(sdk)) === false) { + throw new Error( + `Group "${details.title}" is attempting to use ${JSON.stringify(details.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`, + ) } } @@ -656,14 +708,14 @@ export const build = async ( return { ...details, - sdk: Array.from(new Set([...details.sdk ?? [], ...itemsSDKs])) ?? [], - items + sdk: Array.from(new Set([...(details.sdk ?? []), ...itemsSDKs])) ?? [], + items, } as ManifestGroup }, (item, error) => { console.error('↳', item.title) throw error - } + }, ) console.info('✔️ Applied manifest sdk scoping') @@ -672,31 +724,30 @@ export const build = async ( // It would definitely be preferable we didn't need to do this markdown processing twice // But because we need a full list / hashmap of all the existing docs, we can't // Unless maybe we do some kind of lazy loading of the docs, but this would add complexity - const coreVFiles = await Promise.all(docs.map(async (doc) => { - const vfile = await markdownProcessor() - // Validate links between guides are valid - .use(() => (tree: Node, vfile: VFile) => { - return mdastVisit(tree, - node => { - - if (node.type !== "link") return; - if (!("url" in node)) return; - if (typeof node.url !== "string") return; - if (!node.url.startsWith("/docs/")) return; - if (!("children" in node)) return; + const coreVFiles = await Promise.all( + docs.map(async (doc) => { + const vfile = await markdownProcessor() + // Validate links between guides are valid + .use(() => (tree: Node, vfile: VFile) => { + return mdastVisit(tree, (node) => { + if (node.type !== 'link') return + if (!('url' in node)) return + if (typeof node.url !== 'string') return + if (!node.url.startsWith('/docs/')) return + if (!('children' in node)) return node.url = removeMdxSuffix(node.url) - const [url, hash] = (node.url as string).split("#") + const [url, hash] = (node.url as string).split('#') const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return; + if (ignore === true) return const guide = guides.get(url) if (guide === undefined) { vfile.message(`Guide ${url} not found`, node.position) - return; + return } if (hash !== undefined) { @@ -706,17 +757,14 @@ export const build = async ( vfile.message(`Hash "${hash}" not found in ${url}`, node.position) } } - } - ) - }) - // Validate the components - .use(() => (tree, vfile) => { - - mdastVisit(tree, - (node) => { - const sdk = extractComponentPropValueFromNode(node, vfile, "If", "sdk") + }) + }) + // Validate the components + .use(() => (tree, vfile) => { + mdastVisit(tree, (node) => { + const sdk = extractComponentPropValueFromNode(node, vfile, 'If', 'sdk') - if (sdk === undefined) return; + if (sdk === undefined) return const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk) @@ -727,90 +775,104 @@ export const build = async ( const availableSDKs = manifestItems.flatMap((item) => item.sdk).filter(Boolean) // The doc doesn't exist in the manifest so we are skipping it - if (manifestItems.length === 0) return; + if (manifestItems.length === 0) return - sdksFilter.forEach(sdk => { - (() => { - if (doc.sdk === undefined) return; + sdksFilter.forEach((sdk) => { + ;(() => { + if (doc.sdk === undefined) return const available = doc.sdk.includes(sdk) if (available === false) { - vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${doc.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) + vfile.fail( + ` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${doc.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, + node.position, + ) } - })(); + })() - (() => { + ;(() => { // The doc is generic so we are skipping it - if (availableSDKs.length === 0) return; + if (availableSDKs.length === 0) return const available = availableSDKs.includes(sdk) if (available === false) { - vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, node.position) + vfile.fail( + ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, + node.position, + ) } - })(); + })() }) - } - ) - }) - .process(doc.vfile) - - const distFilePath = `${doc.href.replace("/docs/", "")}.mdx` + }) + }) + .process(doc.vfile) - if (isValidSdk(config)(distFilePath.split('/')[0])) { - throw new Error(`Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`) - } + const distFilePath = `${doc.href.replace('/docs/', '')}.mdx` + if (isValidSdk(config)(distFilePath.split('/')[0])) { + throw new Error( + `Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`, + ) + } - return vfile - })) + return vfile + }), + ) console.info(`✔️ Wrote out ${docs.length} core docs`) - const sdkSpecificVFiles = await Promise.all(config.validSdks.map(async (targetSdk) => { - const vFiles = await Promise.all(docs.map(async (doc) => { - if (doc.sdk === undefined) return null; // skip core docs - if (doc.sdk.includes(targetSdk) === false) return null; // skip docs that are not for the target sdk + const sdkSpecificVFiles = await Promise.all( + config.validSdks.map(async (targetSdk) => { + const vFiles = await Promise.all( + docs.map(async (doc) => { + if (doc.sdk === undefined) return null // skip core docs + if (doc.sdk.includes(targetSdk) === false) return null // skip docs that are not for the target sdk - const vfile = await markdownProcessor() - // scope urls so they point to the current sdk - .use(() => (tree, vfile) => { - return mdastVisit(tree, - node => { - if (node.type !== "link") return; - if (!("url" in node)) { - vfile.fail(`Link node does not have a url property ${pleaseReport}`, node.position) - return; - } - if (typeof node.url !== "string") { - vfile.fail(`Link node url must be a string ${pleaseReport}`, node.position) - return; - } - } - ) - }) - .process({ - ...doc.vfile, messages: [] // reset the messages, otherwise they will be duplicated - }) + const vfile = await markdownProcessor() + // scope urls so they point to the current sdk + .use(() => (tree, vfile) => { + return mdastVisit(tree, (node) => { + if (node.type !== 'link') return + if (!('url' in node)) { + vfile.fail(`Link node does not have a url property ${pleaseReport}`, node.position) + return + } + if (typeof node.url !== 'string') { + vfile.fail(`Link node url must be a string ${pleaseReport}`, node.position) + return + } + }) + }) + .process({ + ...doc.vfile, + messages: [], // reset the messages, otherwise they will be duplicated + }) - return vfile - })) + return vfile + }), + ) - return { targetSdk, vFiles } - })) + return { targetSdk, vFiles } + }), + ) - sdkSpecificVFiles.forEach(({ targetSdk, vFiles }) => console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific guides`)) + sdkSpecificVFiles.forEach(({ targetSdk, vFiles }) => + console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific guides`), + ) const flatSdkSpecificVFiles = sdkSpecificVFiles.flatMap(({ vFiles }) => vFiles) - const output = reporter([ - ...coreVFiles.filter((item): item is NonNullable => item !== null), - ...flatSdkSpecificVFiles.filter((item): item is NonNullable => item !== null) - ], - { quiet: true }) + const output = reporter( + [ + ...coreVFiles.filter((item): item is NonNullable => item !== null), + ...flatSdkSpecificVFiles.filter((item): item is NonNullable => item !== null), + ], + { quiet: true }, + ) - if (output !== "") { + if (output !== '') { console.info(output) } @@ -818,17 +880,17 @@ export const build = async ( } type BuildConfigOptions = { - basePath: string; - validSdks: readonly SDK[]; - docsPath: string; - manifestPath: string; - partialsPath: string; - ignorePaths: string[]; + basePath: string + validSdks: readonly SDK[] + docsPath: string + manifestPath: string + partialsPath: string + ignorePaths: string[] manifestOptions: { - wrapDefault: boolean; - collapseDefault: boolean; - hideTitleDefault: boolean; - }; + wrapDefault: boolean + collapseDefault: boolean + hideTitleDefault: boolean + } } type BuildConfig = ReturnType @@ -855,20 +917,19 @@ export function createConfig(config: BuildConfigOptions) { manifestOptions: config.manifestOptions ?? { wrapDefault: true, collapseDefault: false, - hideTitleDefault: false + hideTitleDefault: false, }, } } const main = async () => { - const config = createConfig({ basePath: process.cwd(), docsPath: './docs', manifestPath: './docs/manifest.json', partialsPath: './_partials', ignorePaths: [ - "/docs/core-1", + '/docs/core-1', '/pricing', '/docs/reference/backend-api', '/docs/reference/frontend-api', @@ -879,23 +940,22 @@ const main = async () => { '/contact/support', '/blog', '/changelog/2024-04-19', - "/docs/_partials" + '/docs/_partials', ], validSdks: VALID_SDKS, manifestOptions: { wrapDefault: true, collapseDefault: false, - hideTitleDefault: false - } + hideTitleDefault: false, + }, }) - const store = createBlankStore(); - - await build(store, config); + const store = createBlankStore() + await build(store, config) } // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts if (require.main === module) { - main(); -} \ No newline at end of file + main() +} From 99931583532a89f6dfa0e07100a15e09678cae17 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 5 Mar 2025 23:07:35 +0800 Subject: [PATCH 044/114] run prettier --- scripts/build-docs.test.ts | 1533 ++++++++++++++++++++---------------- scripts/build-docs.ts | 1168 ++++++++++++++------------- 2 files changed, 1456 insertions(+), 1245 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index f21cf8e05b..f7021c7766 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -1,8 +1,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import os from 'node:os' -import { glob } from 'glob'; - +import { glob } from 'glob' import { describe, expect, onTestFinished, test } from 'vitest' import { build, createBlankStore, createConfig } from './build-docs' @@ -16,16 +15,16 @@ const tempConfig = { // Whether to preserve temp directories after tests // (helpful for debugging, but requires manual cleanup) - preserveTemp: false + preserveTemp: false, } async function createTempFiles( files: { path: string; content: string }[], options?: { - prefix?: string; // Prefix for the temp directory name - preserveTemp?: boolean; // Override global preserveTemp setting - useLocalTemp?: boolean; // Override global useLocalTemp setting - } + prefix?: string // Prefix for the temp directory name + preserveTemp?: boolean // Override global preserveTemp setting + useLocalTemp?: boolean // Override global useLocalTemp setting + }, ) { const prefix = options?.prefix || 'clerk-docs-test-' const preserve = options?.preserveTemp ?? tempConfig.preserveTemp @@ -81,14 +80,14 @@ async function createTempFiles( listFiles: async () => { return glob('**/*', { cwd: tempDir, - nodir: true + nodir: true, }) }, // Read file contents readFile: async (filePath: string): Promise => { return fs.readFile(path.join(tempDir, filePath), 'utf-8') - } + }, } } @@ -106,14 +105,14 @@ async function readFile(filePath: string): Promise { } function normalizeString(str: string): string { - return str.replace(/\r\n/g, '\n').trim(); + return str.replace(/\r\n/g, '\n').trim() } function treeDir(baseDir: string) { return glob('**/*', { cwd: baseDir, - nodir: true // Only return files, not directories - }); + nodir: true, // Only return files, not directories + }) } const baseConfig = { @@ -121,12 +120,12 @@ const baseConfig = { manifestPath: './docs/manifest.json', partialsPath: './_partials', distPath: './dist', - ignorePaths: ["/docs/_partials"], + ignorePaths: ['/docs/_partials'], manifestOptions: { wrapDefault: true, collapseDefault: false, - hideTitleDefault: false - } + hideTitleDefault: false, + }, } test('Basic build test with simple files', async () => { @@ -135,8 +134,8 @@ test('Basic build test with simple files', async () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -146,15 +145,18 @@ title: Simple Test # Simple Test Page -Testing with a simple page.` - } +Testing with a simple page.`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["nextjs", "react"], - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react'], + }), + ) expect(await fileExists(pathJoin('./dist/simple-test.mdx'))).toBe(true) expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(`--- @@ -166,15 +168,18 @@ title: Simple Test Testing with a simple page.`) expect(await fileExists(pathJoin('./dist/nextjs/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/nextjs/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - })) + expect(await readFile(pathJoin('./dist/nextjs/manifest.json'))).toBe( + JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + ) expect(await fileExists(pathJoin('./dist/react/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - })) - + expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe( + JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + ) }) test('Two Docs, each grouped by a different SDK', async () => { @@ -186,26 +191,18 @@ test('Two Docs, each grouped by a different SDK', async () => { navigation: [ [ { - title: "React", - sdk: ["react"], - items: [ - [ - { title: "Quickstart", href: "/docs/quickstart/react" } - ] - ] + title: 'React', + sdk: ['react'], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/react' }]], }, { - title: "Vue", - sdk: ["vue"], - items: [ - [ - { title: "Quickstart", href: "/docs/quickstart/vue" } - ] - ] - } + title: 'Vue', + sdk: ['vue'], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue' }]], + }, ], - ] - }) + ], + }), }, { path: './docs/quickstart/react.mdx', @@ -213,7 +210,7 @@ test('Two Docs, each grouped by a different SDK', async () => { title: Quickstart --- -# React Quickstart` +# React Quickstart`, }, { path: './docs/quickstart/vue.mdx', @@ -221,47 +218,46 @@ title: Quickstart title: Quickstart --- -# Vue Quickstart` - } +# Vue Quickstart`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "vue"] - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'vue'], + }), + ) expect(await fileExists(pathJoin('./dist/react/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ - navigation: [ - [ - { - title: "React", - items: [ - [ - { title: "Quickstart", href: "/docs/quickstart/react" } - ] - ] - }, + expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe( + JSON.stringify({ + navigation: [ + [ + { + title: 'React', + items: [[{ title: 'Quickstart', href: '/docs/quickstart/react' }]], + }, + ], ], - ] - })) + }), + ) expect(await fileExists(pathJoin('./dist/vue/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe(JSON.stringify({ - navigation: [ - [ - { - title: "Vue", - items: [ - [ - { title: "Quickstart", href: "/docs/quickstart/vue" } - ] - ] - }, + expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe( + JSON.stringify({ + navigation: [ + [ + { + title: 'Vue', + items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue' }]], + }, + ], ], - ] - })) + }), + ) const distFiles = await treeDir(pathJoin('./dist')) @@ -270,7 +266,6 @@ title: Quickstart expect(distFiles).toContain('react/manifest.json') expect(distFiles).toContain('quickstart/vue.mdx') expect(distFiles).toContain('quickstart/react.mdx') - }) test('sdk in frontmatter filters the docs', async () => { @@ -278,8 +273,8 @@ test('sdk in frontmatter filters the docs', async () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -290,18 +285,24 @@ sdk: react # Simple Test Page -Testing with a simple page.` - }]) +Testing with a simple page.`, + }, + ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) - expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/react/simple-test" }]] - })) + expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe( + JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/react/simple-test' }]], + }), + ) expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- title: Simple Test @@ -312,7 +313,9 @@ sdk: react Testing with a simple page.`) - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(``) + expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe( + ``, + ) const distFiles = await treeDir(pathJoin('./dist')) @@ -327,8 +330,8 @@ test('3 sdks in frontmatter generates 3 variants', async () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -339,25 +342,34 @@ sdk: react, vue, astro # Simple Test Page -Testing with a simple page.` - } +Testing with a simple page.`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "vue", "astro"] - })) - - expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/react/simple-test" }]] - })) - expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/vue/simple-test" }]] - })) - expect(await readFile(pathJoin('./dist/astro/manifest.json'))).toBe(JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/astro/simple-test" }]] - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'vue', 'astro'], + }), + ) + + expect(await readFile(pathJoin('./dist/react/manifest.json'))).toBe( + JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/react/simple-test' }]], + }), + ) + expect(await readFile(pathJoin('./dist/vue/manifest.json'))).toBe( + JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/vue/simple-test' }]], + }), + ) + expect(await readFile(pathJoin('./dist/astro/manifest.json'))).toBe( + JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/astro/simple-test' }]], + }), + ) const distFiles = await treeDir(pathJoin('./dist')) @@ -376,8 +388,8 @@ test(' content filtered out when sdk is in frontmatter', async () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -392,15 +404,18 @@ sdk: react, expo React Content -Testing with a simple page.` - } +Testing with a simple page.`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "expo"] - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'expo'], + }), + ) expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('React Content') @@ -412,8 +427,8 @@ test('Invalid SDK in frontmatter fails the build', async () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -424,15 +439,18 @@ sdk: react, expo, coffeescript # Simple Test Page -Testing with a simple page.` - } +Testing with a simple page.`, + }, ]) - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "expo"] - })) + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'expo'], + }), + ) await expect(promise).rejects.toThrow(`Invalid SDK ["coffeescript"], the valid SDKs are ["react","expo"]`) }) @@ -442,8 +460,8 @@ test('Invalid SDK in fails the build', async () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -458,15 +476,18 @@ sdk: react, expo astro Content -Testing with a simple page.` - } +Testing with a simple page.`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "expo"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'expo'], + }), + ) expect(output).toContain(`warning sdk \"astro\" in is not a valid SDK`) }) @@ -477,12 +498,12 @@ describe('Includes and Partials', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/_partials/test-partial.mdx', - content: `Test Partial Content` + content: `Test Partial Content`, }, { path: './docs/simple-test.mdx', @@ -492,15 +513,18 @@ title: Simple Test -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toContain('Test Partial Content') }) @@ -510,8 +534,8 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -521,15 +545,18 @@ title: Simple Test -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).toContain(`warning Partial /docs/_partials/test-partial.mdx not found`) }) @@ -539,16 +566,16 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/_partials/test-partial-1.mdx', - content: `` + content: ``, }, { path: './docs/_partials/test-partial-2.mdx', - content: `Test Partial Content` + content: `Test Partial Content`, }, { path: './docs/simple-test.mdx', @@ -558,15 +585,18 @@ title: Simple Test -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) await expect(promise).rejects.toThrow(`Partials inside of partials is not yet supported`) }) @@ -576,8 +606,8 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -587,15 +617,18 @@ title: Simple Test -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).toContain(`warning prop "src" must start with "_partials/"`) }) @@ -607,8 +640,8 @@ describe('Link Validation and Processing', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -618,15 +651,18 @@ title: Simple Test [Non Existent Page](/docs/non-existent-page) -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).toContain(`warning Guide /docs/non-existent-page not found`) }) @@ -636,8 +672,8 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -647,7 +683,7 @@ title: Simple Test [Core Page](/docs/core-page) -# Simple Test Page` +# Simple Test Page`, }, { path: './docs/core-page.mdx', @@ -655,15 +691,18 @@ title: Simple Test title: Core Page --- -# Core Page` - } +# Core Page`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).not.toContain(`warning Guide /docs/core-page not found`) }) @@ -673,8 +712,8 @@ title: Core Page { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Simple Test", href: "/docs/simple-test" }]] - }) + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), }, { path: './docs/simple-test.mdx', @@ -684,16 +723,18 @@ title: Simple Test [Simple Test](/docs/simple-test#non-existent-hash) -# Simple Test Page` - } +# Simple Test Page`, + }, ]) - - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).toContain(`warning Hash "non-existent-hash" not found in /docs/simple-test`) }) @@ -703,11 +744,13 @@ title: Simple Test { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Simple Test", href: "/docs/simple-test" }, - { title: "Headings", href: "/docs/headings" } - ]] - }) + navigation: [ + [ + { title: 'Simple Test', href: '/docs/simple-test' }, + { title: 'Headings', href: '/docs/headings' }, + ], + ], + }), }, { path: './docs/headings.mdx', @@ -715,7 +758,7 @@ title: Simple Test title: Headings --- -# test {{ id: 'my-heading' }}` +# test {{ id: 'my-heading' }}`, }, { path: './docs/simple-test.mdx', @@ -723,27 +766,34 @@ title: Headings title: Simple Test --- -[Headings](/docs/headings#my-heading)` - } +[Headings](/docs/headings#my-heading)`, + }, ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) }) - test('Swap out links for when a link points to an sdk generated guide', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "SDK Filtered Page", href: "/docs/sdk-filtered-page" }, { title: "Core Page", href: "/docs/core-page" }]] - }) + navigation: [ + [ + { title: 'SDK Filtered Page', href: '/docs/sdk-filtered-page' }, + { title: 'Core Page', href: '/docs/core-page' }, + ], + ], + }), }, { path: './docs/sdk-filtered-page.mdx', @@ -752,7 +802,7 @@ title: SDK Filtered Page sdk: react, nextjs --- -SDK filtered page` +SDK filtered page`, }, { path: './docs/core-page.mdx', @@ -763,54 +813,61 @@ title: Core Page # Core page [SDK Filtered Page](/docs/sdk-filtered-page) -` - } +`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs"] - })) - - expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain(`SDK Filtered Page`) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) + + expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain( + `SDK Filtered Page`, + ) }) }) describe('SDK Filtering', () => { - test('should handle SDK filtering with deeply nested manifest structures', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [ - [{ - title: "Top Level", - items: [ - [{ - title: "Mid Level", - sdk: ["react", "nextjs"], - items: [ - [{ - title: "Deep Level", - sdk: ["nextjs"], - items: [ - [{ title: "Deeply Nested Page", href: "/docs/deeply-nested-nextjs" }] - ] - },{ - title: "Deep Level", - sdk: ["react"], + [ + { + title: 'Top Level', + items: [ + [ + { + title: 'Mid Level', + sdk: ['react', 'nextjs'], items: [ - [{ title: "Deeply Nested Page", href: "/docs/deeply-nested-react" }] - ] - }] - ] - }] - ] - }] - ] - }) + [ + { + title: 'Deep Level', + sdk: ['nextjs'], + items: [[{ title: 'Deeply Nested Page', href: '/docs/deeply-nested-nextjs' }]], + }, + { + title: 'Deep Level', + sdk: ['react'], + items: [[{ title: 'Deeply Nested Page', href: '/docs/deeply-nested-react' }]], + }, + ], + ], + }, + ], + ], + }, + ], + ], + }), }, { path: './docs/deeply-nested-nextjs.mdx', @@ -819,7 +876,7 @@ title: Deeply Nested Page sdk: nextjs --- -Content for Next.js users.` +Content for Next.js users.`, }, { path: './docs/deeply-nested-react.mdx', @@ -828,45 +885,50 @@ title: Deeply Nested Page sdk: react --- -Content for React users.` - } +Content for React users.`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs", "javascript-frontend"] - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs', 'javascript-frontend'], + }), + ) // Page should be available in nextjs (from manifest deep nesting) expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toBe(true) expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-react.mdx'))).toBe(false) - expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toContain("Content for Next.js users.") - expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).not.toContain("Content for React users.") - + expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toContain('Content for Next.js users.') + expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).not.toContain('Content for React users.') + // Page should be available in react (from parent manifest item) expect(await fileExists(pathJoin('./dist/react/deeply-nested-react.mdx'))).toBe(true) expect(await fileExists(pathJoin('./dist/react/deeply-nested-nextjs.mdx'))).toBe(false) - expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).toContain("Content for React users.") - expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).not.toContain("Content for Next.js users.") + expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).toContain('Content for React users.') + expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).not.toContain('Content for Next.js users.') // Page should NOT be available in javascript-frontend (filtered out by manifest) expect(await fileExists(pathJoin('./dist/javascript-frontend/deeply-nested-nextjs.mdx'))).toBe(false) expect(await fileExists(pathJoin('./dist/javascript-frontend/deeply-nested-react.mdx'))).toBe(false) - }); + }) test('should correctly process multiple blocks with different SDKs in a single document', async () => { - const { tempDir, pathJoin }= await createTempFiles([ + const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { - title: "Multiple SDK Blocks", - href: "/multiple-sdk-blocks" - } - ]] - }) + navigation: [ + [ + { + title: 'Multiple SDK Blocks', + href: '/multiple-sdk-blocks', + }, + ], + ], + }), }, { path: './docs/multiple-sdk-blocks.mdx', @@ -889,40 +951,43 @@ sdk: react, nextjs, javascript-frontend This content is for JavaScript Frontend users only. -Common content for all SDKs.` - } - ]); +Common content for all SDKs.`, + }, + ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs", "javascript-frontend"] - })); + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs', 'javascript-frontend'], + }), + ) // Check React version - expect(await fileExists(pathJoin('./dist/react/multiple-sdk-blocks.mdx'))).toBe(true); - const reactContent = await readFile(pathJoin('./dist/react/multiple-sdk-blocks.mdx')); - expect(reactContent).toContain("This content is for React users only."); - expect(reactContent).not.toContain("This content is for Next.js users only."); - expect(reactContent).not.toContain("This content is for JavaScript Frontend users only."); - expect(reactContent).toContain("Common content for all SDKs."); + expect(await fileExists(pathJoin('./dist/react/multiple-sdk-blocks.mdx'))).toBe(true) + const reactContent = await readFile(pathJoin('./dist/react/multiple-sdk-blocks.mdx')) + expect(reactContent).toContain('This content is for React users only.') + expect(reactContent).not.toContain('This content is for Next.js users only.') + expect(reactContent).not.toContain('This content is for JavaScript Frontend users only.') + expect(reactContent).toContain('Common content for all SDKs.') // Check Next.js version - expect(await fileExists(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx'))).toBe(true); - const nextjsContent = await readFile(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx')); - expect(nextjsContent).not.toContain("This content is for React users only."); - expect(nextjsContent).toContain("This content is for Next.js users only."); - expect(nextjsContent).not.toContain("This content is for JavaScript Frontend users only."); - expect(nextjsContent).toContain("Common content for all SDKs."); + expect(await fileExists(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx'))).toBe(true) + const nextjsContent = await readFile(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx')) + expect(nextjsContent).not.toContain('This content is for React users only.') + expect(nextjsContent).toContain('This content is for Next.js users only.') + expect(nextjsContent).not.toContain('This content is for JavaScript Frontend users only.') + expect(nextjsContent).toContain('Common content for all SDKs.') // Check JavaScript Frontend version - expect(await fileExists(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx'))).toBe(true); - const jsContent = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx')); - expect(jsContent).not.toContain("This content is for React users only."); - expect(jsContent).not.toContain("This content is for Next.js users only."); - expect(jsContent).toContain("This content is for JavaScript Frontend users only."); - expect(jsContent).toContain("Common content for all SDKs."); - }); + expect(await fileExists(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx'))).toBe(true) + const jsContent = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx')) + expect(jsContent).not.toContain('This content is for React users only.') + expect(jsContent).not.toContain('This content is for Next.js users only.') + expect(jsContent).toContain('This content is for JavaScript Frontend users only.') + expect(jsContent).toContain('Common content for all SDKs.') + }) test('should handle nested components correctly', async () => { const { tempDir, pathJoin } = await createTempFiles([ @@ -930,15 +995,15 @@ Common content for all SDKs.` path: './docs/manifest.json', content: JSON.stringify({ navigation: [ - [{ - title: "Parent Group", - sdk: ["react", "nextjs"], - items: [ - [{ title: "Nested SDK Page", href: "/docs/nested-sdk-page" }] - ] - }] - ] - }) + [ + { + title: 'Parent Group', + sdk: ['react', 'nextjs'], + items: [[{ title: 'Nested SDK Page', href: '/docs/nested-sdk-page' }]], + }, + ], + ], + }), }, { path: './docs/nested-sdk-page.mdx', @@ -957,40 +1022,44 @@ sdk: react, nextjs -Common content for all SDKs.` - } +Common content for all SDKs.`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs"] - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) // Check React output has only React content const reactOutput = await readFile(pathJoin('./dist/react/nested-sdk-page.mdx')) - expect(reactOutput).toContain("This content is for React users.") - expect(reactOutput).not.toContain("This is nested content specifically for Next.js users") - + expect(reactOutput).toContain('This content is for React users.') + expect(reactOutput).not.toContain('This is nested content specifically for Next.js users') + // Check Next.js output has both React and Next.js content const nextjsOutput = await readFile(pathJoin('./dist/nextjs/nested-sdk-page.mdx')) - expect(nextjsOutput).toContain("This content is for React users.") - expect(nextjsOutput).toContain("This is nested content specifically for Next.js users") - - }); + expect(nextjsOutput).toContain('This content is for React users.') + expect(nextjsOutput).toContain('This is nested content specifically for Next.js users') + }) test('should support components with array syntax for multiple SDKs', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { - title: "Multiple SDK Test", - href: "/docs/multiple-sdk-test" - } - ]] - }) + navigation: [ + [ + { + title: 'Multiple SDK Test', + href: '/docs/multiple-sdk-test', + }, + ], + ], + }), }, { path: './docs/multiple-sdk-test.mdx', @@ -1009,32 +1078,35 @@ sdk: react, nextjs, javascript-frontend This content is for JavaScript Frontend users. -Common content for all SDKs.` - } +Common content for all SDKs.`, + }, ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs", "javascript-frontend"] - })) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs', 'javascript-frontend'], + }), + ) // Check React output has React content but not JavaScript Frontend content const reactOutput = await readFile(pathJoin('./dist/react/multiple-sdk-test.mdx')) - expect(reactOutput).toContain("This content is for React and Next.js users.") - expect(reactOutput).not.toContain("This content is for JavaScript Frontend users.") - + expect(reactOutput).toContain('This content is for React and Next.js users.') + expect(reactOutput).not.toContain('This content is for JavaScript Frontend users.') + // Check Next.js output has Next.js content but not JavaScript Frontend content const nextjsOutput = await readFile(pathJoin('./dist/nextjs/multiple-sdk-test.mdx')) - expect(nextjsOutput).toContain("This content is for React and Next.js users.") - expect(nextjsOutput).not.toContain("This content is for JavaScript Frontend users.") - + expect(nextjsOutput).toContain('This content is for React and Next.js users.') + expect(nextjsOutput).not.toContain('This content is for JavaScript Frontend users.') + // Check JavaScript Frontend output has JavaScript Frontend content but not React/Next.js content const jsOutput = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-test.mdx')) - expect(jsOutput).toContain("This content is for JavaScript Frontend users.") - expect(jsOutput).not.toContain("This content is for React and Next.js users.") - }); -}); + expect(jsOutput).toContain('This content is for JavaScript Frontend users.') + expect(jsOutput).not.toContain('This content is for React and Next.js users.') + }) +}) describe('Manifest Handling', () => { test('should apply manifest options (wrapDefault, collapseDefault, hideTitleDefault) correctly', async () => { @@ -1042,31 +1114,59 @@ describe('Manifest Handling', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Group One", items: [[{ title: "Item One", href: "/docs/item-one" }]], wrap: true, collapse: true, hideTitle: false }, - { title: "Group Two", items: [[{ title: "Item Two", href: "/docs/item-two" }]], wrap: true, collapse: false, hideTitle: true }, - { title: "Group Three", items: [[{ title: "Item Three", href: "/docs/item-three" }]], wrap: false, collapse: true, hideTitle: false }, - { title: "Group Four", items: [[{ title: "Item Four", href: "/docs/item-four" }]], wrap: false, collapse: false, hideTitle: true }, - ]] - }) - }, - { path: "./docs/item-one.mdx", content: `---\ntitle: Item One\n---\nItem One` }, - { path: "./docs/item-two.mdx", content: `---\ntitle: Item Two\n---\nItem Two` }, - { path: "./docs/item-three.mdx", content: `---\ntitle: Item Three\n---\nItem Three` }, - { path: "./docs/item-four.mdx", content: `---\ntitle: Item Four\n---\nItem Four` }, - ]) - - - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["nextjs"], - manifestOptions: { - wrapDefault: false, - collapseDefault: false, - hideTitleDefault: false - } - })) + navigation: [ + [ + { + title: 'Group One', + items: [[{ title: 'Item One', href: '/docs/item-one' }]], + wrap: true, + collapse: true, + hideTitle: false, + }, + { + title: 'Group Two', + items: [[{ title: 'Item Two', href: '/docs/item-two' }]], + wrap: true, + collapse: false, + hideTitle: true, + }, + { + title: 'Group Three', + items: [[{ title: 'Item Three', href: '/docs/item-three' }]], + wrap: false, + collapse: true, + hideTitle: false, + }, + { + title: 'Group Four', + items: [[{ title: 'Item Four', href: '/docs/item-four' }]], + wrap: false, + collapse: false, + hideTitle: true, + }, + ], + ], + }), + }, + { path: './docs/item-one.mdx', content: `---\ntitle: Item One\n---\nItem One` }, + { path: './docs/item-two.mdx', content: `---\ntitle: Item Two\n---\nItem Two` }, + { path: './docs/item-three.mdx', content: `---\ntitle: Item Three\n---\nItem Three` }, + { path: './docs/item-four.mdx', content: `---\ntitle: Item Four\n---\nItem Four` }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs'], + manifestOptions: { + wrapDefault: false, + collapseDefault: false, + hideTitleDefault: false, + }, + }), + ) const manifest = JSON.parse(await readFile(pathJoin('./dist/nextjs/manifest.json'))) const groups = manifest.navigation[0] @@ -1086,106 +1186,113 @@ describe('Manifest Handling', () => { expect(groups[3].wrap).toBe(undefined) expect(groups[3].collapse).toBe(undefined) expect(groups[3].hideTitle).toBe(true) - - }); + }) test('should properly inherit SDK filtering from parent groups to child items', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { - title: "SDK Group", - sdk: ["nextjs", "react"], - items: [[ - { - title: "Sub Group", - items: [[ - { title: "SDK Item", href: "/docs/sdk-item" }, - { title: "Nested Group", items: [[{ title: "Nested Item", href: "/docs/nested-item" }]] } - ]] - } - ]] - }, - { - title: "Generic Group", - items: [[ - { - title: "Sub Group", - items: [[ - { title: "Generic Item", href: "/docs/generic-item" } - ]] - } - ]] - }, - { - title: "Vue Group", - sdk: ["vue"], - items: [[ - { - title: "Sub Group", - items: [[{ title: "Vue Item", href: "/docs/vue-item" }]] - } - ]] - } - ]] - }) - }, - { - path: "./docs/sdk-item.mdx", - content: `---\ntitle: SDK Item\n---\nSDK specific content` - }, - { - path: "./docs/nested-item.mdx", - content: `---\ntitle: Nested Item\n---\nNested SDK specific content` - }, - { - path: "./docs/generic-item.mdx", - content: `---\ntitle: Generic Item\n---\nGeneric content` - }, - { - path: "./docs/vue-item.mdx", - content: `---\ntitle: Vue Item\n---\nVue specific content` - } - ]); + navigation: [ + [ + { + title: 'SDK Group', + sdk: ['nextjs', 'react'], + items: [ + [ + { + title: 'Sub Group', + items: [ + [ + { title: 'SDK Item', href: '/docs/sdk-item' }, + { title: 'Nested Group', items: [[{ title: 'Nested Item', href: '/docs/nested-item' }]] }, + ], + ], + }, + ], + ], + }, + { + title: 'Generic Group', + items: [ + [ + { + title: 'Sub Group', + items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], + }, + ], + ], + }, + { + title: 'Vue Group', + sdk: ['vue'], + items: [ + [ + { + title: 'Sub Group', + items: [[{ title: 'Vue Item', href: '/docs/vue-item' }]], + }, + ], + ], + }, + ], + ], + }), + }, + { + path: './docs/sdk-item.mdx', + content: `---\ntitle: SDK Item\n---\nSDK specific content`, + }, + { + path: './docs/nested-item.mdx', + content: `---\ntitle: Nested Item\n---\nNested SDK specific content`, + }, + { + path: './docs/generic-item.mdx', + content: `---\ntitle: Generic Item\n---\nGeneric content`, + }, + { + path: './docs/vue-item.mdx', + content: `---\ntitle: Vue Item\n---\nVue specific content`, + }, + ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["nextjs", "react", "vue"], - })); + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react', 'vue'], + }), + ) // Check nextjs manifest - const nextjsManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/nextjs/manifest.json'), 'utf-8')); - const nextjsGroups = nextjsManifest.navigation[0]; - - expect(nextjsGroups[0].items[0][0].items[0][0].title).toBe("SDK Item"); - expect(nextjsGroups[0].items[0][0].items[0][1].title).toBe("Nested Group"); - expect(nextjsGroups[1].items[0][0].items[0][0].title).toBe("Generic Item"); - expect(nextjsGroups[2]).toBe(undefined); + const nextjsManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/nextjs/manifest.json'), 'utf-8')) + const nextjsGroups = nextjsManifest.navigation[0] + expect(nextjsGroups[0].items[0][0].items[0][0].title).toBe('SDK Item') + expect(nextjsGroups[0].items[0][0].items[0][1].title).toBe('Nested Group') + expect(nextjsGroups[1].items[0][0].items[0][0].title).toBe('Generic Item') + expect(nextjsGroups[2]).toBe(undefined) // Check react manifest - const reactManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/react/manifest.json'), 'utf-8')); - const reactGroups = reactManifest.navigation[0]; + const reactManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/react/manifest.json'), 'utf-8')) + const reactGroups = reactManifest.navigation[0] - expect(reactGroups[0].items[0][0].items[0][0].title).toBe("SDK Item"); - expect(reactGroups[0].items[0][0].items[0][1].title).toBe("Nested Group"); - expect(reactGroups[1].items[0][0].items[0][0].title).toBe("Generic Item"); - expect(reactGroups[2]).toBe(undefined); - + expect(reactGroups[0].items[0][0].items[0][0].title).toBe('SDK Item') + expect(reactGroups[0].items[0][0].items[0][1].title).toBe('Nested Group') + expect(reactGroups[1].items[0][0].items[0][0].title).toBe('Generic Item') + expect(reactGroups[2]).toBe(undefined) // Check vue manifest - const vueManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/vue/manifest.json'), 'utf-8')); - const vueGroups = vueManifest.navigation[0]; - - expect(vueGroups[0].items[0][0].items[0][0].title).toBe("Generic Item"); - expect(vueGroups[1].items[0][0].items[0][0].title).toBe("Vue Item"); - expect(vueGroups[2]).toBe(undefined); - }); + const vueManifest = JSON.parse(await fs.readFile(path.join(tempDir, './dist/vue/manifest.json'), 'utf-8')) + const vueGroups = vueManifest.navigation[0] -}); + expect(vueGroups[0].items[0][0].items[0][0].title).toBe('Generic Item') + expect(vueGroups[1].items[0][0].items[0][0].title).toBe('Vue Item') + expect(vueGroups[2]).toBe(undefined) + }) +}) describe('Path and File Handling', () => { test('should ignore paths specified in ignorePaths during processing', async () => { @@ -1193,11 +1300,13 @@ describe('Path and File Handling', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Regular Guide", href: "/docs/regular-guide" }, - { title: "Ignored Guide", href: "/docs/ignored/ignored-guide" } - ]] - }) + navigation: [ + [ + { title: 'Regular Guide', href: '/docs/regular-guide' }, + { title: 'Ignored Guide', href: '/docs/ignored/ignored-guide' }, + ], + ], + }), }, { path: './docs/regular-guide.mdx', @@ -1205,7 +1314,7 @@ describe('Path and File Handling', () => { title: Regular Guide --- -# Regular Guide Content` +# Regular Guide Content`, }, { path: './docs/ignored/ignored-guide.mdx', @@ -1213,46 +1322,51 @@ title: Regular Guide title: Ignored Guide --- -# Ignored Guide Content` - } - ]); +# Ignored Guide Content`, + }, + ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"], - ignorePaths: ["/docs/ignored"] - })); + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignorePaths: ['/docs/ignored'], + }), + ) // Check that only the regular guide was processed - const distFiles = (await listFiles()).filter(file => file.startsWith('dist/')) - - expect(distFiles).toContain('dist/regular-guide.mdx'); - expect(distFiles).toContain('dist/react/manifest.json'); - expect(distFiles).not.toContain('dist/ignored/ignored-guide.mdx'); - + const distFiles = (await listFiles()).filter((file) => file.startsWith('dist/')) + + expect(distFiles).toContain('dist/regular-guide.mdx') + expect(distFiles).toContain('dist/react/manifest.json') + expect(distFiles).not.toContain('dist/ignored/ignored-guide.mdx') + // Verify that the manifest was filtered correctly expect(JSON.parse(await readFile(pathJoin('./dist/react/manifest.json')))).toEqual({ - navigation: [[ - { - title: "Regular Guide", - href: "/docs/regular-guide" - }, - { - title: "Ignored Guide", - href: "/docs/ignored/ignored-guide" - } - ]] + navigation: [ + [ + { + title: 'Regular Guide', + href: '/docs/regular-guide', + }, + { + title: 'Ignored Guide', + href: '/docs/ignored/ignored-guide', + }, + ], + ], }) - }); + }) test('should detect file path conflicts when a core doc path matches an SDK path', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "React Guide", href: "/docs/react/conflict" }]] - }) + navigation: [[{ title: 'React Guide', href: '/docs/react/conflict' }]], + }), }, { path: './docs/react/conflict.mdx', @@ -1260,30 +1374,37 @@ title: Ignored Guide title: React Guide --- -# This will cause a conflict because it's in a path that starts with "react"` - } - ]); +# This will cause a conflict because it's in a path that starts with "react"`, + }, + ]) // This should throw an error because the file path starts with an SDK name - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - await expect(promise).rejects.toThrow('Attempting to write out a core doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict'); - }); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Attempting to write out a core doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict', + ) + }) test('should remove .mdx suffix from markdown links', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Source Page", href: "/docs/source-page" }, - { title: "Target Page", href: "/docs/target-page" } - ]] - }) + navigation: [ + [ + { title: 'Source Page', href: '/docs/source-page' }, + { title: 'Target Page', href: '/docs/target-page' }, + ], + ], + }), }, { path: './docs/source-page.mdx', @@ -1294,7 +1415,7 @@ title: Source Page # Source Page [Link to Target with .mdx](/docs/target-page.mdx) -[Link to Target without .mdx](/docs/target-page)` +[Link to Target without .mdx](/docs/target-page)`, }, { path: './docs/target-page.mdx', @@ -1302,35 +1423,37 @@ title: Source Page title: Target Page --- -# Target Page Content` - } - ]); +# Target Page Content`, + }, + ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) // Both links should be processed to remove .mdx - const sourcePageContent = await readFile(pathJoin('./dist/source-page.mdx')); - + const sourcePageContent = await readFile(pathJoin('./dist/source-page.mdx')) + // The link should have .mdx removed - expect(sourcePageContent).toContain('[Link to Target with .mdx](/docs/target-page)'); - expect(sourcePageContent).toContain('[Link to Target without .mdx](/docs/target-page)'); - expect(sourcePageContent).not.toContain('/docs/target-page.mdx'); - }); -}); + expect(sourcePageContent).toContain('[Link to Target with .mdx](/docs/target-page)') + expect(sourcePageContent).toContain('[Link to Target without .mdx](/docs/target-page)') + expect(sourcePageContent).not.toContain('/docs/target-page.mdx') + }) +}) describe('Edge Cases', () => { - test('should report errors for malformed frontmatter', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Malformed Frontmatter", href: "/docs/malformed-frontmatter" }]] - }) + navigation: [[{ title: 'Malformed Frontmatter', href: '/docs/malformed-frontmatter' }]], + }), }, { path: './docs/malformed-frontmatter.mdx', @@ -1339,27 +1462,30 @@ title: Malformed Frontmatter description: \`This frontmatter has an unbalanced quote --- -# Content with malformed frontmatter` - } - ]); +# Content with malformed frontmatter`, + }, + ]) // This should throw a parsing error - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - await expect(promise).rejects.toThrow("Plain value cannot start with reserved character"); - }); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow('Plain value cannot start with reserved character') + }) test('should require and validate mandatory frontmatter fields', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Missing Title", href: "/docs/missing-title" }]] - }) + navigation: [[{ title: 'Missing Title', href: '/docs/missing-title' }]], + }), }, { path: './docs/missing-title.mdx', @@ -1367,45 +1493,51 @@ description: \`This frontmatter has an unbalanced quote description: This frontmatter is missing the required title field --- -# Content with missing title in frontmatter` - } - ]); +# Content with missing title in frontmatter`, + }, + ]) // This should throw an error about missing title - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - await expect(promise).rejects.toThrow('Frontmatter must have a "title" property'); - }); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow('Frontmatter must have a "title" property') + }) test('should fail on special characters in paths', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Space in url", href: "/docs/space in url" }, - ]] - }) + navigation: [[{ title: 'Space in url', href: '/docs/space in url' }]], + }), }, { path: './docs/space in url.mdx', - content: `---\ntitle: Space in url\n---` - } - ]); - - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + content: `---\ntitle: Space in url\n---`, + }, + ]) - await expect(promise).rejects.toThrow('Href "/docs/space in url" contains characters that will be encoded by the browser, please remove them') - }); -}); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Href "/docs/space in url" contains characters that will be encoded by the browser, please remove them', + ) + }) +}) describe('Error Reporting', () => { test('should produce clear and informative error messages for validation failures', async () => { @@ -1413,8 +1545,8 @@ describe('Error Reporting', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "Validation Error", href: "/docs/validation-error" }]] - }) + navigation: [[{ title: 'Validation Error', href: '/docs/validation-error' }]], + }), }, { path: './docs/validation-error.mdx', @@ -1425,30 +1557,35 @@ sdk: react, invalid-sdk # Validation Error Page -This page has an invalid SDK in frontmatter.` - } - ]); +This page has an invalid SDK in frontmatter.`, + }, + ]) // This should throw an error with specific message about invalid SDK - const promise = build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); - - await expect(promise).rejects.toThrow('Invalid SDK ["invalid-sdk"], the valid SDKs are ["react"]'); - }); + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow('Invalid SDK ["invalid-sdk"], the valid SDKs are ["react"]') + }) test('should handle errors when a referenced document exists but is invalid', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Valid Document", href: "/docs/valid-document" }, - { title: "Invalid Reference", href: "/docs/invalid-reference" } - ]] - }) + navigation: [ + [ + { title: 'Valid Document', href: '/docs/valid-document' }, + { title: 'Invalid Reference', href: '/docs/invalid-reference' }, + ], + ], + }), }, { path: './docs/valid-document.mdx', @@ -1458,7 +1595,7 @@ title: Valid Document # Valid Document -[Link to Invalid Reference](/docs/invalid-reference#non-existent-header)` +[Link to Invalid Reference](/docs/invalid-reference#non-existent-header)`, }, { path: './docs/invalid-reference.mdx', @@ -1468,31 +1605,36 @@ title: Invalid Reference # Invalid Reference -This document doesn't have the referenced header.` - } - ]); +This document doesn't have the referenced header.`, + }, + ]) // Should complete with warnings - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) // Should report warning about missing hash - expect(output).toContain('warning Hash "non-existent-header" not found in /docs/invalid-reference'); - }); + expect(output).toContain('warning Hash "non-existent-header" not found in /docs/invalid-reference') + }) test('should complete build workflow when errors are present in some files', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Valid Document", href: "/docs/valid-document" }, - { title: "Document with Warnings", href: "/docs/document-with-warnings" } - ]] - }) + navigation: [ + [ + { title: 'Valid Document', href: '/docs/valid-document' }, + { title: 'Document with Warnings', href: '/docs/document-with-warnings' }, + ], + ], + }), }, { path: './docs/valid-document.mdx', @@ -1502,7 +1644,7 @@ title: Valid Document # Valid Document -This is a completely valid document.` +This is a completely valid document.`, }, { path: './docs/document-with-warnings.mdx', @@ -1516,26 +1658,29 @@ title: Document with Warnings This content has an invalid SDK. -` - } - ]); +`, + }, + ]) // Should complete with warnings - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) // Check that the build completed and valid files were created - expect(await fileExists(pathJoin('./dist/valid-document.mdx'))).toBe(true); - expect(await fileExists(pathJoin('./dist/document-with-warnings.mdx'))).toBe(true); - + expect(await fileExists(pathJoin('./dist/valid-document.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/document-with-warnings.mdx'))).toBe(true) + // Check that warnings were reported - expect(output).toContain('warning Guide /docs/non-existent-document not found'); - expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK'); - }); -}); + expect(output).toContain('warning Guide /docs/non-existent-document not found') + expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK') + }) +}) describe('Advanced Features', () => { test('should correctly handle links with anchors to specific sections of documents', async () => { @@ -1543,11 +1688,13 @@ describe('Advanced Features', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Source Document", href: "/docs/source-document" }, - { title: "Target Document", href: "/docs/target-document" } - ]] - }) + navigation: [ + [ + { title: 'Source Document', href: '/docs/source-document' }, + { title: 'Target Document', href: '/docs/target-document' }, + ], + ], + }), }, { path: './docs/source-document.mdx', @@ -1559,7 +1706,7 @@ title: Source Document [Link to Section 1](/docs/target-document#section-1) [Link to Section 2](/docs/target-document#section-2) -[Link to Invalid Section](/docs/target-document#invalid-section)` +[Link to Invalid Section](/docs/target-document#invalid-section)`, }, { path: './docs/target-document.mdx', @@ -1575,34 +1722,39 @@ Content for section 1. ## Section 2 -Content for section 2.` - } - ]); +Content for section 2.`, + }, + ]) - const output = await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) // Valid links should work without warnings - expect(output).not.toContain('warning Hash "section-1" not found'); - expect(output).not.toContain('warning Hash "section-2" not found'); - + expect(output).not.toContain('warning Hash "section-1" not found') + expect(output).not.toContain('warning Hash "section-2" not found') + // Invalid link should produce a warning - expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document'); - }); + expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document') + }) test('should process target="_blank" links in manifest correctly', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[ - { title: "Normal Link", href: "/docs/normal-link" }, - { title: "External Link", href: "https://example.com", target: "_blank" } - ]] - }) + navigation: [ + [ + { title: 'Normal Link', href: '/docs/normal-link' }, + { title: 'External Link', href: 'https://example.com', target: '_blank' }, + ], + ], + }), }, { path: './docs/normal-link.mdx', @@ -1612,34 +1764,38 @@ title: Normal Link # Normal Link -This is a normal document.` - } - ]); +This is a normal document.`, + }, + ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react"] - })); + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) // Check that the manifest contains the target="_blank" attribute - const reactManifest = JSON.parse(await readFile(pathJoin('./dist/react/manifest.json'))); - expect(reactManifest) - .toEqual({ - navigation: [[ - { title: "Normal Link", href: "/docs/normal-link" }, - { title: "External Link", href: "https://example.com", target: "_blank" } - ]] - }) - }); + const reactManifest = JSON.parse(await readFile(pathJoin('./dist/react/manifest.json'))) + expect(reactManifest).toEqual({ + navigation: [ + [ + { title: 'Normal Link', href: '/docs/normal-link' }, + { title: 'External Link', href: 'https://example.com', target: '_blank' }, + ], + ], + }) + }) test('should generate appropriate landing pages for SDK-specific docs', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: "SDK Document", href: "/docs/sdk-document" }]] - }) + navigation: [[{ title: 'SDK Document', href: '/docs/sdk-document' }]], + }), }, { path: './docs/sdk-document.mdx', @@ -1650,25 +1806,30 @@ sdk: react, nextjs # SDK Document -This document is available for React and Next.js.` - } - ]); +This document is available for React and Next.js.`, + }, + ]) - await build(createBlankStore(), createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ["react", "nextjs"] - })); + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) // Check that SDK-specific versions were created - expect(await fileExists(pathJoin('./dist/react/sdk-document.mdx'))).toBe(true); - expect(await fileExists(pathJoin('./dist/nextjs/sdk-document.mdx'))).toBe(true); - + expect(await fileExists(pathJoin('./dist/react/sdk-document.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/nextjs/sdk-document.mdx'))).toBe(true) + // Check that a landing page was created at the original URL - expect(await fileExists(pathJoin('./dist/sdk-document.mdx'))).toBe(true); - + expect(await fileExists(pathJoin('./dist/sdk-document.mdx'))).toBe(true) + // Verify landing page content - const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')); - expect(landingPage).toBe(''); - }); -}); \ No newline at end of file + const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')) + expect(landingPage).toBe( + '', + ) + }) +}) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 3ea75672d6..ea2ea51a24 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -27,49 +27,103 @@ import { filter as mdastFilter } from 'unist-util-filter' import { map as mdastMap } from 'unist-util-map' import { u as mdastBuilder } from 'unist-builder' import remarkFrontmatter from 'remark-frontmatter' -import yaml from "yaml" +import yaml from 'yaml' import { slugifyWithCounter } from '@sindresorhus/slugify' import { toString } from 'mdast-util-to-string' import reporter from 'vfile-reporter' import readdirp from 'readdirp' -import { z } from "zod" -import { fromError } from 'zod-validation-error'; +import { z } from 'zod' +import { fromError } from 'zod-validation-error' import { Node } from 'unist' import chok from 'chokidar' const VALID_SDKS = [ - "nextjs", - "react", - "javascript-frontend", - "chrome-extension", - "expo", - "ios", - "nodejs", - "expressjs", - "fastify", - "react-router", - "remix", - "tanstack-start", - "go", - "astro", - "nuxt", - "vue", - "ruby", - "python", - "javascript-backend", - "sdk-development", - "community-sdk" + 'nextjs', + 'react', + 'javascript-frontend', + 'chrome-extension', + 'expo', + 'ios', + 'nodejs', + 'expressjs', + 'fastify', + 'react-router', + 'remix', + 'tanstack-start', + 'go', + 'astro', + 'nuxt', + 'vue', + 'ruby', + 'python', + 'javascript-backend', + 'sdk-development', + 'community-sdk', ] as const -type SDK = typeof VALID_SDKS[number] +type SDK = (typeof VALID_SDKS)[number] const sdk = z.enum(VALID_SDKS) -const icon = z.enum(["apple", "application-2", "arrow-up-circle", "astro", "angular", "block", "bolt", "book", "box", "c-sharp", "chart", "checkmark-circle", "chrome", "clerk", "code-bracket", "cog-6-teeth", "door", "elysia", "expressjs", "globe", "go", "home", "hono", "javascript", "koa", "link", "linkedin", "lock", "nextjs", "nodejs", "plug", "plus-circle", "python", "react", "redwood", "remix", "react-router", "rocket", "route", "ruby", "rust", "speedometer", "stacked-rectangle", "solid", "svelte", "tanstack", "user-circle", "user-dotted-circle", "vue", "x", "expo", "nuxt", "fastify"]) +const icon = z.enum([ + 'apple', + 'application-2', + 'arrow-up-circle', + 'astro', + 'angular', + 'block', + 'bolt', + 'book', + 'box', + 'c-sharp', + 'chart', + 'checkmark-circle', + 'chrome', + 'clerk', + 'code-bracket', + 'cog-6-teeth', + 'door', + 'elysia', + 'expressjs', + 'globe', + 'go', + 'home', + 'hono', + 'javascript', + 'koa', + 'link', + 'linkedin', + 'lock', + 'nextjs', + 'nodejs', + 'plug', + 'plus-circle', + 'python', + 'react', + 'redwood', + 'remix', + 'react-router', + 'rocket', + 'route', + 'ruby', + 'rust', + 'speedometer', + 'stacked-rectangle', + 'solid', + 'svelte', + 'tanstack', + 'user-circle', + 'user-dotted-circle', + 'vue', + 'x', + 'expo', + 'nuxt', + 'fastify', +]) type Icon = z.infer -const tag = z.enum(["(Beta)", "(Community)"]) +const tag = z.enum(['(Beta)', '(Community)']) type Tag = z.infer @@ -98,56 +152,57 @@ type Manifest = (ManifestItem | ManifestGroup)[][] // Create manifest schema based on config const createManifestSchema = (config: BuildConfig) => { - const manifestItem: z.ZodType = z.object({ - title: z.string(), - href: z.string(), - tag: tag.optional(), - wrap: z.boolean().default(config.manifestOptions.wrapDefault), - icon: icon.optional(), - target: z.enum(["_blank"]).optional(), - sdk: z.array(sdk).optional() - }).strict() - - const manifestGroup: z.ZodType = z.object({ - title: z.string(), - items: z.lazy(() => manifestSchema), - collapse: z.boolean().default(config.manifestOptions.collapseDefault), - tag: tag.optional(), - wrap: z.boolean().default(config.manifestOptions.wrapDefault), - icon: icon.optional(), - hideTitle: z.boolean().default(config.manifestOptions.hideTitleDefault), - sdk: z.array(sdk).optional() - }).strict() - - const manifestSchema: z.ZodType = z.array( - z.array( - z.union([ - manifestItem, - manifestGroup - ]) - ) - ) + const manifestItem: z.ZodType = z + .object({ + title: z.string(), + href: z.string(), + tag: tag.optional(), + wrap: z.boolean().default(config.manifestOptions.wrapDefault), + icon: icon.optional(), + target: z.enum(['_blank']).optional(), + sdk: z.array(sdk).optional(), + }) + .strict() + + const manifestGroup: z.ZodType = z + .object({ + title: z.string(), + items: z.lazy(() => manifestSchema), + collapse: z.boolean().default(config.manifestOptions.collapseDefault), + tag: tag.optional(), + wrap: z.boolean().default(config.manifestOptions.wrapDefault), + icon: icon.optional(), + hideTitle: z.boolean().default(config.manifestOptions.hideTitleDefault), + sdk: z.array(sdk).optional(), + }) + .strict() + + const manifestSchema: z.ZodType = z.array(z.array(z.union([manifestItem, manifestGroup]))) return { manifestItem, manifestGroup, - manifestSchema + manifestSchema, } } -const pleaseReport = "(this is a bug with the build script, please report)" +const pleaseReport = '(this is a bug with the build script, please report)' -const isValidSdk = (config: BuildConfig) => (sdk: string): sdk is SDK => { - return config.validSdks.includes(sdk as SDK) -} +const isValidSdk = + (config: BuildConfig) => + (sdk: string): sdk is SDK => { + return config.validSdks.includes(sdk as SDK) + } -const isValidSdks = (config: BuildConfig) => (sdks: string[]): sdks is SDK[] => { - return sdks.every(isValidSdk(config)) -} +const isValidSdks = + (config: BuildConfig) => + (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk(config)) + } const readManifest = (config: BuildConfig) => async (): Promise => { const { manifestSchema } = createManifestSchema(config) - const unsafe_manifest = await fs.readFile(config.manifestFilePath, { "encoding": "utf-8" }) + const unsafe_manifest = await fs.readFile(config.manifestFilePath, { encoding: 'utf-8' }) const manifest = await manifestSchema.safeParseAsync(JSON.parse(unsafe_manifest).navigation) @@ -162,7 +217,7 @@ const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { const filePath = path.join(config.basePath, docPath) try { - const fileContent = await fs.readFile(filePath, { "encoding": "utf-8" }) + const fileContent = await fs.readFile(filePath, { encoding: 'utf-8' }) return [null, fileContent] as const } catch (error) { return [new Error(`file ${filePath} doesn't exist`, { cause: error }), null] as const @@ -172,8 +227,9 @@ const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { const readDocsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.docsPath, { type: 'files', - fileFilter: (entry) => config.ignorePaths.some((ignoreItem) => - `/docs/${entry.path}`.startsWith(ignoreItem)) === false && entry.path.endsWith('.mdx') + fileFilter: (entry) => + config.ignorePaths.some((ignoreItem) => `/docs/${entry.path}`.startsWith(ignoreItem)) === false && + entry.path.endsWith('.mdx'), }) } @@ -185,48 +241,49 @@ const readPartialsFolder = (config: BuildConfig) => async () => { } const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => { - const readFile = readMarkdownFile(config); + const readFile = readMarkdownFile(config) - return Promise.all(paths.map(async (markdownPath) => { - const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, markdownPath) + return Promise.all( + paths.map(async (markdownPath) => { + const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, markdownPath) - const [error, content] = await readFile(fullPath) + const [error, content] = await readFile(fullPath) - if (error) { - throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) - } + if (error) { + throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) + } - return { - path: markdownPath, - content, - } - })) + return { + path: markdownPath, + content, + } + }), + ) } -const markdownProcessor = remark() - .use(remarkFrontmatter) - .use(remarkMdx) - .freeze() +const markdownProcessor = remark().use(remarkFrontmatter).use(remarkMdx).freeze() type VFile = Awaited> -const ensureDirectory = (config: BuildConfig) => async (dirPath: string): Promise => { - try { - await fs.access(dirPath) - } catch { - await fs.mkdir(dirPath, { recursive: true }) +const ensureDirectory = + (config: BuildConfig) => + async (dirPath: string): Promise => { + try { + await fs.access(dirPath) + } catch { + await fs.mkdir(dirPath, { recursive: true }) + } } -} const writeDistFile = (config: BuildConfig) => async (filePath: string, contents: string) => { - const ensureDir = ensureDirectory(config); + const ensureDir = ensureDirectory(config) const fullPath = path.join(config.distPath, filePath) await ensureDir(path.dirname(fullPath)) - await fs.writeFile(fullPath, contents, { "encoding": "utf-8" }) + await fs.writeFile(fullPath, contents, { encoding: 'utf-8' }) } const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, contents: string) => { - const writeFile = writeDistFile(config); + const writeFile = writeDistFile(config) await writeFile(path.join(sdk, filePath), contents) } @@ -237,80 +294,87 @@ const removeMdxSuffix = (filePath: string) => { return filePath } -type BlankTree }> = Array>; +type BlankTree }> = Array> const traverseTree = async < Tree extends { items: BlankTree }, - InItem extends Extract, - InGroup extends Extract }>, + InItem extends Extract, + InGroup extends Extract }>, OutItem extends { href: string }, OutGroup extends { items: BlankTree }, - OutTree extends BlankTree + OutTree extends BlankTree, >( tree: Tree, itemCallback: (item: InItem, tree: Tree) => Promise = async (item) => item, groupCallback: (group: InGroup, tree: Tree) => Promise = async (group) => group, errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, ): Promise => { - const result = await Promise.all(tree.items.map(async (group) => { - return await Promise.all(group.map(async (item) => { - try { - if ('href' in item) { - return await itemCallback(item, tree); - } + const result = await Promise.all( + tree.items.map(async (group) => { + return await Promise.all( + group.map(async (item) => { + try { + if ('href' in item) { + return await itemCallback(item, tree) + } - if ('items' in item && Array.isArray(item.items)) { - const newGroup = await groupCallback(item, tree); + if ('items' in item && Array.isArray(item.items)) { + const newGroup = await groupCallback(item, tree) - if (newGroup === null) return null; + if (newGroup === null) return null - // @ts-expect-error - OutGroup should always contain "items" property, so this is safe - const newItems = (await traverseTree(newGroup, itemCallback, groupCallback, errorCallback)).map(group => group.filter((item): item is NonNullable => item !== null)) + // @ts-expect-error - OutGroup should always contain "items" property, so this is safe + const newItems = (await traverseTree(newGroup, itemCallback, groupCallback, errorCallback)).map((group) => + group.filter((item): item is NonNullable => item !== null), + ) - return { - ...newGroup, - items: newItems - } - } + return { + ...newGroup, + items: newItems, + } + } - return item as OutItem; - } catch (error) { - if (error instanceof Error && errorCallback !== undefined) { - errorCallback(item, error); - } else { - throw error - } - } - })); - })); + return item as OutItem + } catch (error) { + if (error instanceof Error && errorCallback !== undefined) { + errorCallback(item, error) + } else { + throw error + } + } + }), + ) + }), + ) - return result.map(group => group.filter((item): item is NonNullable => item !== null)) as unknown as OutTree; -}; + return result.map((group) => + group.filter((item): item is NonNullable => item !== null), + ) as unknown as OutTree +} function flattenTree< Tree extends BlankTree, InItem extends Extract, - InGroup extends Extract }> + InGroup extends Extract }>, >(tree: Tree): InItem[] { - const result: InItem[] = []; + const result: InItem[] = [] for (const group of tree) { for (const itemOrGroup of group) { - if ("href" in itemOrGroup) { + if ('href' in itemOrGroup) { // It's an item - result.push(itemOrGroup); - } else if ("items" in itemOrGroup && Array.isArray(itemOrGroup.items)) { + result.push(itemOrGroup) + } else if ('items' in itemOrGroup && Array.isArray(itemOrGroup.items)) { // It's a group with its own sub-tree, flatten it - result.push(...flattenTree(itemOrGroup.items)); + result.push(...flattenTree(itemOrGroup.items)) } } } - return result; + return result } const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { - // This is external so can't change it if (href.startsWith('/docs') === false) return href @@ -332,72 +396,53 @@ const extractComponentPropValueFromNode = ( componentName: string, propName: string, ): string | undefined => { - // Check if it's an MDX component - if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement") { - return undefined; + if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') { + return undefined } // Check if it's the correct component - if (!("name" in node)) return undefined; - if (node.name !== componentName) return undefined; + if (!('name' in node)) return undefined + if (node.name !== componentName) return undefined // Check for attributes - if (!("attributes" in node)) { - vfile?.message( - `<${componentName} /> component has no props`, - node.position - ); - return undefined; + if (!('attributes' in node)) { + vfile?.message(`<${componentName} /> component has no props`, node.position) + return undefined } if (!Array.isArray(node.attributes)) { - vfile?.message( - `<${componentName} /> node attributes is not an array ${pleaseReport}`, - node.position - ); - return undefined; + vfile?.message(`<${componentName} /> node attributes is not an array ${pleaseReport}`, node.position) + return undefined } // Find the requested prop - const propAttribute = node.attributes.find( - (attribute) => attribute.name === propName - ); + const propAttribute = node.attributes.find((attribute) => attribute.name === propName) if (propAttribute === undefined) { - vfile?.message( - `<${componentName} /> component has no "${propName}" attribute`, - node.position - ); - return undefined; + vfile?.message(`<${componentName} /> component has no "${propName}" attribute`, node.position) + return undefined } - const value = propAttribute.value; + const value = propAttribute.value if (value === undefined) { - vfile?.message( - `<${componentName} /> attribute "${propName}" has no value ${pleaseReport}`, - node.position - ); - return undefined; + vfile?.message(`<${componentName} /> attribute "${propName}" has no value ${pleaseReport}`, node.position) + return undefined } // Handle both string values and object values (like JSX expressions) - if (typeof value === "string") { - return value; - } else if (typeof value === "object" && "value" in value) { - return value.value; + if (typeof value === 'string') { + return value + } else if (typeof value === 'object' && 'value' in value) { + return value.value } - vfile?.message( - `<${componentName} /> attribute "${propName}" has an unsupported value type`, - node.position - ); - return undefined; + vfile?.message(`<${componentName} /> attribute "${propName}" has an unsupported value type`, node.position) + return undefined } const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile | undefined, sdkProp: string) => { - const isValidItem = isValidSdk(config) const isValidItems = isValidSdks(config) @@ -406,7 +451,7 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile if (isValidItems(sdks)) { return sdks } else { - const invalidSDKs = sdks.filter(sdk => !isValidItem(sdk)) + const invalidSDKs = sdks.filter((sdk) => !isValidItem(sdk)) vfile?.message(`sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, node.position) } } else { @@ -418,81 +463,83 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile } } -const parseInMarkdownFile = (config: BuildConfig) => async ( - href: string, - partials: { path: string; content: string; }[], - inManifest: boolean, -) => { - const readFile = readMarkdownFile(config); - const [error, fileContent] = await readFile(`${href}.mdx`) - - if (error !== null) { - throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { cause: error }) - } +const parseInMarkdownFile = + (config: BuildConfig) => async (href: string, partials: { path: string; content: string }[], inManifest: boolean) => { + const readFile = readMarkdownFile(config) + const [error, fileContent] = await readFile(`${href}.mdx`) - type Frontmatter = { - title: string; - description?: string; - sdk?: SDK[] - } + if (error !== null) { + throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { + cause: error, + }) + } - let frontmatter: Frontmatter | undefined = undefined + type Frontmatter = { + title: string + description?: string + sdk?: SDK[] + } - const slugify = slugifyWithCounter() - const headingsHashs: Array = [] + let frontmatter: Frontmatter | undefined = undefined - const vfile = await markdownProcessor() - .use(() => (tree, vfile) => { - if (inManifest === false) { - vfile.message("This guide is not in the manifest.json, but will still be publicly accessible and other guides can link to it") - } + const slugify = slugifyWithCounter() + const headingsHashs: Array = [] - if (href !== encodeURI(href)) { - vfile.fail(`Href "${href}" contains characters that will be encoded by the browser, please remove them`) - } - }) - .use(() => (tree, vfile) => { - mdastVisit(tree, - node => node.type === 'yaml' && "value" in node, - node => { - if (!("value" in node)) return; - if (typeof node.value !== "string") return; - - const frontmatterYaml: Record<"title" | "description" | "sdk", string | undefined> = yaml.parse(node.value) + const vfile = await markdownProcessor() + .use(() => (tree, vfile) => { + if (inManifest === false) { + vfile.message( + 'This guide is not in the manifest.json, but will still be publicly accessible and other guides can link to it', + ) + } - const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') + if (href !== encodeURI(href)) { + vfile.fail(`Href "${href}" contains characters that will be encoded by the browser, please remove them`) + } + }) + .use(() => (tree, vfile) => { + mdastVisit( + tree, + (node) => node.type === 'yaml' && 'value' in node, + (node) => { + if (!('value' in node)) return + if (typeof node.value !== 'string') return - if (frontmatterSDKs !== undefined && isValidSdks(config)(frontmatterSDKs) === false) { - const invalidSDKs = frontmatterSDKs.filter(sdk => isValidSdk(config)(sdk) === false) - vfile.fail(`Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(config.validSdks)}`, node.position) - return; - } + const frontmatterYaml: Record<'title' | 'description' | 'sdk', string | undefined> = yaml.parse(node.value) - if (frontmatterYaml.title === undefined) { - vfile.fail(`Frontmatter must have a "title" property`, node.position) - return; - } + const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') - frontmatter = { - title: frontmatterYaml.title, - description: frontmatterYaml.description, - sdk: frontmatterSDKs - } - } - ) + if (frontmatterSDKs !== undefined && isValidSdks(config)(frontmatterSDKs) === false) { + const invalidSDKs = frontmatterSDKs.filter((sdk) => isValidSdk(config)(sdk) === false) + vfile.fail( + `Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(config.validSdks)}`, + node.position, + ) + return + } - if (frontmatter === undefined) { - vfile.fail(`Frontmatter parsing failed for ${href}`) - return; - } + if (frontmatterYaml.title === undefined) { + vfile.fail(`Frontmatter must have a "title" property`, node.position) + return + } - }) - // Validate and embed the - .use(() => (tree, vfile) => { - return mdastMap(tree, - node => { + frontmatter = { + title: frontmatterYaml.title, + description: frontmatterYaml.description, + sdk: frontmatterSDKs, + } + }, + ) - const partialSrc = extractComponentPropValueFromNode(node, vfile, "Include", "src") + if (frontmatter === undefined) { + vfile.fail(`Frontmatter parsing failed for ${href}`) + return + } + }) + // Validate and embed the + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + const partialSrc = extractComponentPropValueFromNode(node, vfile, 'Include', 'src') if (partialSrc === undefined) { return node @@ -503,7 +550,9 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( return node } - const partial = partials.find((partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`) + const partial = partials.find( + (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, + ) if (partial === undefined) { vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) @@ -514,23 +563,27 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( const partialContentVFile = markdownProcessor() .use(() => (tree, vfile) => { - mdastVisit(tree, - node => (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && "name" in node && node.name === "Include", + mdastVisit( + tree, + (node) => + (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && + 'name' in node && + node.name === 'Include', () => { vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) - } + }, ) partialNode = tree }) .processSync({ path: partial.path, - value: partial.content + value: partial.content, }) const partialContentReport = reporter([partialContentVFile], { quiet: true }) - if (partialContentReport !== "") { + if (partialContentReport !== '') { console.error(partialContentReport) } @@ -540,65 +593,62 @@ const parseInMarkdownFile = (config: BuildConfig) => async ( } return Object.assign(node, partialNode) + }) + }) + // extract out the headings to check hashes in links + .use(() => (tree) => { + mdastVisit( + tree, + (node) => node.type === 'heading', + (node) => { + // @ts-expect-error - If the heading has a id in it, this will pick it up + // eg # test {{ id: 'my-heading' }} + // This is for remapping the hash to the custom id + const id = node?.children?.[1]?.data?.estree?.body?.[0]?.expression?.properties?.[0]?.value?.value as + | string + | undefined + + if (id !== undefined) { + headingsHashs.push(id) + } else { + const slug = slugify(toString(node).trim()) + headingsHashs.push(slug) + } + }, + ) + }) + .process({ + path: `${href}.mdx`, + value: fileContent, + }) - } - ) - }) - // extract out the headings to check hashes in links - .use(() => (tree) => { - mdastVisit(tree, - node => node.type === "heading", - node => { - - // @ts-expect-error - If the heading has a id in it, this will pick it up - // eg # test {{ id: 'my-heading' }} - // This is for remapping the hash to the custom id - const id = node?.children?.[1]?.data?.estree?.body?.[0]?.expression?.properties?.[0]?.value?.value as string | undefined - - if (id !== undefined) { - headingsHashs.push(id) - } else { - const slug = slugify(toString(node).trim()) - headingsHashs.push(slug) - } - } - ) - }) - .process({ - path: `${href}.mdx`, - value: fileContent - }) - - if (frontmatter === undefined) { - throw new Error(`Frontmatter parsing failed for ${href}`) - } + if (frontmatter === undefined) { + throw new Error(`Frontmatter parsing failed for ${href}`) + } - return { - href, - sdk: (frontmatter as Frontmatter).sdk, - vfile, - headingsHashs, - frontmatter: frontmatter as Frontmatter + return { + href, + sdk: (frontmatter as Frontmatter).sdk, + vfile, + headingsHashs, + frontmatter: frontmatter as Frontmatter, + } } -} export const createBlankStore = () => ({ - markdownFiles: new Map>>>() + markdownFiles: new Map>>>(), }) -export const build = async ( - store: ReturnType, - config: BuildConfig -) => { +export const build = async (store: ReturnType, config: BuildConfig) => { // Apply currying to create functions pre-configured with config - const ensureDir = ensureDirectory(config); - const getManifest = readManifest(config); - const getDocsFolder = readDocsFolder(config); - const getPartialsFolder = readPartialsFolder(config); - const getPartialsMarkdown = readPartialsMarkdown(config); - const parseMarkdownFile = parseInMarkdownFile(config); - const writeFile = writeDistFile(config); - const writeSdkFile = writeSDKFile(config); + const ensureDir = ensureDirectory(config) + const getManifest = readManifest(config) + const getDocsFolder = readDocsFolder(config) + const getPartialsFolder = readPartialsFolder(config) + const getPartialsMarkdown = readPartialsMarkdown(config) + const parseMarkdownFile = parseInMarkdownFile(config) + const writeFile = writeDistFile(config) + const writeSdkFile = writeSDKFile(config) await ensureDir(config.distPath) @@ -608,62 +658,62 @@ export const build = async ( const docsFiles = await getDocsFolder() console.info('✔️ Read Docs Folder') - const partials = await getPartialsMarkdown( - (await getPartialsFolder()).map(item => item.path) - ) + const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) console.info('✔️ Read Partials') const guides = new Map>>() const guidesInManifest = new Set() // Grab all the docs links in the manifest - await traverseTree({ items: userManifest }, - async (item) => { - if (!item.href?.startsWith('/docs/')) return item - if (item.target !== undefined) return item + await traverseTree({ items: userManifest }, async (item) => { + if (!item.href?.startsWith('/docs/')) return item + if (item.target !== undefined) return item - const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) - if (ignore === true) return item + const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) + if (ignore === true) return item - guidesInManifest.add(item.href) + guidesInManifest.add(item.href) - return item - } - ) + return item + }) console.info('✔️ Parsed in Manifest') // Read in all the guides - const docs = (await Promise.all(docsFiles.map(async (file) => { - const href = removeMdxSuffix(`/docs/${file.path}`) + const docs = ( + await Promise.all( + docsFiles.map(async (file) => { + const href = removeMdxSuffix(`/docs/${file.path}`) - const alreadyLoaded = guides.get(href) + const alreadyLoaded = guides.get(href) - if (alreadyLoaded) return null // already processed + if (alreadyLoaded) return null // already processed - const inManifest = guidesInManifest.has(href) + const inManifest = guidesInManifest.has(href) - let markdownFile: Awaited>; + let markdownFile: Awaited> - const cachedMarkdownFile = store.markdownFiles.get(href) + const cachedMarkdownFile = store.markdownFiles.get(href) - if (cachedMarkdownFile) { - markdownFile = structuredClone(cachedMarkdownFile) - } else { - markdownFile = await parseMarkdownFile(href, partials, inManifest) + if (cachedMarkdownFile) { + markdownFile = structuredClone(cachedMarkdownFile) + } else { + markdownFile = await parseMarkdownFile(href, partials, inManifest) - store.markdownFiles.set(href, structuredClone(markdownFile)) - } + store.markdownFiles.set(href, structuredClone(markdownFile)) + } - guides.set(href, markdownFile) + guides.set(href, markdownFile) - return markdownFile - }))).filter((item): item is NonNullable => item !== null) + return markdownFile + }), + ) + ).filter((item): item is NonNullable => item !== null) console.info(`✔️ Loaded in ${docs.length} guides`) // Goes through and grabs the sdk scoping out of the manifest - const sdkScopedManifest = await traverseTree({ items: userManifest, sdk: undefined as undefined | SDK[] }, + const sdkScopedManifest = await traverseTree( + { items: userManifest, sdk: undefined as undefined | SDK[] }, async (item, tree) => { - if (!item.href?.startsWith('/docs/')) return item if (item.target !== undefined) return item @@ -679,25 +729,30 @@ export const build = async ( const sdk = guide.sdk ?? tree.sdk if (guide.sdk !== undefined && tree.sdk !== undefined) { - if (guide.sdk.every(sdk => tree.sdk?.includes(sdk)) === false) { - throw new Error(`Guide "${item.title}" is attempting to use ${JSON.stringify(guide.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`) + if (guide.sdk.every((sdk) => tree.sdk?.includes(sdk)) === false) { + throw new Error( + `Guide "${item.title}" is attempting to use ${JSON.stringify(guide.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`, + ) } } return { ...item, - sdk + sdk, } }, async (group, tree) => { - - const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter((sdk): sdk is SDK => sdk !== undefined) + const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter( + (sdk): sdk is SDK => sdk !== undefined, + ) const { items, ...details } = group if (details.sdk !== undefined && tree.sdk !== undefined) { - if (details.sdk.every(sdk => tree.sdk?.includes(sdk)) === false) { - throw new Error(`Group "${details.title}" is attempting to use ${JSON.stringify(details.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`) + if (details.sdk.every((sdk) => tree.sdk?.includes(sdk)) === false) { + throw new Error( + `Group "${details.title}" is attempting to use ${JSON.stringify(details.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`, + ) } } @@ -705,14 +760,14 @@ export const build = async ( return { ...details, - sdk: Array.from(new Set([...details.sdk ?? [], ...itemsSDKs])) ?? [], - items + sdk: Array.from(new Set([...(details.sdk ?? []), ...itemsSDKs])) ?? [], + items, } as ManifestGroup }, (item, error) => { console.error('↳', item.title) throw error - } + }, ) console.info('✔️ Applied manifest sdk scoping') @@ -721,31 +776,30 @@ export const build = async ( // It would definitely be preferable we didn't need to do this markdown processing twice // But because we need a full list / hashmap of all the existing docs, we can't // Unless maybe we do some kind of lazy loading of the docs, but this would add complexity - const coreVFiles = await Promise.all(docs.map(async (doc) => { - const vfile = await markdownProcessor() - // Validate links between guides are valid - .use(() => (tree: Node, vfile: VFile) => { - return mdastMap(tree, - node => { - - if (node.type !== "link") return node - if (!("url" in node)) return node - if (typeof node.url !== "string") return node - if (!node.url.startsWith("/docs/")) return node - if (!("children" in node)) return node + const coreVFiles = await Promise.all( + docs.map(async (doc) => { + const vfile = await markdownProcessor() + // Validate links between guides are valid + .use(() => (tree: Node, vfile: VFile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node node.url = removeMdxSuffix(node.url) - const [url, hash] = (node.url as string).split("#") + const [url, hash] = (node.url as string).split('#') const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return node; + if (ignore === true) return node const guide = guides.get(url) if (guide === undefined) { vfile.message(`Guide ${url} not found`, node.position) - return node; + return node } if (hash !== undefined) { @@ -764,30 +818,27 @@ export const build = async ( attributes: [ mdastBuilder('mdxJsxAttribute', { name: 'href', - value: scopeHrefToSDK(url, ':sdk:') + value: scopeHrefToSDK(url, ':sdk:'), }), mdastBuilder('mdxJsxAttribute', { name: 'sdks', value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(guide.sdk) - }) - }) - ] + value: JSON.stringify(guide.sdk), + }), + }), + ], }) } - return node; - } - ) - }) - // Validate the components - .use(() => (tree, vfile) => { - - mdastVisit(tree, - (node) => { - const sdk = extractComponentPropValueFromNode(node, vfile, "If", "sdk") + return node + }) + }) + // Validate the components + .use(() => (tree, vfile) => { + mdastVisit(tree, (node) => { + const sdk = extractComponentPropValueFromNode(node, vfile, 'If', 'sdk') - if (sdk === undefined) return; + if (sdk === undefined) return const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk) @@ -798,229 +849,233 @@ export const build = async ( const availableSDKs = manifestItems.flatMap((item) => item.sdk).filter(Boolean) // The doc doesn't exist in the manifest so we are skipping it - if (manifestItems.length === 0) return; + if (manifestItems.length === 0) return - sdksFilter.forEach(sdk => { - (() => { - if (doc.sdk === undefined) return; + sdksFilter.forEach((sdk) => { + ;(() => { + if (doc.sdk === undefined) return const available = doc.sdk.includes(sdk) if (available === false) { - vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${doc.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position) + vfile.fail( + ` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${doc.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, + node.position, + ) } - })(); + })() - (() => { + ;(() => { // The doc is generic so we are skipping it - if (availableSDKs.length === 0) return; + if (availableSDKs.length === 0) return const available = availableSDKs.includes(sdk) if (available === false) { - vfile.fail(` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, node.position) + vfile.fail( + ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, + node.position, + ) } - })(); + })() }) - } - ) - }) - .process(doc.vfile) + }) + }) + .process(doc.vfile) - const distFilePath = `${doc.href.replace("/docs/", "")}.mdx` + const distFilePath = `${doc.href.replace('/docs/', '')}.mdx` - if (isValidSdk(config)(distFilePath.split('/')[0])) { - throw new Error(`Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`) - } + if (isValidSdk(config)(distFilePath.split('/')[0])) { + throw new Error( + `Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`, + ) + } - if (doc.sdk !== undefined) { - // This is a sdk specific guide, so we want to put a landing page here to redirect the user to a guide customised to their sdk. + if (doc.sdk !== undefined) { + // This is a sdk specific guide, so we want to put a landing page here to redirect the user to a guide customised to their sdk. - await writeFile( - distFilePath, - // It's possible we will want to / need to put some frontmatter here - `` - ) + await writeFile( + distFilePath, + // It's possible we will want to / need to put some frontmatter here + ``, + ) - return vfile - } + return vfile + } - await writeFile(distFilePath, String(vfile)) + await writeFile(distFilePath, String(vfile)) - return vfile - })) + return vfile + }), + ) console.info(`✔️ Wrote out ${docs.length} core docs`) - const sdkSpecificVFiles = await Promise.all(config.validSdks.map(async (targetSdk) => { - - // Goes through and removes any items that are not scoped to the target sdk - const navigation = await traverseTree({ items: sdkScopedManifest }, - async ({ sdk, ...item }) => { - - // This means its generic, not scoped to a specific sdk, so we keep it - if (sdk === undefined) return { - title: item.title, - href: item.href, - tag: item.tag, - wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, - icon: item.icon, - target: item.target - } as const - - // This item is not scoped to the target sdk, so we remove it - if (sdk.includes(targetSdk) === false) return null - - // This is a scoped item and its scoped to our target sdk - return { - title: item.title, - href: scopeHrefToSDK(item.href, targetSdk), - tag: item.tag, - wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, - icon: item.icon, - target: item.target - } as const - }, - // @ts-expect-error - This traverseTree function might just be the death of me - async ({ sdk, ...group }) => { - if (sdk === undefined) return { - title: group.title, - collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, - tag: group.tag, - wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, - icon: group.icon, - hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, - items: group.items, - } - - if (sdk.includes(targetSdk) === false) return null + const sdkSpecificVFiles = await Promise.all( + config.validSdks.map(async (targetSdk) => { + // Goes through and removes any items that are not scoped to the target sdk + const navigation = await traverseTree( + { items: sdkScopedManifest }, + async ({ sdk, ...item }) => { + // This means its generic, not scoped to a specific sdk, so we keep it + if (sdk === undefined) + return { + title: item.title, + href: item.href, + tag: item.tag, + wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, + icon: item.icon, + target: item.target, + } as const + + // This item is not scoped to the target sdk, so we remove it + if (sdk.includes(targetSdk) === false) return null + + // This is a scoped item and its scoped to our target sdk + return { + title: item.title, + href: scopeHrefToSDK(item.href, targetSdk), + tag: item.tag, + wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, + icon: item.icon, + target: item.target, + } as const + }, + // @ts-expect-error - This traverseTree function might just be the death of me + async ({ sdk, ...group }) => { + if (sdk === undefined) + return { + title: group.title, + collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, + tag: group.tag, + wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, + icon: group.icon, + hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, + items: group.items, + } - return { - title: group.title, - collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, - tag: group.tag, - wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, - icon: group.icon, - hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, - items: group.items, - } - } - ) + if (sdk.includes(targetSdk) === false) return null - const vFiles = await Promise.all(docs.map(async (doc) => { - if (doc.sdk === undefined) return null; // skip core docs - if (doc.sdk.includes(targetSdk) === false) return null; // skip docs that are not for the target sdk + return { + title: group.title, + collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, + tag: group.tag, + wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, + icon: group.icon, + hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, + items: group.items, + } + }, + ) - const vfile = await markdownProcessor() - // filter out content that is only available to other sdk's - .use(() => (tree, vfile) => { - return mdastFilter(tree, - node => { + const vFiles = await Promise.all( + docs.map(async (doc) => { + if (doc.sdk === undefined) return null // skip core docs + if (doc.sdk.includes(targetSdk) === false) return null // skip docs that are not for the target sdk - // We aren't passing the vfile here as the as the warning - // should have already been reported above when we initially - // parsed the file + const vfile = await markdownProcessor() + // filter out content that is only available to other sdk's + .use(() => (tree, vfile) => { + return mdastFilter(tree, (node) => { + // We aren't passing the vfile here as the as the warning + // should have already been reported above when we initially + // parsed the file - const sdk = extractComponentPropValueFromNode(node, undefined, "If", "sdk") + const sdk = extractComponentPropValueFromNode(node, undefined, 'If', 'sdk') - if (sdk === undefined) return true + if (sdk === undefined) return true - const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk) + const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk) - if (sdksFilter === undefined) return true + if (sdksFilter === undefined) return true - if (sdksFilter.includes(targetSdk)) { - return true - } - - return false + if (sdksFilter.includes(targetSdk)) { + return true + } - } - ) - }) - // scope urls so they point to the current sdk - .use(() => (tree, vfile) => { - return mdastMap(tree, - node => { - if (node.type !== "link") return node - if (!("url" in node)) { - vfile.fail(`Link node does not have a url property ${pleaseReport}`, node.position) - return node - } - if (typeof node.url !== "string") { - vfile.fail(`Link node url must be a string ${pleaseReport}`, node.position) - return node - } - if (!node.url.startsWith("/docs/")) { - return node - } + return false + }) + }) + // scope urls so they point to the current sdk + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) { + vfile.fail(`Link node does not have a url property ${pleaseReport}`, node.position) + return node + } + if (typeof node.url !== 'string') { + vfile.fail(`Link node url must be a string ${pleaseReport}`, node.position) + return node + } + if (!node.url.startsWith('/docs/')) { + return node + } - const guide = guides.get(node.url) + const guide = guides.get(node.url) - if (guide === undefined) { } + if (guide === undefined) { + } - return node - } - ) - }) - .process({ - ...doc.vfile, messages: [] // reset the messages, otherwise they will be duplicated - }) + return node + }) + }) + .process({ + ...doc.vfile, + messages: [], // reset the messages, otherwise they will be duplicated + }) - await writeSdkFile(targetSdk, `${doc.href.replace("/docs/", "")}.mdx`, String(vfile)) + await writeSdkFile(targetSdk, `${doc.href.replace('/docs/', '')}.mdx`, String(vfile)) - return vfile - })) + return vfile + }), + ) - await writeSdkFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) + await writeSdkFile(targetSdk, 'manifest.json', JSON.stringify({ navigation })) - return { targetSdk, vFiles } - })) + return { targetSdk, vFiles } + }), + ) - sdkSpecificVFiles.forEach(({ targetSdk, vFiles }) => console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific guides`)) + sdkSpecificVFiles.forEach(({ targetSdk, vFiles }) => + console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific guides`), + ) const flatSdkSpecificVFiles = sdkSpecificVFiles.flatMap(({ vFiles }) => vFiles) - const output = reporter([ - ...coreVFiles.filter((item): item is NonNullable => item !== null), - ...flatSdkSpecificVFiles.filter((item): item is NonNullable => item !== null) - ], - { quiet: true }) + const output = reporter( + [ + ...coreVFiles.filter((item): item is NonNullable => item !== null), + ...flatSdkSpecificVFiles.filter((item): item is NonNullable => item !== null), + ], + { quiet: true }, + ) - if (output !== "") { + if (output !== '') { console.info(output) } return output } -const watchAndRebuild = ( - store: ReturnType, - config: BuildConfig -) => { - const watcher = chok.watch( - [ - config.docsPath, - ], - { - alwaysStat: true, - ignored: (filePath, stats) => { - if (stats === undefined) return false - if (stats.isDirectory()) return false +const watchAndRebuild = (store: ReturnType, config: BuildConfig) => { + const watcher = chok.watch([config.docsPath], { + alwaysStat: true, + ignored: (filePath, stats) => { + if (stats === undefined) return false + if (stats.isDirectory()) return false - const relativePath = path.relative(config.docsPath, filePath) + const relativePath = path.relative(config.docsPath, filePath) - const isManifest = relativePath === 'manifest.json' - const isMarkdown = relativePath.endsWith('.mdx') + const isManifest = relativePath === 'manifest.json' + const isMarkdown = relativePath.endsWith('.mdx') - return !(isManifest || isMarkdown) - }, - ignoreInitial: true, - } - ) - - watcher.on("all", async (event, filePath) => { + return !(isManifest || isMarkdown) + }, + ignoreInitial: true, + }) + watcher.on('all', async (event, filePath) => { console.info(`File ${filePath} changed`, { event }) const href = removeMdxSuffix(`/${path.relative(config.basePath, filePath)}`) @@ -1028,24 +1083,22 @@ const watchAndRebuild = ( store.markdownFiles.delete(href) await build(store, config) - }) - } type BuildConfigOptions = { - basePath: string; - validSdks: readonly SDK[]; - docsPath: string; - manifestPath: string; - partialsPath: string; - distPath: string; - ignorePaths: string[]; + basePath: string + validSdks: readonly SDK[] + docsPath: string + manifestPath: string + partialsPath: string + distPath: string + ignorePaths: string[] manifestOptions: { - wrapDefault: boolean; - collapseDefault: boolean; - hideTitleDefault: boolean; - }; + wrapDefault: boolean + collapseDefault: boolean + hideTitleDefault: boolean + } } type BuildConfig = ReturnType @@ -1075,13 +1128,12 @@ export function createConfig(config: BuildConfigOptions) { manifestOptions: config.manifestOptions ?? { wrapDefault: true, collapseDefault: false, - hideTitleDefault: false + hideTitleDefault: false, }, } } const main = async () => { - const config = createConfig({ basePath: process.cwd(), docsPath: './docs', @@ -1089,7 +1141,7 @@ const main = async () => { partialsPath: './_partials', distPath: './dist', ignorePaths: [ - "/docs/core-1", + '/docs/core-1', '/pricing', '/docs/reference/backend-api', '/docs/reference/frontend-api', @@ -1100,33 +1152,31 @@ const main = async () => { '/contact/support', '/blog', '/changelog/2024-04-19', - "/docs/_partials" + '/docs/_partials', ], validSdks: VALID_SDKS, manifestOptions: { wrapDefault: true, collapseDefault: false, - hideTitleDefault: false - } + hideTitleDefault: false, + }, }) - const store = createBlankStore(); + const store = createBlankStore() - await build(store, config); + await build(store, config) const args = process.argv.slice(2) const watchFlag = args.includes('--watch') if (watchFlag) { - console.info(`Watching for changes...`) - watchAndRebuild(store, config); + watchAndRebuild(store, config) } - } // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts if (require.main === module) { - main(); -} \ No newline at end of file + main() +} From 24cd69d4473fac29de304007b8ddd02eb4c3b98a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 5 Mar 2025 23:45:21 +0800 Subject: [PATCH 045/114] run prettier again --- scripts/build-docs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 7ae3f4ad97..07e32abe57 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -790,7 +790,6 @@ export const build = async (store: ReturnType, config: ) } })() - ;(() => { // The doc is generic so we are skipping it if (availableSDKs.length === 0) return From d52b38ea23a3cb6398a77ba5d34fc2b90fe97ca5 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 5 Mar 2025 23:48:13 +0800 Subject: [PATCH 046/114] run prettier again --- scripts/build-docs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index ea2ea51a24..8ded8838a9 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -864,7 +864,6 @@ export const build = async (store: ReturnType, config: ) } })() - ;(() => { // The doc is generic so we are skipping it if (availableSDKs.length === 0) return From 81332d613fce8a56e9470634efb38ef4194195d4 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 6 Mar 2025 01:03:19 +0800 Subject: [PATCH 047/114] clean up the pr a little --- .gitignore | 1 - package-lock.json | 43 ------------------------------------------- package.json | 3 --- 3 files changed, 47 deletions(-) diff --git a/.gitignore b/.gitignore index 73ec6f8efc..461839a0c4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ # production /build -/dist # misc .DS_Store diff --git a/package-lock.json b/package-lock.json index b7df2e739b..24ece37110 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,9 +24,6 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", - "unist-builder": "^4.0.0", - "unist-util-filter": "^5.0.1", - "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", @@ -3929,32 +3926,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-builder": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", - "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-filter": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz", - "integrity": "sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - } - }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -3968,20 +3939,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-map/-/unist-util-map-4.0.0.tgz", - "integrity": "sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-position-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", diff --git a/package.json b/package.json index 2f832e2f5e..243d643145 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,6 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", - "unist-builder": "^4.0.0", - "unist-util-filter": "^5.0.1", - "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", From 322c33359f8d0ad2d65b871c603831440048377d Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Mar 2025 02:18:07 +0800 Subject: [PATCH 048/114] Fail the run on warnings --- scripts/build-docs.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 07e32abe57..8b681023e6 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -951,10 +951,16 @@ const main = async () => { const store = createBlankStore() - await build(store, config) + return await build(store, config) } // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts if (require.main === module) { - main() + (async () => { + const output = await main() + + if (output !== '') { + process.exit(1) + } + })() } From 0727b647ab1b7d5adc771cf0ebe264ecee2cf626 Mon Sep 17 00:00:00 2001 From: Jeff Escalante Date: Thu, 6 Mar 2025 14:11:00 -0500 Subject: [PATCH 049/114] review comments --- scripts/build-docs.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 8b681023e6..83f559ffb8 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -523,6 +523,7 @@ const parseInMarkdownFile = return } + // this could be done as part of the partial reading instead of here const partialContentVFile = markdownProcessor() .use(() => (tree, vfile) => { mdastVisit( @@ -609,6 +610,7 @@ export const build = async (store: ReturnType, config: const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) console.info('✔️ Read Partials') + // slightly confusing variable naming const guides = new Map>>() const guidesInManifest = new Set() @@ -632,6 +634,7 @@ export const build = async (store: ReturnType, config: docsFiles.map(async (file) => { const href = removeMdxSuffix(`/docs/${file.path}`) + // maybe don't need caching here? const alreadyLoaded = guides.get(href) if (alreadyLoaded) return null // already processed @@ -640,6 +643,7 @@ export const build = async (store: ReturnType, config: let markdownFile: Awaited> + // maybe don't need caching here? const cachedMarkdownFile = store.markdownFiles.get(href) if (cachedMarkdownFile) { @@ -894,6 +898,7 @@ type BuildConfigOptions = { type BuildConfig = ReturnType +// This is what this function does, and why!? export function createConfig(config: BuildConfigOptions) { const resolve = (relativePath: string) => { return path.isAbsolute(relativePath) ? relativePath : path.join(config.basePath, relativePath) From 0eb81d695177fcfe747582fb5abbbfeabf0d6a1c Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Mar 2025 04:45:11 +0800 Subject: [PATCH 050/114] Implement improvements from code review --- package-lock.json | 31 ---- package.json | 1 - scripts/build-docs.test.ts | 96 ++++++------ scripts/build-docs.ts | 311 ++++++++++++++----------------------- 4 files changed, 166 insertions(+), 273 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24ece37110..da8f5573cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "devDependencies": { "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", - "chokidar": "^4.0.3", "concurrently": "^8.2.2", "glob": "^11.0.1", "prettier": "^3.2.5", @@ -1359,36 +1358,6 @@ "node": ">= 16" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chokidar/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", diff --git a/package.json b/package.json index 243d643145..5a8791a3ea 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "devDependencies": { "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", - "chokidar": "^4.0.3", "concurrently": "^8.2.2", "glob": "^11.0.1", "prettier": "^3.2.5", diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 03c517445e..14b97b3d6a 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -4,7 +4,7 @@ import os from 'node:os' import { glob } from 'glob' import { describe, expect, onTestFinished, test } from 'vitest' -import { build, createBlankStore, createConfig } from './build-docs' +import { build, createConfig } from './build-docs' const tempConfig = { // Set to true to use local repo temp directory instead of system temp @@ -91,30 +91,6 @@ async function createTempFiles( } } -async function fileExists(filePath: string): Promise { - try { - await fs.access(filePath) - return true - } catch { - return false - } -} - -async function readFile(filePath: string): Promise { - return normalizeString(await fs.readFile(filePath, 'utf-8')) -} - -function normalizeString(str: string): string { - return str.replace(/\r\n/g, '\n').trim() -} - -function treeDir(baseDir: string) { - return glob('**/*', { - cwd: baseDir, - nodir: true, // Only return files, not directories - }) -} - const baseConfig = { docsPath: './docs', manifestPath: './docs/manifest.json', @@ -149,7 +125,6 @@ Testing with a simple page.`, ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -182,7 +157,6 @@ Testing with a simple page.`, ]) const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -219,7 +193,6 @@ Testing with a simple page.`, ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -230,6 +203,56 @@ Testing with a simple page.`, expect(output).toContain(`warning sdk \"astro\" in is not a valid SDK`) }) +test('should fail when child SDK is not in parent SDK list', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'Authentication', + sdk: ['react'], + items: [ + [ + { + title: 'Login', + href: '/docs/auth/login', + sdk: ['react', 'python'], // python not in parent + }, + ], + ], + }, + ], + ], + }), + }, + { + path: './docs/auth/login.mdx', + content: `--- +title: Login +sdk: react, python +--- + +# Login Page + +Authentication login documentation.`, + }, + ]) + + const promise = build( + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'python', 'nextjs'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Guide "Login" is attempting to use ["react","python"] But its being filtered down to ["react"] in the manifest.json', + ) +}) + describe('Includes and Partials', () => { test('Invalid partial src fails the build', async () => { const { tempDir } = await createTempFiles([ @@ -252,7 +275,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -292,7 +314,6 @@ title: Simple Test ]) const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -324,7 +345,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -358,7 +378,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -398,7 +417,6 @@ title: Core Page ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -430,7 +448,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -473,7 +490,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -506,7 +522,6 @@ title: React Guide // This should throw an error because the file path starts with an SDK name const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -515,7 +530,7 @@ title: React Guide ) await expect(promise).rejects.toThrow( - 'Attempting to write out a core doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict', + 'Doc "/docs/react/conflict" is attempting to write out a doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict.', ) }) }) @@ -542,7 +557,6 @@ description: \`This frontmatter has an unbalanced quote // This should throw a parsing error const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -573,7 +587,6 @@ description: This frontmatter is missing the required title field // This should throw an error about missing title const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -599,7 +612,6 @@ description: This frontmatter is missing the required title field ]) const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -637,7 +649,6 @@ This page has an invalid SDK in frontmatter.`, // This should throw an error with specific message about invalid SDK const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -685,7 +696,6 @@ This document doesn't have the referenced header.`, // Should complete with warnings const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -738,7 +748,6 @@ title: Document with Warnings // Should complete with warnings const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -797,7 +806,6 @@ Content for section 2.`, ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 83f559ffb8..dd213a1703 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1,13 +1,14 @@ -// Things this build script does - -// - [x] Validates the manifest -// - [x] Validates the markdown files contents (including frontmatter) -// - [x] Validates links (including hashes) between docs are valid -// - [x] Validates the sdk filtering in the manifest -// - [x] Validates the sdk filtering in the frontmatter -// - [x] Validates the sdk filtering in the component -// - [x] Checks that the sdk is available in the manifest -// - [x] Checks that the sdk is available in the frontmatter +// Things this script does + +// Validates +// - The manifest +// - The markdown files contents (including frontmatter) +// - Links (including hashes) between docs are valid +// - The sdk filtering in the manifest +// - The sdk filtering in the frontmatter +// - The sdk filtering in the component +// - Checks that the sdk is available in the manifest +// - Checks that the sdk is available in the frontmatter import fs from 'node:fs/promises' import path from 'node:path' @@ -23,7 +24,6 @@ import readdirp from 'readdirp' import { z } from 'zod' import { fromError } from 'zod-validation-error' import { Node } from 'unist' -import chok from 'chokidar' const VALID_SDKS = [ 'nextjs', @@ -178,15 +178,15 @@ const pleaseReport = '(this is a bug with the build script, please report)' const isValidSdk = (config: BuildConfig) => - (sdk: string): sdk is SDK => { - return config.validSdks.includes(sdk as SDK) - } + (sdk: string): sdk is SDK => { + return config.validSdks.includes(sdk as SDK) + } const isValidSdks = (config: BuildConfig) => - (sdks: string[]): sdks is SDK[] => { - return sdks.every(isValidSdk(config)) - } + (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk(config)) + } const readManifest = (config: BuildConfig) => async (): Promise => { const { manifestSchema } = createManifestSchema(config) @@ -241,9 +241,35 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) } + const partialContentVFile = markdownProcessor() + .use(() => (tree, vfile) => { + mdastVisit( + tree, + (node) => + (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && + 'name' in node && + node.name === 'Include', + (node) => { + vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) + }, + ) + }) + .processSync({ + path: markdownPath, + value: content, + }) + + const partialContentReport = reporter([partialContentVFile], { quiet: true }) + + if (partialContentReport !== '') { + console.error(partialContentReport) + process.exit(1) + } + return { path: markdownPath, content, + vfile: partialContentVFile, } }), ) @@ -340,22 +366,6 @@ function flattenTree< return result } -const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { - // This is external so can't change it - if (href.startsWith('/docs') === false) return href - - const hrefSegments = href.split('/') - - // This is a little hacky so we might change it - // if the url already contains the sdk, we don't need to change it - if (hrefSegments.includes(targetSDK)) { - return href - } - - // Add the sdk to the url - return `/docs/${targetSDK}/${hrefSegments.slice(2).join('/')}` -} - const extractComponentPropValueFromNode = ( node: Node, vfile: VFile | undefined, @@ -522,31 +532,6 @@ const parseInMarkdownFile = vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) return } - - // this could be done as part of the partial reading instead of here - const partialContentVFile = markdownProcessor() - .use(() => (tree, vfile) => { - mdastVisit( - tree, - (node) => - (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && - 'name' in node && - node.name === 'Include', - () => { - vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) - }, - ) - }) - .processSync({ - path: partial.path, - value: partial.content, - }) - - const partialContentReport = reporter([partialContentVFile], { quiet: true }) - - if (partialContentReport !== '') { - console.error(partialContentReport) - } }) }) // extract out the headings to check hashes in links @@ -589,11 +574,7 @@ const parseInMarkdownFile = } } -export const createBlankStore = () => ({ - markdownFiles: new Map>>>(), -}) - -export const build = async (store: ReturnType, config: BuildConfig) => { +export const build = async (config: BuildConfig) => { // Apply currying to create functions pre-configured with config const getManifest = readManifest(config) const getDocsFolder = readDocsFolder(config) @@ -608,11 +589,10 @@ export const build = async (store: ReturnType, config: console.info('✔️ Read Docs Folder') const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) - console.info('✔️ Read Partials') + console.info(`✔️ Read ${partials.length} Partials`) - // slightly confusing variable naming - const guides = new Map>>() - const guidesInManifest = new Set() + const docsMap = new Map>>() + const docsInManifest = new Set() // Grab all the docs links in the manifest await traverseTree({ items: userManifest }, async (item) => { @@ -622,45 +602,27 @@ export const build = async (store: ReturnType, config: const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) if (ignore === true) return item - guidesInManifest.add(item.href) + docsInManifest.add(item.href) return item }) console.info('✔️ Parsed in Manifest') - // Read in all the guides - const docs = ( - await Promise.all( - docsFiles.map(async (file) => { - const href = removeMdxSuffix(`/docs/${file.path}`) + // Read in all the docs + const docsArray = await Promise.all( + docsFiles.map(async (file) => { + const href = removeMdxSuffix(`/docs/${file.path}`) - // maybe don't need caching here? - const alreadyLoaded = guides.get(href) + const inManifest = docsInManifest.has(href) - if (alreadyLoaded) return null // already processed + const markdownFile = await parseMarkdownFile(href, partials, inManifest) - const inManifest = guidesInManifest.has(href) + docsMap.set(href, markdownFile) - let markdownFile: Awaited> - - // maybe don't need caching here? - const cachedMarkdownFile = store.markdownFiles.get(href) - - if (cachedMarkdownFile) { - markdownFile = structuredClone(cachedMarkdownFile) - } else { - markdownFile = await parseMarkdownFile(href, partials, inManifest) - - store.markdownFiles.set(href, structuredClone(markdownFile)) - } - - guides.set(href, markdownFile) - - return markdownFile - }), - ) - ).filter((item): item is NonNullable => item !== null) - console.info(`✔️ Loaded in ${docs.length} guides`) + return markdownFile + }), + ) + console.info(`✔️ Loaded in ${docsArray.length} docs`) // Goes through and grabs the sdk scoping out of the manifest const sdkScopedManifest = await traverseTree( @@ -672,18 +634,18 @@ export const build = async (store: ReturnType, config: const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) if (ignore === true) return item // even thou we are not processing them, we still need to keep them - const guide = guides.get(item.href) + const doc = docsMap.get(item.href) - if (guide === undefined) { - throw new Error(`Guide "${item.title}" in manifest.json not found in the docs folder at ${item.href}.mdx`) + if (doc === undefined) { + throw new Error(`Doc "${item.title}" in manifest.json not found in the docs folder at ${item.href}.mdx`) } - const sdk = guide.sdk ?? tree.sdk + const sdk = doc.sdk ?? tree.sdk - if (guide.sdk !== undefined && tree.sdk !== undefined) { - if (guide.sdk.every((sdk) => tree.sdk?.includes(sdk)) === false) { + if (doc.sdk !== undefined && tree.sdk !== undefined) { + if (doc.sdk.every((sdk) => tree.sdk?.includes(sdk)) === false) { throw new Error( - `Guide "${item.title}" is attempting to use ${JSON.stringify(guide.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`, + `Doc "${item.title}" is attempting to use ${JSON.stringify(doc.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`, ) } } @@ -693,26 +655,40 @@ export const build = async (store: ReturnType, config: sdk, } }, - async (group, tree) => { - const itemsSDKs = Array.from(new Set(group.items?.flatMap((item) => item.flatMap((item) => item.sdk)))).filter( - (sdk): sdk is SDK => sdk !== undefined, - ) + async ({ items, ...details }, tree) => { + + // This takes all the children items, grabs the sdks out of them, and combines that in to a list + const groupsItemsCombinedSDKs = (() => { + const sdks = items?.flatMap((item) => item.flatMap((item) => item.sdk)) + + if (sdks === undefined) return [] + + return Array.from(new Set(sdks)).filter((sdk): sdk is SDK => sdk !== undefined) + })() + + // This is the sdk of the group + const groupSDK = details.sdk - const { items, ...details } = group + // This is the sdk of the parent group + const parentSDK = tree.sdk - if (details.sdk !== undefined && tree.sdk !== undefined) { - if (details.sdk.every((sdk) => tree.sdk?.includes(sdk)) === false) { + if (groupSDK !== undefined && parentSDK !== undefined) { + if (groupSDK.every((sdk) => parentSDK?.includes(sdk)) === false) { throw new Error( - `Group "${details.title}" is attempting to use ${JSON.stringify(details.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`, + `Group "${details.title}" is attempting to use ${JSON.stringify(groupSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, ) } } - if (itemsSDKs.length === 0) return { ...details, sdk: details.sdk ?? tree.sdk, items } as ManifestGroup + // If there are no children items, then the we either use the group we are looking at sdks if its defined, or its parent group + if (groupsItemsCombinedSDKs.length === 0) { + return { ...details, sdk: groupSDK ?? parentSDK, items } as ManifestGroup + } return { ...details, - sdk: Array.from(new Set([...(details.sdk ?? []), ...itemsSDKs])) ?? [], + // If there are children items, then we combine the sdks of the group and the children items sdks + sdk: Array.from(new Set([...(groupSDK ?? []), ...groupsItemsCombinedSDKs])) ?? [], items, } as ManifestGroup }, @@ -725,11 +701,8 @@ export const build = async (store: ReturnType, config: const flatSDKScopedManifest = flattenTree(sdkScopedManifest) - // It would definitely be preferable we didn't need to do this markdown processing twice - // But because we need a full list / hashmap of all the existing docs, we can't - // Unless maybe we do some kind of lazy loading of the docs, but this would add complexity const coreVFiles = await Promise.all( - docs.map(async (doc) => { + docsArray.map(async (doc) => { const vfile = await markdownProcessor() // Validate links between guides are valid .use(() => (tree: Node, vfile: VFile) => { @@ -740,14 +713,12 @@ export const build = async (store: ReturnType, config: if (!node.url.startsWith('/docs/')) return if (!('children' in node)) return - node.url = removeMdxSuffix(node.url) - - const [url, hash] = (node.url as string).split('#') + const [url, hash] = removeMdxSuffix(node.url).split('#') const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) if (ignore === true) return - const guide = guides.get(url) + const guide = docsMap.get(url) if (guide === undefined) { vfile.message(`Guide ${url} not found`, node.position) @@ -782,7 +753,7 @@ export const build = async (store: ReturnType, config: if (manifestItems.length === 0) return sdksFilter.forEach((sdk) => { - ;(() => { + ; (() => { if (doc.sdk === undefined) return const available = doc.sdk.includes(sdk) @@ -794,19 +765,19 @@ export const build = async (store: ReturnType, config: ) } })() - ;(() => { - // The doc is generic so we are skipping it - if (availableSDKs.length === 0) return - - const available = availableSDKs.includes(sdk) - - if (available === false) { - vfile.fail( - ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, - node.position, - ) - } - })() + ; (() => { + // The doc is generic so we are skipping it + if (availableSDKs.length === 0) return + + const available = availableSDKs.includes(sdk) + + if (available === false) { + vfile.fail( + ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, + node.position, + ) + } + })() }) }) }) @@ -816,7 +787,7 @@ export const build = async (store: ReturnType, config: if (isValidSdk(config)(distFilePath.split('/')[0])) { throw new Error( - `Attempting to write out a core doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`, + `Doc "${doc.href}" is attempting to write out a doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`, ) } @@ -824,62 +795,9 @@ export const build = async (store: ReturnType, config: }), ) - console.info(`✔️ Wrote out ${docs.length} core docs`) - - const sdkSpecificVFiles = await Promise.all( - config.validSdks.map(async (targetSdk) => { - const vFiles = await Promise.all( - docs.map(async (doc) => { - if (doc.sdk === undefined) return null // skip core docs - if (doc.sdk.includes(targetSdk) === false) return null // skip docs that are not for the target sdk - - const vfile = await markdownProcessor() - // scope urls so they point to the current sdk - .use(() => (tree, vfile) => { - return mdastVisit(tree, (node) => { - if (node.type !== 'link') return - if (!('url' in node)) { - vfile.fail(`Link node does not have a url property ${pleaseReport}`, node.position) - return - } - if (typeof node.url !== 'string') { - vfile.fail(`Link node url must be a string ${pleaseReport}`, node.position) - return - } - }) - }) - .process({ - ...doc.vfile, - messages: [], // reset the messages, otherwise they will be duplicated - }) - - return vfile - }), - ) - - return { targetSdk, vFiles } - }), - ) - - sdkSpecificVFiles.forEach(({ targetSdk, vFiles }) => - console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific guides`), - ) - - const flatSdkSpecificVFiles = sdkSpecificVFiles.flatMap(({ vFiles }) => vFiles) - - const output = reporter( - [ - ...coreVFiles.filter((item): item is NonNullable => item !== null), - ...flatSdkSpecificVFiles.filter((item): item is NonNullable => item !== null), - ], - { quiet: true }, - ) - - if (output !== '') { - console.info(output) - } + console.info(`✔️ Validated all docs`) - return output + return reporter(coreVFiles, { quiet: true }) } type BuildConfigOptions = { @@ -898,7 +816,7 @@ type BuildConfigOptions = { type BuildConfig = ReturnType -// This is what this function does, and why!? +// Takes the basePath and resolves the relative paths to be absolute paths export function createConfig(config: BuildConfigOptions) { const resolve = (relativePath: string) => { return path.isAbsolute(relativePath) ? relativePath : path.join(config.basePath, relativePath) @@ -954,17 +872,16 @@ const main = async () => { }, }) - const store = createBlankStore() - - return await build(store, config) + return await build(config) } // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts if (require.main === module) { - (async () => { + ; (async () => { const output = await main() if (output !== '') { + console.info(output) process.exit(1) } })() From d1ca7fce7b2c853e5fd7ac13b008449173238c57 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Mar 2025 04:47:18 +0800 Subject: [PATCH 051/114] test build script --- .github/workflows/test-build.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/test-build.yml diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml new file mode 100644 index 0000000000..abb906f11f --- /dev/null +++ b/.github/workflows/test-build.yml @@ -0,0 +1,15 @@ +name: Test build script + +on: + push: + paths: + - './scripts/build-docs.ts' + - './scripts/build-docs.test.ts' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: npm i + - run: npm run test From 5d6d29801ff36004ad7c8c5f435ade85cfb4e55e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Mar 2025 04:48:06 +0800 Subject: [PATCH 052/114] run formatter --- scripts/build-docs.ts | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index dd213a1703..6ed680c822 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -178,15 +178,15 @@ const pleaseReport = '(this is a bug with the build script, please report)' const isValidSdk = (config: BuildConfig) => - (sdk: string): sdk is SDK => { - return config.validSdks.includes(sdk as SDK) - } + (sdk: string): sdk is SDK => { + return config.validSdks.includes(sdk as SDK) + } const isValidSdks = (config: BuildConfig) => - (sdks: string[]): sdks is SDK[] => { - return sdks.every(isValidSdk(config)) - } + (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk(config)) + } const readManifest = (config: BuildConfig) => async (): Promise => { const { manifestSchema } = createManifestSchema(config) @@ -656,7 +656,6 @@ export const build = async (config: BuildConfig) => { } }, async ({ items, ...details }, tree) => { - // This takes all the children items, grabs the sdks out of them, and combines that in to a list const groupsItemsCombinedSDKs = (() => { const sdks = items?.flatMap((item) => item.flatMap((item) => item.sdk)) @@ -753,7 +752,7 @@ export const build = async (config: BuildConfig) => { if (manifestItems.length === 0) return sdksFilter.forEach((sdk) => { - ; (() => { + ;(() => { if (doc.sdk === undefined) return const available = doc.sdk.includes(sdk) @@ -765,19 +764,19 @@ export const build = async (config: BuildConfig) => { ) } })() - ; (() => { - // The doc is generic so we are skipping it - if (availableSDKs.length === 0) return - - const available = availableSDKs.includes(sdk) - - if (available === false) { - vfile.fail( - ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, - node.position, - ) - } - })() + ;(() => { + // The doc is generic so we are skipping it + if (availableSDKs.length === 0) return + + const available = availableSDKs.includes(sdk) + + if (available === false) { + vfile.fail( + ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, + node.position, + ) + } + })() }) }) }) @@ -877,7 +876,7 @@ const main = async () => { // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts if (require.main === module) { - ; (async () => { + ;(async () => { const output = await main() if (output !== '') { From 39673a2de67e5c27e10137ee5c68c1818a045124 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Mar 2025 04:56:34 +0800 Subject: [PATCH 053/114] switch all terminology from guide over to doc --- scripts/build-docs.test.ts | 12 ++++++------ scripts/build-docs.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 14b97b3d6a..94cf5f0b31 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -249,7 +249,7 @@ Authentication login documentation.`, ) await expect(promise).rejects.toThrow( - 'Guide "Login" is attempting to use ["react","python"] But its being filtered down to ["react"] in the manifest.json', + 'Doc "Login" is attempting to use ["react","python"] But its being filtered down to ["react"] in the manifest.json', ) }) @@ -385,7 +385,7 @@ title: Simple Test }), ) - expect(output).toContain(`warning Guide /docs/non-existent-page not found`) + expect(output).toContain(`warning Doc /docs/non-existent-page not found`) }) test('Validate link between two pages is valid', async () => { @@ -424,7 +424,7 @@ title: Core Page }), ) - expect(output).not.toContain(`warning Guide /docs/core-page not found`) + expect(output).not.toContain(`warning Doc /docs/core-page not found`) }) test('Warn if link is to existent page but with invalid hash', async () => { @@ -507,13 +507,13 @@ describe('Path and File Handling', () => { { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'React Guide', href: '/docs/react/conflict' }]], + navigation: [[{ title: 'React Doc', href: '/docs/react/conflict' }]], }), }, { path: './docs/react/conflict.mdx', content: `--- -title: React Guide +title: React Doc --- # This will cause a conflict because it's in a path that starts with "react"`, @@ -756,7 +756,7 @@ title: Document with Warnings ) // Check that warnings were reported - expect(output).toContain('warning Guide /docs/non-existent-document not found') + expect(output).toContain('warning Doc /docs/non-existent-document not found') expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK') }) }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 6ed680c822..5326a01185 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -465,7 +465,7 @@ const parseInMarkdownFile = .use(() => (tree, vfile) => { if (inManifest === false) { vfile.message( - 'This guide is not in the manifest.json, but will still be publicly accessible and other guides can link to it', + 'This doc is not in the manifest.json, but will still be publicly accessible and other docs can link to it', ) } @@ -703,7 +703,7 @@ export const build = async (config: BuildConfig) => { const coreVFiles = await Promise.all( docsArray.map(async (doc) => { const vfile = await markdownProcessor() - // Validate links between guides are valid + // Validate links between docs are valid .use(() => (tree: Node, vfile: VFile) => { return mdastVisit(tree, (node) => { if (node.type !== 'link') return @@ -717,15 +717,15 @@ export const build = async (config: BuildConfig) => { const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) if (ignore === true) return - const guide = docsMap.get(url) + const doc = docsMap.get(url) - if (guide === undefined) { - vfile.message(`Guide ${url} not found`, node.position) + if (doc === undefined) { + vfile.message(`Doc ${url} not found`, node.position) return } if (hash !== undefined) { - const hasHash = guide.headingsHashs.includes(hash) + const hasHash = doc.headingsHashs.includes(hash) if (hasHash === false) { vfile.message(`Hash "${hash}" not found in ${url}`, node.position) @@ -759,7 +759,7 @@ export const build = async (config: BuildConfig) => { if (available === false) { vfile.fail( - ` component is attempting to filter to sdk "${sdk}" but it is not available in the guides frontmatter ["${doc.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, + ` component is attempting to filter to sdk "${sdk}" but it is not available in the docs frontmatter ["${doc.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, node.position, ) } From 488776e56a025ca66ff1b0ff45d71043c7ca7614 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Mar 2025 22:14:51 +0800 Subject: [PATCH 054/114] fix sdk manifest filtering --- scripts/build-docs.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 5326a01185..548a712edb 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -640,12 +640,19 @@ export const build = async (config: BuildConfig) => { throw new Error(`Doc "${item.title}" in manifest.json not found in the docs folder at ${item.href}.mdx`) } - const sdk = doc.sdk ?? tree.sdk + // This is the sdk of the doc + const docSDK = doc.sdk - if (doc.sdk !== undefined && tree.sdk !== undefined) { - if (doc.sdk.every((sdk) => tree.sdk?.includes(sdk)) === false) { + // This is the sdk of the parent group + const parentSDK = tree.sdk + + // either use the defined sdk of the doc, or the parent group + const sdk = docSDK ?? parentSDK + + if (docSDK !== undefined && parentSDK !== undefined) { + if (docSDK.every((sdk) => parentSDK?.includes(sdk)) === false) { throw new Error( - `Doc "${item.title}" is attempting to use ${JSON.stringify(doc.sdk)} But its being filtered down to ${JSON.stringify(tree.sdk)} in the manifest.json`, + `Doc "${item.title}" is attempting to use ${JSON.stringify(docSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, ) } } @@ -684,6 +691,14 @@ export const build = async (config: BuildConfig) => { return { ...details, sdk: groupSDK ?? parentSDK, items } as ManifestGroup } + if (groupSDK !== undefined && groupSDK.length > 0) { + return { + ...details, + sdk: groupSDK, + items, + } as ManifestGroup + } + return { ...details, // If there are children items, then we combine the sdks of the group and the children items sdks From e49329fa524723b102d86a6b7feedb8ba79c3dbd Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Mar 2025 22:55:11 +0800 Subject: [PATCH 055/114] component will no always have sdk prop --- scripts/build-docs.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 548a712edb..e1757cc726 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -371,6 +371,7 @@ const extractComponentPropValueFromNode = ( vfile: VFile | undefined, componentName: string, propName: string, + required = true, ): string | undefined => { // Check if it's an MDX component if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') { @@ -396,14 +397,18 @@ const extractComponentPropValueFromNode = ( const propAttribute = node.attributes.find((attribute) => attribute.name === propName) if (propAttribute === undefined) { - vfile?.message(`<${componentName} /> component has no "${propName}" attribute`, node.position) + if (required === true) { + vfile?.message(`<${componentName} /> component has no "${propName}" attribute`, node.position) + } return undefined } const value = propAttribute.value if (value === undefined) { - vfile?.message(`<${componentName} /> attribute "${propName}" has no value ${pleaseReport}`, node.position) + if (required === true) { + vfile?.message(`<${componentName} /> attribute "${propName}" has no value ${pleaseReport}`, node.position) + } return undefined } @@ -751,7 +756,7 @@ export const build = async (config: BuildConfig) => { // Validate the components .use(() => (tree, vfile) => { mdastVisit(tree, (node) => { - const sdk = extractComponentPropValueFromNode(node, vfile, 'If', 'sdk') + const sdk = extractComponentPropValueFromNode(node, vfile, 'If', 'sdk', false) if (sdk === undefined) return From cca07846d65090400938a78f5322588d236879ca Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 8 Mar 2025 03:05:18 +0800 Subject: [PATCH 056/114] use __dirname and update to support base dir being ./scripts/ --- scripts/build-docs.test.ts | 8 ++++---- scripts/build-docs.ts | 16 +++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 94cf5f0b31..02f009d882 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -73,7 +73,7 @@ async function createTempFiles( // Return useful helpers return { - tempDir, + tempDir: path.join(tempDir, 'scripts'), // emulate that the base path is the scripts folder, to emulate __dirname pathJoin: (...paths: string[]) => path.join(tempDir, ...paths), // Get a list of all files in the temp directory @@ -92,9 +92,9 @@ async function createTempFiles( } const baseConfig = { - docsPath: './docs', - manifestPath: './docs/manifest.json', - partialsPath: './_partials', + docsPath: '../docs', + manifestPath: '../docs/manifest.json', + partialsPath: '../docs/_partials', ignorePaths: ['/docs/_partials'], manifestOptions: { wrapDefault: true, diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index e1757cc726..775e6f1890 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -202,7 +202,7 @@ const readManifest = (config: BuildConfig) => async (): Promise => { } const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { - const filePath = path.join(config.basePath, docPath) + const filePath = path.join(config.docsPath, docPath) try { const fileContent = await fs.readFile(filePath, { encoding: 'utf-8' }) @@ -222,7 +222,7 @@ const readDocsFolder = (config: BuildConfig) => async () => { } const readPartialsFolder = (config: BuildConfig) => async () => { - return readdirp.promise(path.join(config.docsPath, config.partialsRelativePath), { + return readdirp.promise(config.partialsPath, { type: 'files', fileFilter: '*.mdx', }) @@ -447,7 +447,7 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile const parseInMarkdownFile = (config: BuildConfig) => async (href: string, partials: { path: string; content: string }[], inManifest: boolean) => { const readFile = readMarkdownFile(config) - const [error, fileContent] = await readFile(`${href}.mdx`) + const [error, fileContent] = await readFile(`${href}.mdx`.replace("/docs/", "")) if (error !== null) { throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { @@ -865,10 +865,10 @@ export function createConfig(config: BuildConfigOptions) { const main = async () => { const config = createConfig({ - basePath: process.cwd(), - docsPath: './docs', - manifestPath: './docs/manifest.json', - partialsPath: './_partials', + basePath: __dirname, + docsPath: '../docs', + manifestPath: '../docs/manifest.json', + partialsPath: '../docs/_partials', ignorePaths: [ '/docs/core-1', '/pricing', @@ -891,6 +891,8 @@ const main = async () => { }, }) + console.log(config) + return await build(config) } From 1c1010ea0db46c6b82e69e4481aace4bc0d0ed00 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 10 Mar 2025 23:26:39 +0800 Subject: [PATCH 057/114] add pull request trigger --- .github/workflows/test-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index abb906f11f..df0210ac3d 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -5,6 +5,10 @@ on: paths: - './scripts/build-docs.ts' - './scripts/build-docs.test.ts' + pull_request: + paths: + - './scripts/build-docs.ts' + - './scripts/build-docs.test.ts' jobs: test: From 8bfa30659830ec853a393a13326c6f34cd633bc5 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 10 Mar 2025 23:29:13 +0800 Subject: [PATCH 058/114] =?UTF-8?q?=E2=9C=A8=20prettier=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/build-docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 775e6f1890..f2a779dfa1 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -447,7 +447,7 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile const parseInMarkdownFile = (config: BuildConfig) => async (href: string, partials: { path: string; content: string }[], inManifest: boolean) => { const readFile = readMarkdownFile(config) - const [error, fileContent] = await readFile(`${href}.mdx`.replace("/docs/", "")) + const [error, fileContent] = await readFile(`${href}.mdx`.replace('/docs/', '')) if (error !== null) { throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { From ac002e7c85b18b0eb6a6d2408e32286bc333c5b3 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 10 Mar 2025 23:36:32 +0800 Subject: [PATCH 059/114] update github action paths --- .github/workflows/test-build.yml | 8 ++++---- scripts/build-docs.ts | 16 ++++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index df0210ac3d..c7e5f90c4d 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -3,12 +3,12 @@ name: Test build script on: push: paths: - - './scripts/build-docs.ts' - - './scripts/build-docs.test.ts' + - 'scripts/build-docs.ts' + - 'scripts/build-docs.test.ts' pull_request: paths: - - './scripts/build-docs.ts' - - './scripts/build-docs.test.ts' + - 'scripts/build-docs.ts' + - 'scripts/build-docs.test.ts' jobs: test: diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index f2a779dfa1..c40c0d7572 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -891,19 +891,15 @@ const main = async () => { }, }) - console.log(config) + const output = await build(config) - return await build(config) + if (output !== '') { + console.info(output) + process.exit(1) + } } // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts if (require.main === module) { - ;(async () => { - const output = await main() - - if (output !== '') { - console.info(output) - process.exit(1) - } - })() + main() } From 50bb6d522d6881e35be286152fa9664403e65b30 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 10 Mar 2025 23:37:26 +0800 Subject: [PATCH 060/114] Only need to do it on pushes --- .github/workflows/test-build.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index c7e5f90c4d..52df742834 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -5,10 +5,6 @@ on: paths: - 'scripts/build-docs.ts' - 'scripts/build-docs.test.ts' - pull_request: - paths: - - 'scripts/build-docs.ts' - - 'scripts/build-docs.test.ts' jobs: test: From a77115af03e809eb80f84050ba044a5d0b5002ba Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 10 Mar 2025 23:54:21 +0800 Subject: [PATCH 061/114] remove dev mode --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 5a8791a3ea..01641b6e22 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "lint:check-quickstarts": "node ./scripts/check-quickstarts.mjs", "lint:check-frontmatter": "node ./scripts/check-frontmatter.mjs", "build": "tsx ./scripts/build-docs.ts", - "dev": "tsx ./scripts/build-docs.ts --watch", "test": "vitest --silent" }, "devDependencies": { From c8a75d212bc8a379ed73656931ba90bef936a8fe Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 10 Mar 2025 23:56:07 +0800 Subject: [PATCH 062/114] omit an warning on a missing description --- scripts/build-docs.test.ts | 33 +++++++++++++++++++++++++++++++++ scripts/build-docs.ts | 4 ++++ 2 files changed, 37 insertions(+) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 02f009d882..62665d3102 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -116,6 +116,7 @@ test('Basic build test with simple files', async () => { path: './docs/simple-test.mdx', content: `--- title: Simple Test +description: This is a simple test page --- # Simple Test Page @@ -135,6 +136,38 @@ Testing with a simple page.`, expect(output).toBe('') }) +test('Warning on missing description in frontmatter', async () => { + // Create temp environment with minimal files array + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +# Simple Test Page + +Testing with a simple page.`, + }, + ]) + + const output = await build( + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react'], + }), + ) + + expect(output).toContain('warning Frontmatter should have a "description" property') +}) + test('Invalid SDK in frontmatter fails the build', async () => { const { tempDir, pathJoin } = await createTempFiles([ { diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index c40c0d7572..c6e8f40aac 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -504,6 +504,10 @@ const parseInMarkdownFile = return } + if (frontmatterYaml.description === undefined) { + vfile.message(`Frontmatter should have a "description" property`, node.position) + } + frontmatter = { title: frontmatterYaml.title, description: frontmatterYaml.description, From b1e9dd41cf289f3394b7d163a942203a204322a7 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 00:07:51 +0800 Subject: [PATCH 063/114] More robust id pull for markdown file headings --- scripts/build-docs.test.ts | 2 +- scripts/build-docs.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 62665d3102..84f5064731 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -510,7 +510,7 @@ title: Simple Test title: Headings --- -# test {{ id: 'my-heading' }}`, +# test {{ toc: false, id: 'my-heading' }}`, }, { path: './docs/simple-test.mdx', diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index c6e8f40aac..b4a93f3981 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -552,7 +552,7 @@ const parseInMarkdownFile = // @ts-expect-error - If the heading has a id in it, this will pick it up // eg # test {{ id: 'my-heading' }} // This is for remapping the hash to the custom id - const id = node?.children?.[1]?.data?.estree?.body?.[0]?.expression?.properties?.[0]?.value?.value as + const id = node?.children.find((child) => child.type === 'mdxTextExpression')?.data?.estree?.body.find((child) => child.type === 'ExpressionStatement')?.expression?.properties.find((prop) => prop.key.name === 'id').value.value as | string | undefined From 5b8e0854e1dc2d29617be487e8909d9f912afb22 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 00:21:43 +0800 Subject: [PATCH 064/114] fix heading id pull --- scripts/build-docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index b4a93f3981..b77b2baee8 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -552,7 +552,7 @@ const parseInMarkdownFile = // @ts-expect-error - If the heading has a id in it, this will pick it up // eg # test {{ id: 'my-heading' }} // This is for remapping the hash to the custom id - const id = node?.children.find((child) => child.type === 'mdxTextExpression')?.data?.estree?.body.find((child) => child.type === 'ExpressionStatement')?.expression?.properties.find((prop) => prop.key.name === 'id').value.value as + const id = node?.children?.find((child) => child?.type === 'mdxTextExpression')?.data?.estree?.body?.find((child) => child?.type === 'ExpressionStatement')?.expression?.properties?.find((prop) => prop?.key?.name === 'id')?.value?.value as | string | undefined From 55f67f4420e8155a249c8451d9a1a194664f5648 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 00:30:32 +0800 Subject: [PATCH 065/114] Fix up the quick link in the terminal to files --- scripts/build-docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index b77b2baee8..2873f843a4 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -566,7 +566,7 @@ const parseInMarkdownFile = ) }) .process({ - path: `${href}.mdx`, + path: `${href.substring(1)}.mdx`, value: fileContent, }) From e11d4e42c3ded99d702ecde68d39bc45939e03d9 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 00:31:15 +0800 Subject: [PATCH 066/114] add the build script (in its current validation state) as a lint step --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 01641b6e22..f9d7204f10 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint:formatting": "prettier . --check", "lint:check-quickstarts": "node ./scripts/check-quickstarts.mjs", "lint:check-frontmatter": "node ./scripts/check-frontmatter.mjs", + "lint:validation": "npm run build", "build": "tsx ./scripts/build-docs.ts", "test": "vitest --silent" }, From 4bcaf9703f6f01c30e40cc89c9bcdc720ce6cce6 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 00:39:39 +0800 Subject: [PATCH 067/114] =?UTF-8?q?=E2=9C=A8=20prettier=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/build-docs.test.ts | 42 +++++++++++++++++++------------------- scripts/build-docs.ts | 7 ++++--- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 84f5064731..aadbce5d83 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -137,33 +137,33 @@ Testing with a simple page.`, }) test('Warning on missing description in frontmatter', async () => { - // Create temp environment with minimal files array - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- + // Create temp environment with minimal files array + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- title: Simple Test --- # Simple Test Page Testing with a simple page.`, - }, - ]) - - const output = await build( - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['nextjs', 'react'], - }), - ) + }, + ]) + + const output = await build( + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react'], + }), + ) expect(output).toContain('warning Frontmatter should have a "description" property') }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 2873f843a4..de6edb97e0 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -552,9 +552,10 @@ const parseInMarkdownFile = // @ts-expect-error - If the heading has a id in it, this will pick it up // eg # test {{ id: 'my-heading' }} // This is for remapping the hash to the custom id - const id = node?.children?.find((child) => child?.type === 'mdxTextExpression')?.data?.estree?.body?.find((child) => child?.type === 'ExpressionStatement')?.expression?.properties?.find((prop) => prop?.key?.name === 'id')?.value?.value as - | string - | undefined + const id = node?.children + ?.find((child) => child?.type === 'mdxTextExpression') + ?.data?.estree?.body?.find((child) => child?.type === 'ExpressionStatement') + ?.expression?.properties?.find((prop) => prop?.key?.name === 'id')?.value?.value as string | undefined if (id !== undefined) { headingsHashs.push(id) From fbb46eca7abfd07fc8a6c06af0cf8855d4815188 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 02:49:15 +0800 Subject: [PATCH 068/114] Validate contents of partials --- package-lock.json | 15 +++++++++++ package.json | 1 + scripts/build-docs.test.ts | 52 ++++++++++++++++++++++++++++++++++++++ scripts/build-docs.ts | 26 ++++++++++++++----- 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index da8f5573cd..e6a024e745 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", + "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", @@ -3908,6 +3909,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-map/-/unist-util-map-4.0.0.tgz", + "integrity": "sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-position-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", diff --git a/package.json b/package.json index f9d7204f10..5c0faf6ca6 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", + "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index aadbce5d83..452ccf2933 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -532,6 +532,58 @@ title: Simple Test expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) }) + + test('Check link and hash in partial is valid', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Page 1', href: '/docs/page-1' }, + { title: 'Page 2', href: '/docs/page-2' }, + ], + ], + }), + }, + { + path: './docs/page-1.mdx', + content: `--- +title: Page 1 +--- + +`, + }, + { + path: './docs/_partials/links.mdx', + content: `--- +title: Links +--- + +[Page 2](/docs/page-2#my-heading) +[Page 2](/docs/page-3)`, + }, + { + path: './docs/page-2.mdx', + content: `--- +title: Page 2 +--- + +test`, + }, + ]) + + const output = await build( + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + expect(output).toContain(`warning Hash "my-heading" not found in /docs/page-2`) + expect(output).toContain(`warning Doc /docs/page-3 not found`) + }) }) describe('Path and File Handling', () => { diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index de6edb97e0..ad2ea9cabb 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -15,6 +15,7 @@ import path from 'node:path' import remarkMdx from 'remark-mdx' import { remark } from 'remark' import { visit as mdastVisit } from 'unist-util-visit' +import { map as mdastMap } from 'unist-util-map' import remarkFrontmatter from 'remark-frontmatter' import yaml from 'yaml' import { slugifyWithCounter } from '@sindresorhus/slugify' @@ -241,7 +242,9 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) } - const partialContentVFile = markdownProcessor() + let partialNode: Node | null = null + + const partialContentVFile = await markdownProcessor() .use(() => (tree, vfile) => { mdastVisit( tree, @@ -253,8 +256,10 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) }, ) + + partialNode = tree }) - .processSync({ + .process({ path: markdownPath, value: content, }) @@ -266,10 +271,15 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => process.exit(1) } + if (partialNode === null) { + throw new Error(`Failed to parse the content of ${markdownPath}`) + } + return { path: markdownPath, content, vfile: partialContentVFile, + node: partialNode as Node, } }), ) @@ -445,7 +455,7 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile } const parseInMarkdownFile = - (config: BuildConfig) => async (href: string, partials: { path: string; content: string }[], inManifest: boolean) => { + (config: BuildConfig) => async (href: string, partials: { path: string; content: string; node: Node }[], inManifest: boolean) => { const readFile = readMarkdownFile(config) const [error, fileContent] = await readFile(`${href}.mdx`.replace('/docs/', '')) @@ -523,14 +533,14 @@ const parseInMarkdownFile = }) // Validate the .use(() => (tree, vfile) => { - return mdastVisit(tree, (node) => { + return mdastMap(tree, (node) => { const partialSrc = extractComponentPropValueFromNode(node, vfile, 'Include', 'src') - if (partialSrc === undefined) return + if (partialSrc === undefined) return node if (partialSrc.startsWith('_partials/') === false) { vfile.message(` prop "src" must start with "_partials/"`, node.position) - return + return node } const partial = partials.find( @@ -539,8 +549,10 @@ const parseInMarkdownFile = if (partial === undefined) { vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) - return + return node } + + return Object.assign(node, partial.node) }) }) // extract out the headings to check hashes in links From 18ecb6b0c3737e53a65435f3a68ab89446c93675 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 03:12:03 +0800 Subject: [PATCH 069/114] separate out the partials validation to give better warning messages --- scripts/build-docs.ts | 52 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index ad2ea9cabb..4882644167 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -260,7 +260,7 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => partialNode = tree }) .process({ - path: markdownPath, + path: `docs/_partials/${markdownPath}`, value: content, }) @@ -533,14 +533,14 @@ const parseInMarkdownFile = }) // Validate the .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { + return mdastVisit(tree, (node) => { const partialSrc = extractComponentPropValueFromNode(node, vfile, 'Include', 'src') - if (partialSrc === undefined) return node + if (partialSrc === undefined) return if (partialSrc.startsWith('_partials/') === false) { vfile.message(` prop "src" must start with "_partials/"`, node.position) - return node + return } const partial = partials.find( @@ -549,10 +549,10 @@ const parseInMarkdownFile = if (partial === undefined) { vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) - return node + return } - return Object.assign(node, partial.node) + return }) }) // extract out the headings to check hashes in links @@ -737,6 +737,44 @@ export const build = async (config: BuildConfig) => { const flatSDKScopedManifest = flattenTree(sdkScopedManifest) + const partialsVFiles = await Promise.all( + partials.map(async (partial) => { + return await markdownProcessor() + // validate links in partials to docs are valid + .use(() => (tree, vfile) => { + return mdastVisit(tree, (node) => { + if (node.type !== 'link') return + if (!('url' in node)) return + if (typeof node.url !== 'string') return + if (!node.url.startsWith('/docs/')) return + if (!('children' in node)) return + + const [url, hash] = removeMdxSuffix(node.url).split('#') + + const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return + + const doc = docsMap.get(url) + + if (doc === undefined) { + vfile.message(`Doc ${url} not found`, node.position) + return + } + + if (hash !== undefined) { + const hasHash = doc.headingsHashs.includes(hash) + + if (hasHash === false) { + vfile.message(`Hash "${hash}" not found in ${url}`, node.position) + } + } + }) + }) + .process(partial.vfile) + }) + ) + console.info(`✔️ Validated all partials`) + const coreVFiles = await Promise.all( docsArray.map(async (doc) => { const vfile = await markdownProcessor() @@ -833,7 +871,7 @@ export const build = async (config: BuildConfig) => { console.info(`✔️ Validated all docs`) - return reporter(coreVFiles, { quiet: true }) + return reporter([...coreVFiles, ...partialsVFiles], { quiet: true }) } type BuildConfigOptions = { From c764355d6895ea595be278b66f76ddedc6d7380a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 03:21:32 +0800 Subject: [PATCH 070/114] remove unused dep --- package-lock.json | 15 --------------- package.json | 1 - scripts/build-docs.ts | 1 - 3 files changed, 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6a024e745..da8f5573cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", - "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", @@ -3909,20 +3908,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-map/-/unist-util-map-4.0.0.tgz", - "integrity": "sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-position-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", diff --git a/package.json b/package.json index 5c0faf6ca6..f9d7204f10 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", - "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.1", "vfile-reporter": "^8.0.0", diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 4882644167..4e660fddcb 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -15,7 +15,6 @@ import path from 'node:path' import remarkMdx from 'remark-mdx' import { remark } from 'remark' import { visit as mdastVisit } from 'unist-util-visit' -import { map as mdastMap } from 'unist-util-map' import remarkFrontmatter from 'remark-frontmatter' import yaml from 'yaml' import { slugifyWithCounter } from '@sindresorhus/slugify' From e7a14259e78783644958c3159f2f5beb414cb7c2 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 11 Mar 2025 03:53:02 +0800 Subject: [PATCH 071/114] =?UTF-8?q?=E2=9C=A8=20prettier=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/build-docs.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 4e660fddcb..c672991842 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -454,7 +454,8 @@ const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile } const parseInMarkdownFile = - (config: BuildConfig) => async (href: string, partials: { path: string; content: string; node: Node }[], inManifest: boolean) => { + (config: BuildConfig) => + async (href: string, partials: { path: string; content: string; node: Node }[], inManifest: boolean) => { const readFile = readMarkdownFile(config) const [error, fileContent] = await readFile(`${href}.mdx`.replace('/docs/', '')) @@ -770,7 +771,7 @@ export const build = async (config: BuildConfig) => { }) }) .process(partial.vfile) - }) + }), ) console.info(`✔️ Validated all partials`) From 1ed5a37c95f571e8a00099b9f89390fd3d9bc379 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 12 Mar 2025 00:53:05 +0800 Subject: [PATCH 072/114] fix logs --- scripts/build-docs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index c7f82c9b7d..e3354f12b2 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -861,7 +861,7 @@ export const build = async (store: ReturnType, config: const doc = docsMap.get(url) if (doc === undefined) { - vfile.message(`(Partials) Doc ${url} not found`, node.position) + vfile.message(`Doc ${url} not found`, node.position) return node } @@ -924,7 +924,7 @@ export const build = async (store: ReturnType, config: const doc = docsMap.get(url) if (doc === undefined) { - vfile.message(`(Core Docs) Doc ${url} not found`, node.position) + vfile.message(`Doc ${url} not found`, node.position) return node } From c0e8c737ac25eb65d3552827acebd8dcb9e1749d Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 12 Mar 2025 01:00:30 +0800 Subject: [PATCH 073/114] Remove .mdx file extension from links with a hash --- scripts/build-docs.test.ts | 8 +++++++- scripts/build-docs.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 133a3e34c6..8e3656aefe 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -1523,7 +1523,9 @@ title: Source Page # Source Page [Link to Target with .mdx](/docs/target-page.mdx) -[Link to Target without .mdx](/docs/target-page)`, +[Link to Target without .mdx](/docs/target-page) +[Link to Target with hash](/docs/target-page#target-page-content) +[Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, }, { path: './docs/target-page.mdx', @@ -1550,6 +1552,10 @@ title: Target Page // The link should have .mdx removed expect(sourcePageContent).toContain('[Link to Target with .mdx](/docs/target-page)') expect(sourcePageContent).toContain('[Link to Target without .mdx](/docs/target-page)') + expect(sourcePageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') + expect(sourcePageContent).toContain( + '[Link to Target with hash and .mdx](/docs/target-page#target-page-content)', + ) expect(sourcePageContent).not.toContain('/docs/target-page.mdx') }) }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index e3354f12b2..ac1ae1f909 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -324,9 +324,21 @@ const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, } const removeMdxSuffix = (filePath: string) => { + + if (filePath.includes('#')) { + const [url, hash] = filePath.split('#') + + if (url.endsWith('.mdx')) { + return url.slice(0, -4) + `#${hash}` + } + + return url + `#${hash}` + } + if (filePath.endsWith('.mdx')) { return filePath.slice(0, -4) } + return filePath } From febb305e5b5047c8fb285c2a7168b2beaaf70a06 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 12 Mar 2025 01:33:34 +0800 Subject: [PATCH 074/114] ensure consistent removal of .mdx from core, scoped and partials --- scripts/build-docs.test.ts | 238 ++++++++++++++++++++++++++++++------- scripts/build-docs.ts | 42 ++++--- 2 files changed, 220 insertions(+), 60 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 8e3656aefe..6025c63f74 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -554,6 +554,43 @@ title: Simple Test expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toContain('Test Partial Content') }) + test(' Component embeds content in to sdk scoped guide', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/_partials/test-partial.mdx', + content: `Test Partial Content`, + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react +--- + + + +# Simple Test Page`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('Test Partial Content') + }) + test('Invalid partial src fails the build', async () => { const { tempDir } = await createTempFiles([ { @@ -1501,63 +1538,182 @@ title: React Doc ) }) - test('should remove .mdx suffix from markdown links', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Source Page', href: '/docs/source-page' }, - { title: 'Target Page', href: '/docs/target-page' }, - ], +test('should remove .mdx suffix from links in standard pages', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Target Page', href: '/docs/target-page' }, + { title: 'Standard Page', href: '/docs/standard-page' }, ], - }), - }, - { - path: './docs/source-page.mdx', - content: `--- -title: Source Page + ], + }), + }, + { + path: './docs/target-page.mdx', + content: `--- +title: Target Page --- -# Source Page +# Target Page Content`, + }, + { + path: './docs/standard-page.mdx', + content: `--- +title: Standard Page +--- + +# Standard Page [Link to Target with .mdx](/docs/target-page.mdx) [Link to Target without .mdx](/docs/target-page) [Link to Target with hash](/docs/target-page#target-page-content) [Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, - }, - { - path: './docs/target-page.mdx', - content: `--- + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + // links should be processed to remove .mdx + const standardPageContent = await readFile(pathJoin('./dist/standard-page.mdx')) + expect(standardPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') + expect(standardPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') + expect(standardPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') + expect(standardPageContent).toContain( + '[Link to Target with hash and .mdx](/docs/target-page#target-page-content)', + ) + expect(standardPageContent).not.toContain('/docs/target-page.mdx') +}) + +test('should remove .mdx suffix from links in pages with partials', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Target Page', href: '/docs/target-page' }, + { title: 'Partials Page', href: '/docs/partials-page' }, + ], + ], + }), + }, + { + path: './docs/target-page.mdx', + content: `--- title: Target Page --- # Target Page Content`, - }, - ]) + }, + { + path: "./docs/_partials/links.mdx", + content: `--- +title: Links +--- - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], +[Link to Target with .mdx](/docs/target-page.mdx) +[Link to Target without .mdx](/docs/target-page) +[Link to Target with hash](/docs/target-page#target-page-content) +[Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, + }, + { + path: './docs/partials-page.mdx', + content: `--- +title: Partials Page +--- + +`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + // Partials should be processed to remove .mdx + const partialsPageContent = await readFile(pathJoin('./dist/partials-page.mdx')) + expect(partialsPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') + expect(partialsPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') + expect(partialsPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') + expect(partialsPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') + expect(partialsPageContent).not.toContain('/docs/target-page.mdx') +}) + +test('should remove .mdx suffix from links in scoped pages', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Target Page', href: '/docs/target-page' }, + { title: 'Scoped Page', href: '/docs/scoped-page' }, + ], + ], }), - ) + }, + { + path: './docs/target-page.mdx', + content: `--- +title: Target Page +--- - // Both links should be processed to remove .mdx - const sourcePageContent = await readFile(pathJoin('./dist/source-page.mdx')) +# Target Page Content`, + }, + { + path: "./docs/_partials/links.mdx", + content: `--- +title: Links +--- - // The link should have .mdx removed - expect(sourcePageContent).toContain('[Link to Target with .mdx](/docs/target-page)') - expect(sourcePageContent).toContain('[Link to Target without .mdx](/docs/target-page)') - expect(sourcePageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') - expect(sourcePageContent).toContain( - '[Link to Target with hash and .mdx](/docs/target-page#target-page-content)', - ) - expect(sourcePageContent).not.toContain('/docs/target-page.mdx') - }) +[Link to Target with .mdx](/docs/target-page.mdx) +[Link to Target without .mdx](/docs/target-page) +[Link to Target with hash](/docs/target-page#target-page-content) +[Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, + }, + { + path: './docs/scoped-page.mdx', + content: `--- +title: Scoped Page +sdk: expo +--- + +`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['expo'], + }), + ) + + // Scoped page should be processed to remove .mdx + const scopedPageContent = await readFile(pathJoin('./dist/expo/scoped-page.mdx')) + expect(scopedPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') + expect(scopedPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') + expect(scopedPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') + expect(scopedPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') + expect(scopedPageContent).not.toContain('/docs/target-page.mdx') +}) }) describe('Edge Cases', () => { diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index ac1ae1f909..ac6fa843d7 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -257,6 +257,7 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => let partialNode: Node | null = null const partialContentVFile = await markdownProcessor() + .use(() => tree => { partialNode = tree }) .use(() => (tree, vfile) => { mdastVisit( tree, @@ -268,8 +269,6 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) }, ) - - partialNode = tree }) .process({ path: `docs/_partials/${markdownPath}`, @@ -540,6 +539,7 @@ const parseInMarkdownFile = const headingsHashs: Array = [] const vfile = await markdownProcessor() + // Some validation .use(() => (tree, vfile) => { if (inManifest === false) { vfile.message( @@ -551,6 +551,7 @@ const parseInMarkdownFile = vfile.fail(`Href "${href}" contains characters that will be encoded by the browser, please remove them`) } }) + // Pull out the frontmatter .use(() => (tree, vfile) => { mdastVisit( tree, @@ -916,6 +917,22 @@ export const build = async (store: ReturnType, config: const coreVFiles = await Promise.all( docsArray.map(async (doc) => { const vfile = await markdownProcessor() + // embed the partials into the doc + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + const partialSrc = extractComponentPropValueFromNode(node, vfile, 'Include', 'src') + + if (partialSrc === undefined) return node + + const partial = partials.find( + (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, + ) + + if (partial === undefined) return node // a warning will have already been reported + + return Object.assign(node, partial.node) + }) + }) // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component .use(() => (tree: Node, vfile: VFile) => { return mdastMap(tree, (node) => { @@ -1018,22 +1035,6 @@ export const build = async (store: ReturnType, config: }) }) }) - // embed the partials into the doc - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode(node, vfile, 'Include', 'src') - - if (partialSrc === undefined) return node - - const partial = partials.find( - (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, - ) - - if (partial === undefined) return node // a warning will have already been reported - - return Object.assign(node, partial.node) - }) - }) .process(doc.vfile) const distFilePath = `${doc.href.replace('/docs/', '')}.mdx` @@ -1110,7 +1111,10 @@ export const build = async (store: ReturnType, config: return node } - const [url, hash] = node.url.split('#') + // we are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + const [url, hash] = (node.url as string).split('#') const doc = docsMap.get(url) From 6815773a7a432e57f00c089cdefd6733e787ac0e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 14 Mar 2025 02:00:32 +0800 Subject: [PATCH 075/114] use consistent props naming --- scripts/build-docs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index ac6fa843d7..bc0d9a9398 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1051,7 +1051,7 @@ export const build = async (store: ReturnType, config: await writeFile( distFilePath, // It's possible we will want to / need to put some frontmatter here - ``, + ``, ) return vfile From 75b3c114527fe8aa2313e12c04085899d204ca69 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 14 Mar 2025 02:09:01 +0800 Subject: [PATCH 076/114] Fix up the tests --- scripts/build-docs.test.ts | 5 +++-- scripts/build-docs.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 6025c63f74..10eaa42bf7 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -300,7 +300,7 @@ sdk: react Testing with a simple page.`) expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe( - ``, + ``, ) const distFiles = await treeDir(pathJoin('./dist')) @@ -2071,6 +2071,7 @@ This is a normal document.`, path: './docs/sdk-document.mdx', content: `--- title: SDK Document +description: This document is available for React and Next.js. sdk: react, nextjs --- @@ -2099,7 +2100,7 @@ This document is available for React and Next.js.`, // Verify landing page content const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')) expect(landingPage).toBe( - '', + ``, ) }) }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index bc0d9a9398..69605aaf03 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1051,7 +1051,7 @@ export const build = async (store: ReturnType, config: await writeFile( distFilePath, // It's possible we will want to / need to put some frontmatter here - ``, + ``, ) return vfile From 2d0e80c61f4b89e154948b66e722b94e50b1c729 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 14 Mar 2025 05:13:39 +0800 Subject: [PATCH 077/114] Don't scope doc from manifest sdk filtering, only on frontmatter sdk labeling --- scripts/build-docs.test.ts | 299 +++++++++++++++++++++---------------- scripts/build-docs.ts | 27 ++-- 2 files changed, 187 insertions(+), 139 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 10eaa42bf7..b164324af1 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -236,12 +236,12 @@ title: Quickstart { title: 'React', sdk: ['react'], - items: [[{ title: 'Quickstart', href: '/docs/:sdk:/quickstart/react', sdk: ['react'] }]], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/react', sdk: ['react'] }]], }, { title: 'Vue', sdk: ['vue'], - items: [[{ title: 'Quickstart', href: '/docs/:sdk:/quickstart/vue', sdk: ['vue'] }]], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue', sdk: ['vue'] }]], }, ], ], @@ -960,6 +960,55 @@ Content for React users.`, }), ) + expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + navigation: [ + [ + { + title: 'Top Level', + sdk: ['react', 'nextjs'], + items: [ + [ + { + title: 'Mid Level', + sdk: ['react', 'nextjs'], + items: [ + [ + { + title: 'Deep Level', + sdk: ['nextjs'], + items: [ + [ + { + href: '/docs/:sdk:/deeply-nested-nextjs', + sdk: ['nextjs'], + title: 'Deeply Nested Page', + }, + ], + ], + }, + { + title: 'Deep Level', + sdk: ['react'], + items: [ + [ + { + title: 'Deeply Nested Page', + sdk: ['react'], + href: '/docs/:sdk:/deeply-nested-react', + }, + ], + ], + }, + ], + ], + }, + ], + ], + }, + ], + ], + }) + // Page should be available in nextjs (from manifest deep nesting) expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toBe(true) expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-react.mdx'))).toBe(false) @@ -1344,11 +1393,11 @@ describe('Manifest Handling', () => { sdk: ['nextjs', 'react'], items: [ [ - { title: 'SDK Item', sdk: ['nextjs', 'react'], href: '/docs/:sdk:/sdk-item' }, + { title: 'SDK Item', sdk: ['nextjs', 'react'], href: '/docs/sdk-item' }, { title: 'Nested Group', sdk: ['nextjs', 'react'], - items: [[{ title: 'Nested Item', sdk: ['nextjs', 'react'], href: '/docs/:sdk:/nested-item' }]], + items: [[{ title: 'Nested Item', sdk: ['nextjs', 'react'], href: '/docs/nested-item' }]], }, ], ], @@ -1375,7 +1424,7 @@ describe('Manifest Handling', () => { { title: 'Sub Group', sdk: ['vue'], - items: [[{ title: 'Vue Item', sdk: ['vue'], href: '/docs/:sdk:/vue-item' }]], + items: [[{ title: 'Vue Item', sdk: ['vue'], href: '/docs/vue-item' }]], }, ], ], @@ -1538,30 +1587,30 @@ title: React Doc ) }) -test('should remove .mdx suffix from links in standard pages', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Target Page', href: '/docs/target-page' }, - { title: 'Standard Page', href: '/docs/standard-page' }, + test('should remove .mdx suffix from links in standard pages', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Target Page', href: '/docs/target-page' }, + { title: 'Standard Page', href: '/docs/standard-page' }, + ], ], - ], - }), - }, - { - path: './docs/target-page.mdx', - content: `--- + }), + }, + { + path: './docs/target-page.mdx', + content: `--- title: Target Page --- # Target Page Content`, - }, - { - path: './docs/standard-page.mdx', - content: `--- + }, + { + path: './docs/standard-page.mdx', + content: `--- title: Standard Page --- @@ -1571,53 +1620,51 @@ title: Standard Page [Link to Target without .mdx](/docs/target-page) [Link to Target with hash](/docs/target-page#target-page-content) [Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, - }, - ]) + }, + ]) - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) - // links should be processed to remove .mdx - const standardPageContent = await readFile(pathJoin('./dist/standard-page.mdx')) - expect(standardPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') - expect(standardPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') - expect(standardPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') - expect(standardPageContent).toContain( - '[Link to Target with hash and .mdx](/docs/target-page#target-page-content)', - ) - expect(standardPageContent).not.toContain('/docs/target-page.mdx') -}) + // links should be processed to remove .mdx + const standardPageContent = await readFile(pathJoin('./dist/standard-page.mdx')) + expect(standardPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') + expect(standardPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') + expect(standardPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') + expect(standardPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') + expect(standardPageContent).not.toContain('/docs/target-page.mdx') + }) -test('should remove .mdx suffix from links in pages with partials', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Target Page', href: '/docs/target-page' }, - { title: 'Partials Page', href: '/docs/partials-page' }, + test('should remove .mdx suffix from links in pages with partials', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Target Page', href: '/docs/target-page' }, + { title: 'Partials Page', href: '/docs/partials-page' }, + ], ], - ], - }), - }, - { - path: './docs/target-page.mdx', - content: `--- + }), + }, + { + path: './docs/target-page.mdx', + content: `--- title: Target Page --- # Target Page Content`, - }, - { - path: "./docs/_partials/links.mdx", - content: `--- + }, + { + path: './docs/_partials/links.mdx', + content: `--- title: Links --- @@ -1625,59 +1672,59 @@ title: Links [Link to Target without .mdx](/docs/target-page) [Link to Target with hash](/docs/target-page#target-page-content) [Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, - }, - { - path: './docs/partials-page.mdx', - content: `--- + }, + { + path: './docs/partials-page.mdx', + content: `--- title: Partials Page --- `, - }, - ]) + }, + ]) - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) - // Partials should be processed to remove .mdx - const partialsPageContent = await readFile(pathJoin('./dist/partials-page.mdx')) - expect(partialsPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') - expect(partialsPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') - expect(partialsPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') - expect(partialsPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') - expect(partialsPageContent).not.toContain('/docs/target-page.mdx') -}) + // Partials should be processed to remove .mdx + const partialsPageContent = await readFile(pathJoin('./dist/partials-page.mdx')) + expect(partialsPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') + expect(partialsPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') + expect(partialsPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') + expect(partialsPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') + expect(partialsPageContent).not.toContain('/docs/target-page.mdx') + }) -test('should remove .mdx suffix from links in scoped pages', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Target Page', href: '/docs/target-page' }, - { title: 'Scoped Page', href: '/docs/scoped-page' }, + test('should remove .mdx suffix from links in scoped pages', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Target Page', href: '/docs/target-page' }, + { title: 'Scoped Page', href: '/docs/scoped-page' }, + ], ], - ], - }), - }, - { - path: './docs/target-page.mdx', - content: `--- + }), + }, + { + path: './docs/target-page.mdx', + content: `--- title: Target Page --- # Target Page Content`, - }, - { - path: "./docs/_partials/links.mdx", - content: `--- + }, + { + path: './docs/_partials/links.mdx', + content: `--- title: Links --- @@ -1685,35 +1732,35 @@ title: Links [Link to Target without .mdx](/docs/target-page) [Link to Target with hash](/docs/target-page#target-page-content) [Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, - }, - { - path: './docs/scoped-page.mdx', - content: `--- + }, + { + path: './docs/scoped-page.mdx', + content: `--- title: Scoped Page sdk: expo --- `, - }, - ]) + }, + ]) - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['expo'], - }), - ) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['expo'], + }), + ) - // Scoped page should be processed to remove .mdx - const scopedPageContent = await readFile(pathJoin('./dist/expo/scoped-page.mdx')) - expect(scopedPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') - expect(scopedPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') - expect(scopedPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') - expect(scopedPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') - expect(scopedPageContent).not.toContain('/docs/target-page.mdx') -}) + // Scoped page should be processed to remove .mdx + const scopedPageContent = await readFile(pathJoin('./dist/expo/scoped-page.mdx')) + expect(scopedPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') + expect(scopedPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') + expect(scopedPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') + expect(scopedPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') + expect(scopedPageContent).not.toContain('/docs/target-page.mdx') + }) }) describe('Edge Cases', () => { diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 69605aaf03..49e9679b80 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -257,7 +257,9 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => let partialNode: Node | null = null const partialContentVFile = await markdownProcessor() - .use(() => tree => { partialNode = tree }) + .use(() => (tree) => { + partialNode = tree + }) .use(() => (tree, vfile) => { mdastVisit( tree, @@ -323,7 +325,6 @@ const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, } const removeMdxSuffix = (filePath: string) => { - if (filePath.includes('#')) { const [url, hash] = filePath.split('#') @@ -337,7 +338,7 @@ const removeMdxSuffix = (filePath: string) => { if (filePath.endsWith('.mdx')) { return filePath.slice(0, -4) } - + return filePath } @@ -743,7 +744,7 @@ export const build = async (store: ReturnType, config: throw new Error(`Doc "${item.title}" in manifest.json not found in the docs folder at ${item.href}.mdx`) } - // This is the sdk of the doc + // This is the sdk of the doc as defined in the docs frontmatter const docSDK = doc.sdk // This is the sdk of the parent group @@ -775,7 +776,7 @@ export const build = async (store: ReturnType, config: return Array.from(new Set(sdks)).filter((sdk): sdk is SDK => sdk !== undefined) })() - // This is the sdk of the group + // This is the sdk of the group as defined in the manifest.json const groupSDK = details.sdk // This is the sdk of the parent group @@ -821,19 +822,19 @@ export const build = async (store: ReturnType, config: JSON.stringify({ navigation: await traverseTree( { items: sdkScopedManifest }, - async ({ sdk, ...item }) => { + async (item) => { return { title: item.title, - href: sdk !== undefined ? scopeHrefToSDK(item.href, ':sdk:') : item.href, + href: docsMap.get(item.href)?.sdk !== undefined ? scopeHrefToSDK(item.href, ':sdk:') : item.href, tag: item.tag, wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, icon: item.icon, target: item.target, - sdk: sdk, - } as const + sdk: item.sdk, + } }, // @ts-expect-error - This traverseTree function might just be the death of me - async ({ sdk, ...group }) => { + async (group) => { return { title: group.title, collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, @@ -841,9 +842,9 @@ export const build = async (store: ReturnType, config: wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, icon: group.icon, hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, - sdk: sdk, + sdk: group.sdk, items: group.items, - } as const + } }, ), }), @@ -1051,7 +1052,7 @@ export const build = async (store: ReturnType, config: await writeFile( distFilePath, // It's possible we will want to / need to put some frontmatter here - ``, + ``, ) return vfile From b08e7601acfd107713490366032e746cc3d9fbdb Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 15 Mar 2025 00:56:59 +0800 Subject: [PATCH 078/114] attach the canonical link in the frontmatter for scoped pages --- scripts/build-docs.test.ts | 40 ++++++++++++++++++++++++++++++++++++++ scripts/build-docs.ts | 16 +++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index b164324af1..eb065dc18f 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -293,6 +293,7 @@ Testing with a simple page.`, expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- title: Simple Test sdk: react +canonical: /docs/simple-test --- # Simple Test Page @@ -1217,6 +1218,45 @@ Common content for all SDKs.`, expect(jsOutput).toContain('This content is for JavaScript Frontend users.') expect(jsOutput).not.toContain('This content is for React and Next.js users.') }) + + test('should embed canonical link in frontmatter', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'Overview', + href: '/docs/overview', + }, + ], + ], + }), + }, + { + path: './docs/overview.mdx', + content: `--- +title: Overview +sdk: fastify, expressjs +--- + +# Hello World`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['fastify', 'expressjs'], + }), + ) + + expect(await readFile(pathJoin('./dist/fastify/overview.mdx'))).toContain('canonical: /docs/overview') + expect(await readFile(pathJoin('./dist/expressjs/overview.mdx'))).toContain('canonical: /docs/overview') + }) }) describe('Manifest Handling', () => { diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 49e9679b80..7652a53e75 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1129,6 +1129,22 @@ export const build = async (store: ReturnType, config: return node }) }) + // Insert the canonical link into the doc frontmatter + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'yaml') return node + if (!('value' in node)) return node + if (typeof node.value !== 'string') return node + + const frontmatter = yaml.parse(node.value) + + frontmatter.canonical = doc.href + + node.value = yaml.stringify(frontmatter).split('\n').slice(0, -1).join('\n') + + return node + }) + }) .process({ ...doc.vfile, messages: [], // reset the messages, otherwise they will be duplicated From 8b605c0d27a7d6c0bc8850cfb20ea39af617338a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 18 Mar 2025 04:17:17 +0800 Subject: [PATCH 079/114] include :sdk: scoping in sdk frontmatter scoped docs --- scripts/build-docs.test.ts | 6 +++--- scripts/build-docs.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index eb065dc18f..9ed44c2993 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -293,7 +293,7 @@ Testing with a simple page.`, expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- title: Simple Test sdk: react -canonical: /docs/simple-test +canonical: /docs/:sdk:/simple-test --- # Simple Test Page @@ -1254,8 +1254,8 @@ sdk: fastify, expressjs }), ) - expect(await readFile(pathJoin('./dist/fastify/overview.mdx'))).toContain('canonical: /docs/overview') - expect(await readFile(pathJoin('./dist/expressjs/overview.mdx'))).toContain('canonical: /docs/overview') + expect(await readFile(pathJoin('./dist/fastify/overview.mdx'))).toContain('canonical: /docs/:sdk:/overview') + expect(await readFile(pathJoin('./dist/expressjs/overview.mdx'))).toContain('canonical: /docs/:sdk:/overview') }) }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 7652a53e75..35ca511ae1 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1138,7 +1138,7 @@ export const build = async (store: ReturnType, config: const frontmatter = yaml.parse(node.value) - frontmatter.canonical = doc.href + frontmatter.canonical = doc.sdk ? scopeHrefToSDK(doc.href, ':sdk:') : doc.href node.value = yaml.stringify(frontmatter).split('\n').slice(0, -1).join('\n') From 11e96b2dab2790b286cfcccf140598f6a1cd5027 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 19 Mar 2025 21:42:18 +0800 Subject: [PATCH 080/114] fix up build dev script --- package-lock.json | 447 +++++++++++++++++++++++++++++++++++++++--- package.json | 4 +- scripts/build-docs.ts | 34 ++-- 3 files changed, 431 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index b7df2e739b..ceec96cbbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "clerk-docs-2023", "version": "0.1.0", "devDependencies": { + "@parcel/watcher": "^2.5.1", "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", - "chokidar": "^4.0.3", "concurrently": "^8.2.2", "glob": "^11.0.1", "prettier": "^3.2.5", @@ -719,6 +719,315 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.8", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", @@ -1305,6 +1614,19 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1362,36 +1684,6 @@ "node": ">= 16" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chokidar/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1622,6 +1914,19 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -1763,6 +2068,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1850,6 +2168,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1859,6 +2187,29 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3199,6 +3550,20 @@ } ] }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/minimatch": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", @@ -3251,6 +3616,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -3844,6 +4216,19 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", diff --git a/package.json b/package.json index f9b5cdd1e4..56942654d2 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "test": "vitest --silent" }, "devDependencies": { +"@parcel/watcher": "^2.5.1", "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", - "chokidar": "^4.0.3", - "concurrently": "^8.2.2", + "concurrently": "^8.2.2", "glob": "^11.0.1", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 35ca511ae1..70e3285d68 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -36,7 +36,7 @@ import readdirp from 'readdirp' import { z } from 'zod' import { fromError } from 'zod-validation-error' import { Node } from 'unist' -import chok from 'chokidar' +import watcher from '@parcel/watcher' const VALID_SDKS = [ 'nextjs', @@ -1172,28 +1172,19 @@ export const build = async (store: ReturnType, config: } const watchAndRebuild = (store: ReturnType, config: BuildConfig) => { - const watcher = chok.watch([config.docsPath], { - alwaysStat: true, - ignored: (filePath, stats) => { - if (stats === undefined) return false - if (stats.isDirectory()) return false - - const relativePath = path.relative(config.docsPath, filePath) - - const isManifest = relativePath === 'manifest.json' - const isMarkdown = relativePath.endsWith('.mdx') - - return !(isManifest || isMarkdown) - }, - ignoreInitial: true, - }) + watcher.subscribe(config.docsPath, async (error, events) => { + if (error !== null) { + console.error(error) + return + } - watcher.on('all', async (event, filePath) => { - console.info(`File ${filePath} changed`, { event }) + events.forEach((event) => { + const href = removeMdxSuffix(`/docs/${path.relative(config.docsPath, event.path)}`) - const href = removeMdxSuffix(`/${path.relative(config.basePath, filePath)}`) + store.markdownFiles.delete(href) - store.markdownFiles.delete(href) + console.log(store.markdownFiles.keys()) + }) const output = await build(store, config) @@ -1286,7 +1277,6 @@ const main = async () => { if (output !== '') { console.info(output) - process.exit(1) } const args = process.argv.slice(2) @@ -1296,6 +1286,8 @@ const main = async () => { console.info(`Watching for changes...`) watchAndRebuild(store, config) + } else if (output !== '') { + process.exit(1) } } From c6625ae07e2ffb7684cfc947adb941ac73034ad4 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 20 Mar 2025 03:54:09 +0800 Subject: [PATCH 081/114] cache the partials --- scripts/build-docs.ts | 123 ++++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 51 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 70e3285d68..864765592a 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -241,63 +241,80 @@ const readPartialsFolder = (config: BuildConfig) => async () => { }) } -const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => { +const readPartial = (config: BuildConfig) => async (filePath: string) => { const readFile = readMarkdownFile(config) - return Promise.all( - paths.map(async (markdownPath) => { - const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, markdownPath) + const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, filePath) - const [error, content] = await readFile(fullPath) + const [error, content] = await readFile(fullPath) - if (error) { - throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) - } + if (error) { + throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) + } - let partialNode: Node | null = null + let partialNode: Node | null = null - const partialContentVFile = await markdownProcessor() - .use(() => (tree) => { - partialNode = tree - }) - .use(() => (tree, vfile) => { - mdastVisit( - tree, - (node) => - (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && - 'name' in node && - node.name === 'Include', - (node) => { - vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) - }, - ) - }) - .process({ - path: `docs/_partials/${markdownPath}`, - value: content, - }) + const partialContentVFile = await markdownProcessor() + .use(() => (tree) => { + partialNode = tree + }) + .use(() => (tree, vfile) => { + mdastVisit( + tree, + (node) => + (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && + 'name' in node && + node.name === 'Include', + (node) => { + vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) + }, + ) + }) + .process({ + path: `docs/_partials/${filePath}`, + value: content, + }) - const partialContentReport = reporter([partialContentVFile], { quiet: true }) + const partialContentReport = reporter([partialContentVFile], { quiet: true }) - if (partialContentReport !== '') { - console.error(partialContentReport) - process.exit(1) - } + if (partialContentReport !== '') { + console.error(partialContentReport) + process.exit(1) + } - if (partialNode === null) { - throw new Error(`Failed to parse the content of ${markdownPath}`) - } + if (partialNode === null) { + throw new Error(`Failed to parse the content of ${filePath}`) + } - return { - path: markdownPath, - content, - vfile: partialContentVFile, - node: partialNode as Node, - } - }), - ) + return { + path: filePath, + content, + vfile: partialContentVFile, + node: partialNode as Node, + } } +const readPartialsMarkdown = + (config: BuildConfig, store: ReturnType) => async (paths: string[]) => { + const read = readPartial(config) + + return Promise.all( + paths.map(async (markdownPath) => { + const cachedValue = store.partialsFiles.get(markdownPath) + + if (cachedValue !== undefined) { + return cachedValue + } + + const partial = await read(markdownPath) + + store.partialsFiles.set(markdownPath, partial) + + return partial + }), + ) + } + const markdownProcessor = remark().use(remarkFrontmatter).use(remarkMdx).freeze() type VFile = Awaited> @@ -661,6 +678,7 @@ const parseInMarkdownFile = export const createBlankStore = () => ({ markdownFiles: new Map>>>(), + partialsFiles: new Map>>>(), }) export const build = async (store: ReturnType, config: BuildConfig) => { @@ -669,7 +687,7 @@ export const build = async (store: ReturnType, config: const getManifest = readManifest(config) const getDocsFolder = readDocsFolder(config) const getPartialsFolder = readPartialsFolder(config) - const getPartialsMarkdown = readPartialsMarkdown(config) + const getPartialsMarkdown = readPartialsMarkdown(config, store) const parseMarkdownFile = parseInMarkdownFile(config) const writeFile = writeDistFile(config) const writeSdkFile = writeSDKFile(config) @@ -1179,15 +1197,18 @@ const watchAndRebuild = (store: ReturnType, config: Bui } events.forEach((event) => { - const href = removeMdxSuffix(`/docs/${path.relative(config.docsPath, event.path)}`) - - store.markdownFiles.delete(href) - - console.log(store.markdownFiles.keys()) + store.markdownFiles.delete(removeMdxSuffix(`/docs/${path.relative(config.docsPath, event.path)}`)) + store.partialsFiles.delete(path.relative(config.partialsPath, event.path)) }) + const now = performance.now() + const output = await build(store, config) + const after = performance.now() + + console.log(`Rebuilt docs in ${after - now} milliseconds`) + if (output !== '') { console.info(output) } From c5e48720a88b37ede5fdfe46757e4b78d435a133 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Sat, 22 Mar 2025 00:46:44 +0800 Subject: [PATCH 082/114] don't crash out when in dev mode on a validation error --- scripts/build-docs.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 864765592a..bf348e6a4f 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1201,16 +1201,22 @@ const watchAndRebuild = (store: ReturnType, config: Bui store.partialsFiles.delete(path.relative(config.partialsPath, event.path)) }) - const now = performance.now() + try { + const now = performance.now() - const output = await build(store, config) + const output = await build(store, config) - const after = performance.now() + const after = performance.now() - console.log(`Rebuilt docs in ${after - now} milliseconds`) + console.log(`Rebuilt docs in ${after - now} milliseconds`) - if (output !== '') { - console.info(output) + if (output !== '') { + console.info(output) + } + } catch (error) { + console.error(error) + + return } }) } From 79484dd1007a84610b1ab0acdef9e67292908d92 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 26 Mar 2025 21:29:15 +0800 Subject: [PATCH 083/114] better (and type safe) heading id grabbing --- scripts/build-docs.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index bf348e6a4f..53ec02d6b0 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -645,9 +645,27 @@ const parseInMarkdownFile = // eg # test {{ id: 'my-heading' }} // This is for remapping the hash to the custom id const id = node?.children - ?.find((child) => child?.type === 'mdxTextExpression') - ?.data?.estree?.body?.find((child) => child?.type === 'ExpressionStatement') - ?.expression?.properties?.find((prop) => prop?.key?.name === 'id')?.value?.value as string | undefined + ?.find( + (child: unknown) => + typeof child === 'object' && child !== null && 'type' in child && child?.type === 'mdxTextExpression', + ) + ?.data?.estree?.body?.find( + (child: unknown) => + typeof child === 'object' && + child !== null && + 'type' in child && + child?.type === 'ExpressionStatement', + ) + ?.expression?.properties?.find( + (prop: unknown) => + typeof prop === 'object' && + prop !== null && + 'key' in prop && + typeof prop.key === 'object' && + prop.key !== null && + 'name' in prop.key && + prop.key.name === 'id', + )?.value?.value as string | undefined if (id !== undefined) { headingsHashs.push(id) From 9b4942966845cbaa76a729c6b2e1600e656efd8b Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 26 Mar 2025 23:44:41 +0800 Subject: [PATCH 084/114] fix weird package.json styling --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 56942654d2..43d2808b6b 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "test": "vitest --silent" }, "devDependencies": { -"@parcel/watcher": "^2.5.1", + "@parcel/watcher": "^2.5.1", "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", - "concurrently": "^8.2.2", + "concurrently": "^8.2.2", "glob": "^11.0.1", "prettier": "^3.2.5", "prettier-plugin-astro": "^0.14.0", From a8851151463187555cbac5d98a2bece6ac29ce08 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 27 Mar 2025 00:24:36 +0800 Subject: [PATCH 085/114] update contributing guide to document sdk scoping --- CONTRIBUTING.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4633e33b60..8e46d34dcb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -156,6 +156,13 @@ type LinkItem = { * Set to "_blank" to open link in a new tab */ target?: '_blank' + + /** + * Limit this page to only show when the user has one of the specified sdks active + * + * @example ['nextjs', 'react'] + */ + sdk?: string[] } type SubNavItem = { /** @@ -195,6 +202,13 @@ type SubNavItem = { * @default false */ collapse?: boolean + + /** + * Limit this group to only show when the user has one of the specified sdks active + * + * @example ['nextjs', 'react'] + */ + sdk?: string[] } ``` @@ -280,6 +294,25 @@ You may also set `search` to a boolean value, which acts as an `exclude` value. +#### SDK + +the `sdk` frontmatter is the best way to define what sdks a page supports. If you are writing documentation that only works under certain sdks, setting this value will indicate to the docs to only make the page available when the reader has one of the specified sdks available. + +```diff + --- + title: ++ sdk: nextjs, react + --- +``` + +This does a couple things: + +- The pages url gets generated out, say the above page is at `/docs/clerk-provider.mdx` then `/docs/nextjs/clerk-provider` and `/docs/expo/clerk-provider` will be generated. + - The base url `/docs/clerk-provider` will still exist, but will show a grid of the available variants. +- The page will only show up in the sidebar navigation if the reader has one of the specified sdks active. +- Links to this page will be 'smart' and point the user towards the correct variant of the page. +- A variant selector will be shown in the top right of the page, allowing the user to switch between the different variants of the page. + ### Headings Headings should be nested by their rank. Headings with an equal or higher rank start a new section, headings with a lower rank start new subsections that are part of the higher ranked section. Please see the [Web Accessibility Initiative documentation](https://www.w3.org/WAI/tutorials/page-structure/headings/) for more information. From 2b55b30277dd9ce0e87d8ad7225fe5a428fdedf4 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 8 Apr 2025 10:30:28 -0700 Subject: [PATCH 086/114] Merge validation and build scripts --- scripts/build-docs.test.ts | 449 +++++++++++++++++++++++++++++++++++++ scripts/build-docs.ts | 301 ++++++++++++++++++------- 2 files changed, 670 insertions(+), 80 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 9ed44c2993..9d55df0607 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -121,6 +121,7 @@ const baseConfig = { partialsPath: '../docs/_partials', distPath: '../dist', ignorePaths: ['/docs/_partials'], + ignoreWarnings: {}, manifestOptions: { wrapDefault: true, collapseDefault: false, @@ -179,6 +180,39 @@ Testing with a simple page.`) ) }) +test('Warning on missing description in frontmatter', async () => { + // Create temp environment with minimal files array + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +# Simple Test Page + +Testing with a simple page.`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react'], + }), + ) + + expect(output).toContain('warning Frontmatter should have a "description" property') +}) + test('Two Docs, each grouped by a different SDK', async () => { // Create temp environment with minimal files array const { tempDir, pathJoin } = await createTempFiles([ @@ -2191,3 +2225,418 @@ This document is available for React and Next.js.`, ) }) }) + +describe('configuration', () => { + describe('ignoreWarnings', () => { + test('Should ignore certain warnings for a file when set', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[]], + }), + }, + { + path: './docs/index.mdx', + content: `--- +title: Index +description: This page has a description +--- + +# Page exists but not in manifest`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignoreWarnings: { + '/docs/index.mdx': ['doc-not-in-manifest'], + }, + }), + ) + + expect(output).not.toContain( + 'This doc is not in the manifest.json, but will still be publicly accessible and other docs can link to it', + ) + expect(output).toBe('') + }) + + test('Should ignore multiple warnings for a single file', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[]], + }), + }, + { + path: './docs/problem-file.mdx', + content: `--- +title: Problem File +description: This page has a description +--- + +# Test Page + +[Missing Link](/docs/non-existent) + + + This uses an invalid SDK + +`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignoreWarnings: { + '/docs/problem-file.mdx': ['doc-not-in-manifest', 'link-doc-not-found', 'invalid-sdk-in-if'], + }, + }), + ) + + expect(output).not.toContain('This doc is not in the manifest.json') + expect(output).not.toContain('Doc /docs/non-existent not found') + expect(output).not.toContain('sdk "invalid-sdk" in is not a valid SDK') + expect(output).toBe('') + }) + + test('Should ignore the same warning for multiple files', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[]], + }), + }, + { + path: './docs/file1.mdx', + content: `--- +title: File 1 +description: This page has a description +--- + +[Missing Link](/docs/non-existent)`, + }, + { + path: './docs/file2.mdx', + content: `--- +title: File 2 +description: This page has a description +--- + +[Another Missing Link](/docs/another-non-existent)`, + }, + ]) + + // Should complete without the ignored warnings + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignoreWarnings: { + '/docs/file1.mdx': ['doc-not-in-manifest', 'link-doc-not-found'], + '/docs/file2.mdx': ['doc-not-in-manifest', 'link-doc-not-found'], + }, + }), + ) + + // Check that warnings are suppressed for both files + expect(output).not.toContain('Doc /docs/non-existent not found') + expect(output).not.toContain('Doc /docs/another-non-existent not found') + expect(output).toBe('') + }) + + test('Should only ignore specified warnings, leaving others intact', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'Partial Ignore', + href: '/docs/partial-ignore', + }, + ], + ], + }), + }, + { + path: './docs/partial-ignore.mdx', + content: `--- +title: Partial Ignore +description: This page has a description +--- + +[Missing Link](/docs/non-existent) + + + This uses an invalid SDK + +`, + }, + ]) + + // Only ignore the link warning, but leave SDK warning + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignoreWarnings: { + '/docs/partial-ignore.mdx': ['link-doc-not-found'], + }, + }), + ) + + expect(output).not.toContain( + 'This doc is not in the manifest.json, but will still be publicly accessible and other docs can link to it', + ) + + // Link warning should be suppressed + expect(output).not.toContain('Doc /docs/non-existent not found') + + // But SDK warning should still appear + expect(output).toContain('sdk "invalid-sdk" in is not a valid SDK') + }) + + test('Should handle ignoring warnings for component attribute validation', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[]], + }), + }, + { + path: './docs/component-issues.mdx', + content: `--- +title: Component Issues +description: This page has a description +--- + + + +`, + }, + ]) + + // Ignore component attribute warnings + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignoreWarnings: { + '/docs/component-issues.mdx': [ + 'doc-not-in-manifest', + 'component-missing-attribute', + 'include-src-not-partials', + ], + }, + }), + ) + + // Component warnings should be suppressed + expect(output).not.toContain(' component has no "src" attribute') + expect(output).not.toContain(' prop "src" must start with "_partials/"') + expect(output).toBe('') + }) + + test('Should ignore frontmatter description warning', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Missing Description', href: '/docs/missing-description' }]], + }), + }, + { + path: './docs/missing-description.mdx', + content: `--- +title: Missing Description +--- + +# This page is missing a description +`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignoreWarnings: { + '/docs/missing-description.mdx': ['frontmatter-missing-description'], + }, + }), + ) + + expect(output).not.toContain('Frontmatter should have a "description" property') + expect(output).toBe('') + }) + + test('Should ignore link hash warnings', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Source Page', href: '/docs/source-page' }, + { title: 'Target Page', href: '/docs/target-page' }, + ], + ], + }), + }, + { + path: './docs/source-page.mdx', + content: `--- +title: Source Page +description: A page with links to another page +--- + +[Link with invalid hash](/docs/target-page#non-existent-section) +`, + }, + { + path: './docs/target-page.mdx', + content: `--- +title: Target Page +description: The page being linked to +--- + +# Target Page +`, + }, + ]) + + // Ignore hash warnings + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignoreWarnings: { + '/docs/source-page.mdx': ['link-hash-not-found'], + }, + }), + ) + + // Hash warning should be suppressed + expect(output).not.toContain('Hash "non-existent-section" not found in /docs/target-page') + expect(output).toBe('') + }) + + test('Should allow non-fatal errors to be ignored for specific paths', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'SDK Group', + sdk: ['react'], + items: [ + [ + { + title: 'SDK Doc', + href: '/docs/sdk-doc', + sdk: ['react', 'nodejs'], // nodejs not in parent + }, + ], + ], + }, + ], + ], + }), + }, + { + path: './docs/sdk-doc.mdx', + content: `--- +title: SDK Doc +sdk: react, nodejs +description: This page has a description +--- + +# SDK Document +`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nodejs'], + ignoreWarnings: { + '/docs/sdk-doc.mdx': ['doc-sdk-filtered-by-parent'], + }, + }), + ) + + expect(output).toBe('') + }) + + test('Should respect ignoreWarnings in partials validation', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Test Page', href: '/docs/test-page' }]], + }), + }, + { + path: './docs/_partials/test-partial.mdx', + content: `[Missing Link](/docs/non-existent)`, + }, + { + path: './docs/test-page.mdx', + content: `--- +title: Test Page +description: Test page with partial +--- + + + +# Test Page`, + }, + ]) + + // Ignore link warnings in partials + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + ignoreWarnings: { + '/docs/_partials/test-partial.mdx': ['link-doc-not-found'], + }, + }), + ) + + // Link warning in partial should be suppressed + expect(output).not.toContain('Doc /docs/non-existent not found') + expect(output).toBe('') + }) + }) +}) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 53ec02d6b0..6e4edbca93 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -34,14 +34,120 @@ import { toString } from 'mdast-util-to-string' import reporter from 'vfile-reporter' import readdirp from 'readdirp' import { z } from 'zod' -import { fromError } from 'zod-validation-error' -import { Node } from 'unist' +import { fromError, type ValidationError } from 'zod-validation-error' +import { Node, Position } from 'unist' import watcher from '@parcel/watcher' +const errorMessages = { + // Manifest errors + 'manifest-parse-error': (error: ValidationError): string => `Failed to parse manifest: ${error}`, + + // Component errors + 'component-no-props': (componentName: string): string => `<${componentName} /> component has no props`, + 'component-attributes-not-array': (componentName: string): string => + `<${componentName} /> node attributes is not an array (this is a bug with the build script, please report)`, + 'component-missing-attribute': (componentName: string, propName: string): string => + `<${componentName} /> component has no "${propName}" attribute`, + 'component-attribute-no-value': (componentName: string, propName: string): string => + `<${componentName} /> attribute "${propName}" has no value (this is a bug with the build script, please report)`, + 'component-attribute-unsupported-type': (componentName: string, propName: string): string => + `<${componentName} /> attribute "${propName}" has an unsupported value type`, + + // SDK errors + 'invalid-sdks-in-if': (invalidSDKs: string[]): string => + `sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, + 'invalid-sdk-in-if': (sdk: string): string => `sdk "${sdk}" in is not a valid SDK`, + 'invalid-sdk-in-frontmatter': (invalidSDKs: string[], validSdks: SDK[]): string => + `Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(validSdks)}`, + 'if-component-sdk-not-in-frontmatter': (sdk: SDK, docSdk: SDK[]): string => + ` component is attempting to filter to sdk "${sdk}" but it is not available in the docs frontmatter ["${docSdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, + 'if-component-sdk-not-in-manifest': (sdk: SDK, href: string): string => + ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, + 'doc-sdk-filtered-by-parent': (title: string, docSDK: SDK[], parentSDK: SDK[]): string => + `Doc "${title}" is attempting to use ${JSON.stringify(docSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, + 'group-sdk-filtered-by-parent': (title: string, groupSDK: SDK[], parentSDK: SDK[]): string => + `Group "${title}" is attempting to use ${JSON.stringify(groupSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, + + // Document structure errors + 'doc-not-in-manifest': (): string => + 'This doc is not in the manifest.json, but will still be publicly accessible and other docs can link to it', + 'invalid-href-encoding': (href: string): string => + `Href "${href}" contains characters that will be encoded by the browser, please remove them`, + 'frontmatter-missing-title': (): string => 'Frontmatter must have a "title" property', + 'frontmatter-missing-description': (): string => 'Frontmatter should have a "description" property', + 'frontmatter-parse-failed': (href: string): string => `Frontmatter parsing failed for ${href}`, + 'doc-not-found': (title: string, href: string): string => + `Doc "${title}" in manifest.json not found in the docs folder at ${href}.mdx`, + 'sdk-path-conflict': (href: string, path: string): string => + `Doc "${href}" is attempting to write out a doc to ${path} but the first part of the path is a valid SDK, this causes a file path conflict.`, + + // Include component errors + 'include-src-not-partials': (): string => ` prop "src" must start with "_partials/"`, + 'partial-not-found': (src: string): string => `Partial /docs/${src}.mdx not found`, + 'partials-inside-partials': (): string => + 'Partials inside of partials is not yet supported (this is a bug with the build script, please report)', + + // Link validation errors + 'link-doc-not-found': (url: string): string => `Doc ${url} not found`, + 'link-hash-not-found': (hash: string, url: string): string => `Hash "${hash}" not found in ${url}`, + + // File reading errors + 'file-read-error': (filePath: string): string => `file ${filePath} doesn't exist`, + 'partial-read-error': (path: string): string => `Failed to read in ${path} from partials file`, + 'markdown-read-error': (href: string): string => `Attempting to read in ${href}.mdx failed`, + 'partial-parse-error': (path: string): string => `Failed to parse the content of ${path}`, +} as const + +type WarningCode = keyof typeof errorMessages + +// Helper function to check if a warning should be ignored +const shouldIgnoreWarning = (config: BuildConfig, filePath: string, warningCode: WarningCode): boolean => { + if (!config.ignoreWarnings) { + return false + } + + const ignoreList = config.ignoreWarnings[filePath] + if (!ignoreList) { + return false + } + + return ignoreList.includes(warningCode) +} + +const safeMessage = >( + config: BuildConfig, + vfile: VFile, + filePath: string, + warningCode: TCode, + args: TArgs, + position?: Position, +) => { + if (!shouldIgnoreWarning(config, filePath, warningCode)) { + // @ts-expect-error - TypeScript has trouble with spreading args into the function + const message = errorMessages[warningCode](...args) + vfile.message(message, position) + } +} + +const safeFail = >( + config: BuildConfig, + vfile: VFile, + filePath: string, + warningCode: TCode, + args: TArgs, + position?: Position, +) => { + if (!shouldIgnoreWarning(config, filePath, warningCode)) { + // @ts-expect-error - TypeScript has trouble with spreading args into the function + const message = errorMessages[warningCode](...args) + vfile.fail(message, position) + } +} + const VALID_SDKS = [ 'nextjs', 'react', - 'javascript-frontend', + 'js-frontend', 'chrome-extension', 'expo', 'ios', @@ -50,14 +156,14 @@ const VALID_SDKS = [ 'fastify', 'react-router', 'remix', - 'tanstack-start', + 'tanstack-react-start', 'go', 'astro', 'nuxt', 'vue', 'ruby', 'python', - 'javascript-backend', + 'js-backend', 'sdk-development', 'community-sdk', ] as const @@ -187,8 +293,6 @@ const createManifestSchema = (config: BuildConfig) => { } } -const pleaseReport = '(this is a bug with the build script, please report)' - const isValidSdk = (config: BuildConfig) => (sdk: string): sdk is SDK => { @@ -211,7 +315,7 @@ const readManifest = (config: BuildConfig) => async (): Promise => { return manifest.data } - throw new Error(`Failed to parse manifest: ${fromError(manifest.error)}`) + throw new Error(errorMessages['manifest-parse-error'](fromError(manifest.error))) } const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { @@ -221,7 +325,7 @@ const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { const fileContent = await fs.readFile(filePath, { encoding: 'utf-8' }) return [null, fileContent] as const } catch (error) { - return [new Error(`file ${filePath} doesn't exist`, { cause: error }), null] as const + return [new Error(errorMessages['file-read-error'](filePath), { cause: error }), null] as const } } @@ -249,7 +353,7 @@ const readPartial = (config: BuildConfig) => async (filePath: string) => { const [error, content] = await readFile(fullPath) if (error) { - throw new Error(`Failed to read in ${fullPath} from partials file`, { cause: error }) + throw new Error(errorMessages['partial-read-error'](fullPath), { cause: error }) } let partialNode: Node | null = null @@ -266,7 +370,7 @@ const readPartial = (config: BuildConfig) => async (filePath: string) => { 'name' in node && node.name === 'Include', (node) => { - vfile.fail(`Partials inside of partials is not yet supported, ${pleaseReport}`, node.position) + safeFail(config, vfile, fullPath, 'partials-inside-partials', [], node.position) }, ) }) @@ -283,7 +387,7 @@ const readPartial = (config: BuildConfig) => async (filePath: string) => { } if (partialNode === null) { - throw new Error(`Failed to parse the content of ${filePath}`) + throw new Error(errorMessages['partial-parse-error'](filePath)) } return { @@ -456,11 +560,13 @@ const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { } const extractComponentPropValueFromNode = ( + config: BuildConfig, node: Node, vfile: VFile | undefined, componentName: string, propName: string, required = true, + filePath: string, ): string | undefined => { // Check if it's an MDX component if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') { @@ -473,12 +579,16 @@ const extractComponentPropValueFromNode = ( // Check for attributes if (!('attributes' in node)) { - vfile?.message(`<${componentName} /> component has no props`, node.position) + if (vfile) { + safeMessage(config, vfile, filePath, 'component-no-props', [componentName], node.position) + } return undefined } if (!Array.isArray(node.attributes)) { - vfile?.message(`<${componentName} /> node attributes is not an array ${pleaseReport}`, node.position) + if (vfile) { + safeMessage(config, vfile, filePath, 'component-attributes-not-array', [componentName], node.position) + } return undefined } @@ -486,8 +596,8 @@ const extractComponentPropValueFromNode = ( const propAttribute = node.attributes.find((attribute) => attribute.name === propName) if (propAttribute === undefined) { - if (required === true) { - vfile?.message(`<${componentName} /> component has no "${propName}" attribute`, node.position) + if (required === true && vfile) { + safeMessage(config, vfile, filePath, 'component-missing-attribute', [componentName, propName], node.position) } return undefined } @@ -495,8 +605,8 @@ const extractComponentPropValueFromNode = ( const value = propAttribute.value if (value === undefined) { - if (required === true) { - vfile?.message(`<${componentName} /> attribute "${propName}" has no value ${pleaseReport}`, node.position) + if (required === true && vfile) { + safeMessage(config, vfile, filePath, 'component-attribute-no-value', [componentName, propName], node.position) } return undefined } @@ -508,30 +618,44 @@ const extractComponentPropValueFromNode = ( return value.value } - vfile?.message(`<${componentName} /> attribute "${propName}" has an unsupported value type`, node.position) + if (vfile) { + safeMessage( + config, + vfile, + filePath, + 'component-attribute-unsupported-type', + [componentName, propName], + node.position, + ) + } return undefined } -const extractSDKsFromIfProp = (config: BuildConfig) => (node: Node, vfile: VFile | undefined, sdkProp: string) => { - const isValidItem = isValidSdk(config) - const isValidItems = isValidSdks(config) +const extractSDKsFromIfProp = + (config: BuildConfig) => (node: Node, vfile: VFile | undefined, sdkProp: string, filePath: string) => { + const isValidItem = isValidSdk(config) + const isValidItems = isValidSdks(config) - if (sdkProp.includes('", "') || sdkProp.includes("', '") || sdkProp.includes('["') || sdkProp.includes('"]')) { - const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) as string[] - if (isValidItems(sdks)) { - return sdks - } else { - const invalidSDKs = sdks.filter((sdk) => !isValidItem(sdk)) - vfile?.message(`sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, node.position) - } - } else { - if (isValidItem(sdkProp)) { - return [sdkProp] + if (sdkProp.includes('", "') || sdkProp.includes("', '") || sdkProp.includes('["') || sdkProp.includes('"]')) { + const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) as string[] + if (isValidItems(sdks)) { + return sdks + } else { + const invalidSDKs = sdks.filter((sdk) => !isValidItem(sdk)) + if (vfile) { + safeMessage(config, vfile, filePath, 'invalid-sdks-in-if', [invalidSDKs], node.position) + } + } } else { - vfile?.message(`sdk "${sdkProp}" in is not a valid SDK`, node.position) + if (isValidItem(sdkProp)) { + return [sdkProp] + } else { + if (vfile) { + safeMessage(config, vfile, filePath, 'invalid-sdk-in-if', [sdkProp], node.position) + } + } } } -} const parseInMarkdownFile = (config: BuildConfig) => @@ -540,7 +664,7 @@ const parseInMarkdownFile = const [error, fileContent] = await readFile(`${href}.mdx`.replace('/docs/', '')) if (error !== null) { - throw new Error(`Attempting to read in ${href}.mdx failed, with error message: ${error.message}`, { + throw new Error(errorMessages['markdown-read-error'](href), { cause: error, }) } @@ -555,18 +679,17 @@ const parseInMarkdownFile = const slugify = slugifyWithCounter() const headingsHashs: Array = [] + const filePath = `${href}.mdx` const vfile = await markdownProcessor() // Some validation .use(() => (tree, vfile) => { if (inManifest === false) { - vfile.message( - 'This doc is not in the manifest.json, but will still be publicly accessible and other docs can link to it', - ) + safeMessage(config, vfile, filePath, 'doc-not-in-manifest', []) } if (href !== encodeURI(href)) { - vfile.fail(`Href "${href}" contains characters that will be encoded by the browser, please remove them`) + safeFail(config, vfile, filePath, 'invalid-href-encoding', [href]) } }) // Pull out the frontmatter @@ -584,20 +707,24 @@ const parseInMarkdownFile = if (frontmatterSDKs !== undefined && isValidSdks(config)(frontmatterSDKs) === false) { const invalidSDKs = frontmatterSDKs.filter((sdk) => isValidSdk(config)(sdk) === false) - vfile.fail( - `Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(config.validSdks)}`, + safeFail( + config, + vfile, + filePath, + 'invalid-sdk-in-frontmatter', + [invalidSDKs, config.validSdks as SDK[]], node.position, ) return } if (frontmatterYaml.title === undefined) { - vfile.fail(`Frontmatter must have a "title" property`, node.position) + safeFail(config, vfile, filePath, 'frontmatter-missing-title', [], node.position) return } if (frontmatterYaml.description === undefined) { - vfile.message(`Frontmatter should have a "description" property`, node.position) + safeMessage(config, vfile, filePath, 'frontmatter-missing-description', [], node.position) } frontmatter = { @@ -609,19 +736,19 @@ const parseInMarkdownFile = ) if (frontmatter === undefined) { - vfile.fail(`Frontmatter parsing failed for ${href}`) + safeFail(config, vfile, filePath, 'frontmatter-parse-failed', [href]) return } }) // Validate the .use(() => (tree, vfile) => { return mdastVisit(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode(node, vfile, 'Include', 'src') + const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) if (partialSrc === undefined) return if (partialSrc.startsWith('_partials/') === false) { - vfile.message(` prop "src" must start with "_partials/"`, node.position) + safeMessage(config, vfile, filePath, 'include-src-not-partials', [], node.position) return } @@ -630,7 +757,7 @@ const parseInMarkdownFile = ) if (partial === undefined) { - vfile.message(`Partial /docs/${removeMdxSuffix(partialSrc)}.mdx not found`, node.position) + safeMessage(config, vfile, filePath, 'partial-not-found', [removeMdxSuffix(partialSrc)], node.position) return } }) @@ -682,7 +809,7 @@ const parseInMarkdownFile = }) if (frontmatter === undefined) { - throw new Error(`Frontmatter parsing failed for ${href}`) + throw new Error(errorMessages['frontmatter-parse-failed'](href)) } return { @@ -777,10 +904,14 @@ export const build = async (store: ReturnType, config: const doc = docsMap.get(item.href) if (doc === undefined) { - throw new Error(`Doc "${item.title}" in manifest.json not found in the docs folder at ${item.href}.mdx`) + const filePath = `${item.href}.mdx` + if (!shouldIgnoreWarning(config, filePath, 'doc-not-found')) { + throw new Error(errorMessages['doc-not-found'](item.title, item.href)) + } + return item } - // This is the sdk of the doc as defined in the docs frontmatter + // This is the sdk of the doc const docSDK = doc.sdk // This is the sdk of the parent group @@ -791,9 +922,10 @@ export const build = async (store: ReturnType, config: if (docSDK !== undefined && parentSDK !== undefined) { if (docSDK.every((sdk) => parentSDK?.includes(sdk)) === false) { - throw new Error( - `Doc "${item.title}" is attempting to use ${JSON.stringify(docSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, - ) + const filePath = `${item.href}.mdx` + if (!shouldIgnoreWarning(config, filePath, 'doc-sdk-filtered-by-parent')) { + throw new Error(errorMessages['doc-sdk-filtered-by-parent'](item.title, docSDK, parentSDK)) + } } } @@ -812,7 +944,7 @@ export const build = async (store: ReturnType, config: return Array.from(new Set(sdks)).filter((sdk): sdk is SDK => sdk !== undefined) })() - // This is the sdk of the group as defined in the manifest.json + // This is the sdk of the group const groupSDK = details.sdk // This is the sdk of the parent group @@ -820,9 +952,10 @@ export const build = async (store: ReturnType, config: if (groupSDK !== undefined && parentSDK !== undefined) { if (groupSDK.every((sdk) => parentSDK?.includes(sdk)) === false) { - throw new Error( - `Group "${details.title}" is attempting to use ${JSON.stringify(groupSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, - ) + const filePath = `/docs/groups/${details.title}.mdx` + if (!shouldIgnoreWarning(config, filePath, 'group-sdk-filtered-by-parent')) { + throw new Error(errorMessages['group-sdk-filtered-by-parent'](details.title, groupSDK, parentSDK)) + } } } @@ -890,6 +1023,7 @@ export const build = async (store: ReturnType, config: const partialsVFiles = await Promise.all( partials.map(async (partial) => { + const partialPath = `docs/_partials/${partial.path}` return await markdownProcessor() // validate links in partials to docs are valid and replace the links to sdk scoped pages with the sdk link component .use(() => (tree, vfile) => { @@ -911,7 +1045,7 @@ export const build = async (store: ReturnType, config: const doc = docsMap.get(url) if (doc === undefined) { - vfile.message(`Doc ${url} not found`, node.position) + safeMessage(config, vfile, partialPath, 'link-doc-not-found', [url], node.position) return node } @@ -919,7 +1053,7 @@ export const build = async (store: ReturnType, config: const hasHash = doc.headingsHashs.includes(hash) if (hasHash === false) { - vfile.message(`Hash "${hash}" not found in ${url}`, node.position) + safeMessage(config, vfile, partialPath, 'link-hash-not-found', [hash, url], node.position) } } @@ -953,11 +1087,12 @@ export const build = async (store: ReturnType, config: const coreVFiles = await Promise.all( docsArray.map(async (doc) => { + const filePath = `${doc.href}.mdx` const vfile = await markdownProcessor() // embed the partials into the doc .use(() => (tree, vfile) => { return mdastMap(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode(node, vfile, 'Include', 'src') + const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) if (partialSrc === undefined) return node @@ -990,7 +1125,7 @@ export const build = async (store: ReturnType, config: const doc = docsMap.get(url) if (doc === undefined) { - vfile.message(`Doc ${url} not found`, node.position) + safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) return node } @@ -998,7 +1133,7 @@ export const build = async (store: ReturnType, config: const hasHash = doc.headingsHashs.includes(hash) if (hasHash === false) { - vfile.message(`Hash "${hash}" not found in ${url}`, node.position) + safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) } } @@ -1028,11 +1163,11 @@ export const build = async (store: ReturnType, config: // Validate the components .use(() => (tree, vfile) => { mdastVisit(tree, (node) => { - const sdk = extractComponentPropValueFromNode(node, vfile, 'If', 'sdk', false) + const sdk = extractComponentPropValueFromNode(config, node, vfile, 'If', 'sdk', false, filePath) if (sdk === undefined) return - const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk) + const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk, filePath) if (sdksFilter === undefined) return @@ -1050,8 +1185,12 @@ export const build = async (store: ReturnType, config: const available = doc.sdk.includes(sdk) if (available === false) { - vfile.fail( - ` component is attempting to filter to sdk "${sdk}" but it is not available in the docs frontmatter ["${doc.sdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, + safeFail( + config, + vfile, + filePath, + 'if-component-sdk-not-in-frontmatter', + [sdk, doc.sdk], node.position, ) } @@ -1063,10 +1202,7 @@ export const build = async (store: ReturnType, config: const available = availableSDKs.includes(sdk) if (available === false) { - vfile.fail( - ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${doc.href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, - node.position, - ) + safeFail(config, vfile, filePath, 'if-component-sdk-not-in-manifest', [sdk, doc.href], node.position) } })() }) @@ -1077,9 +1213,9 @@ export const build = async (store: ReturnType, config: const distFilePath = `${doc.href.replace('/docs/', '')}.mdx` if (isValidSdk(config)(distFilePath.split('/')[0])) { - throw new Error( - `Doc "${doc.href}" is attempting to write out a doc to ${distFilePath} but the first part of the path is a valid SDK, this causes a file path conflict.`, - ) + if (!shouldIgnoreWarning(config, filePath, 'sdk-path-conflict')) { + throw new Error(errorMessages['sdk-path-conflict'](doc.href, distFilePath)) + } } if (doc.sdk !== undefined) { @@ -1109,6 +1245,7 @@ export const build = async (store: ReturnType, config: if (doc.sdk === undefined) return null // skip core docs if (doc.sdk.includes(targetSdk) === false) return null // skip docs that are not for the target sdk + const filePath = `${doc.href}.mdx` const vfile = await markdownProcessor() // filter out content that is only available to other sdk's .use(() => (tree, vfile) => { @@ -1116,12 +1253,11 @@ export const build = async (store: ReturnType, config: // We aren't passing the vfile here as the as the warning // should have already been reported above when we initially // parsed the file - - const sdk = extractComponentPropValueFromNode(node, undefined, 'If', 'sdk') + const sdk = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', true, filePath) if (sdk === undefined) return true - const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk) + const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk, filePath) if (sdksFilter === undefined) return true @@ -1137,11 +1273,11 @@ export const build = async (store: ReturnType, config: return mdastMap(tree, (node) => { if (node.type !== 'link') return node if (!('url' in node)) { - vfile.fail(`Link node does not have a url property ${pleaseReport}`, node.position) + safeFail(config, vfile, filePath, 'link-doc-not-found', ['url property missing'], node.position) return node } if (typeof node.url !== 'string') { - vfile.fail(`Link node url must be a string ${pleaseReport}`, node.position) + safeFail(config, vfile, filePath, 'link-doc-not-found', ['url not a string'], node.position) return node } if (!node.url.startsWith('/docs/')) { @@ -1156,7 +1292,7 @@ export const build = async (store: ReturnType, config: const doc = docsMap.get(url) if (doc === undefined) { - vfile.fail(`(SDK Specific Docs) Doc ${url} not found`, node.position) + safeFail(config, vfile, filePath, 'link-doc-not-found', [url], node.position) return node } @@ -1247,6 +1383,7 @@ type BuildConfigOptions = { partialsPath: string distPath: string ignorePaths: string[] + ignoreWarnings?: Record manifestOptions: { wrapDefault: boolean collapseDefault: boolean @@ -1279,6 +1416,7 @@ export function createConfig(config: BuildConfigOptions) { distPath: resolve(config.distPath), ignorePaths: config.ignorePaths, + ignoreWarnings: config.ignoreWarnings || {}, manifestOptions: config.manifestOptions ?? { wrapDefault: true, collapseDefault: false, @@ -1308,6 +1446,9 @@ const main = async () => { '/changelog/2024-04-19', '/docs/_partials', ], + ignoreWarnings: { + '/docs/index.mdx': ['doc-not-in-manifest'], + }, validSdks: VALID_SDKS, manifestOptions: { wrapDefault: true, From 1673620ff295a668a6ae3391f2078b4b32424df9 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 8 Apr 2025 11:39:36 -0700 Subject: [PATCH 087/114] re-install @parcel/watcher --- package-lock.json | 55 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index de75d0c1c9..ceec96cbbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1614,6 +1614,19 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2055,6 +2068,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2174,6 +2200,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3520,8 +3556,12 @@ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, "node_modules/minimatch": { @@ -4176,6 +4216,19 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", From 2a09e2a48f82e9c92c9541256c9793f7a2433e79 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 8 Apr 2025 12:43:11 -0700 Subject: [PATCH 088/114] fix up the tests --- scripts/build-docs.test.ts | 26 ++++++++++---------- scripts/build-docs.ts | 49 +++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 9d55df0607..b419781fec 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -991,7 +991,7 @@ Content for React users.`, createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'javascript-frontend'], + validSdks: ['react', 'nextjs', 'js-frontend'], }), ) @@ -1056,9 +1056,9 @@ Content for React users.`, expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).toContain('Content for React users.') expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).not.toContain('Content for Next.js users.') - // Page should NOT be available in javascript-frontend (filtered out by manifest) - expect(await fileExists(pathJoin('./dist/javascript-frontend/deeply-nested-nextjs.mdx'))).toBe(false) - expect(await fileExists(pathJoin('./dist/javascript-frontend/deeply-nested-react.mdx'))).toBe(false) + // Page should NOT be available in js-frontend (filtered out by manifest) + expect(await fileExists(pathJoin('./dist/js-frontend/deeply-nested-nextjs.mdx'))).toBe(false) + expect(await fileExists(pathJoin('./dist/js-frontend/deeply-nested-react.mdx'))).toBe(false) }) test('should correctly process multiple blocks with different SDKs in a single document', async () => { @@ -1080,7 +1080,7 @@ Content for React users.`, path: './docs/multiple-sdk-blocks.mdx', content: `--- title: Multiple SDK Blocks -sdk: react, nextjs, javascript-frontend +sdk: react, nextjs, js-frontend --- # Multiple SDK Blocks @@ -1093,7 +1093,7 @@ sdk: react, nextjs, javascript-frontend This content is for Next.js users only. - + This content is for JavaScript Frontend users only. @@ -1106,7 +1106,7 @@ Common content for all SDKs.`, createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'javascript-frontend'], + validSdks: ['react', 'nextjs', 'js-frontend'], }), ) @@ -1127,8 +1127,8 @@ Common content for all SDKs.`, expect(nextjsContent).toContain('Common content for all SDKs.') // Check JavaScript Frontend version - expect(await fileExists(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx'))).toBe(true) - const jsContent = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-blocks.mdx')) + expect(await fileExists(pathJoin('./dist/js-frontend/multiple-sdk-blocks.mdx'))).toBe(true) + const jsContent = await readFile(pathJoin('./dist/js-frontend/multiple-sdk-blocks.mdx')) expect(jsContent).not.toContain('This content is for React users only.') expect(jsContent).not.toContain('This content is for Next.js users only.') expect(jsContent).toContain('This content is for JavaScript Frontend users only.') @@ -1211,7 +1211,7 @@ Common content for all SDKs.`, path: './docs/multiple-sdk-test.mdx', content: `--- title: Multiple SDK Test -sdk: react, nextjs, javascript-frontend +sdk: react, nextjs, js-frontend --- # Multiple SDK Test @@ -1220,7 +1220,7 @@ sdk: react, nextjs, javascript-frontend This content is for React and Next.js users. - + This content is for JavaScript Frontend users. @@ -1233,7 +1233,7 @@ Common content for all SDKs.`, createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'javascript-frontend'], + validSdks: ['react', 'nextjs', 'js-frontend'], }), ) @@ -1248,7 +1248,7 @@ Common content for all SDKs.`, expect(nextjsOutput).not.toContain('This content is for JavaScript Frontend users.') // Check JavaScript Frontend output has JavaScript Frontend content but not React/Next.js content - const jsOutput = await readFile(pathJoin('./dist/javascript-frontend/multiple-sdk-test.mdx')) + const jsOutput = await readFile(pathJoin('./dist/js-frontend/multiple-sdk-test.mdx')) expect(jsOutput).toContain('This content is for JavaScript Frontend users.') expect(jsOutput).not.toContain('This content is for React and Next.js users.') }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 6f5f8c4e9f..1c09eef01f 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -374,6 +374,21 @@ const readPartial = (config: BuildConfig) => async (filePath: string) => { }, ) }) + // Process links in partials and remove the .mdx suffix + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node + + // We are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + return node + }) + }) .process({ path: `docs/_partials/${filePath}`, value: content, @@ -1021,7 +1036,7 @@ export const build = async (store: ReturnType, config: const partialsVFiles = await Promise.all( partials.map(async (partial) => { - const partialPath = `docs/_partials/${partial.path}` + const partialPath = `/docs/_partials/${partial.path}` return await markdownProcessor() // validate links in partials to docs are valid and replace the links to sdk scoped pages with the sdk link component .use(() => (tree, vfile) => { @@ -1088,22 +1103,6 @@ export const build = async (store: ReturnType, config: docsArray.map(async (doc) => { const filePath = `${doc.href}.mdx` const vfile = await markdownProcessor() - // embed the partials into the doc - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) - - if (partialSrc === undefined) return node - - const partial = partials.find( - (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, - ) - - if (partial === undefined) return node // a warning will have already been reported - - return Object.assign(node, partial.node) - }) - }) // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component .use(() => (tree: Node, vfile: VFile) => { return mdastMap(tree, (node) => { @@ -1208,6 +1207,22 @@ export const build = async (store: ReturnType, config: }) }) }) + // embed the partials into the doc + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) + + if (partialSrc === undefined) return node + + const partial = partials.find( + (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, + ) + + if (partial === undefined) return node // a warning will have already been reported + + return Object.assign(node, partial.node) + }) + }) .process(doc.vfile) const distFilePath = `${doc.href.replace('/docs/', '')}.mdx` From 8f3b1c464cd5eaee659781e2bf258a00d24382f3 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 8 Apr 2025 17:29:46 -0700 Subject: [PATCH 089/114] use the wide template on the sdk redirect page --- scripts/build-docs.test.ts | 4 ++-- scripts/build-docs.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index b419781fec..3c82471e52 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -335,7 +335,7 @@ canonical: /docs/:sdk:/simple-test Testing with a simple page.`) expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe( - ``, + `---\ntemplate: wide\n---\n`, ) const distFiles = await treeDir(pathJoin('./dist')) @@ -2221,7 +2221,7 @@ This document is available for React and Next.js.`, // Verify landing page content const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')) expect(landingPage).toBe( - ``, + `---\ntemplate: wide\n---\n`, ) }) }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 1c09eef01f..158370953b 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1239,7 +1239,10 @@ export const build = async (store: ReturnType, config: await writeFile( distFilePath, // It's possible we will want to / need to put some frontmatter here - ``, + `--- +template: wide +--- +`, ) return vfile From 58701f8f257cb1587ce1861a9a54def2f024354a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Apr 2025 14:22:42 -0700 Subject: [PATCH 090/114] validate headings ids in and not in if components, and add some testing --- scripts/build-docs.test.ts | 289 +++++++++++++++++++++++++++++++++- scripts/build-docs.ts | 311 +++++++++++++++++++++++++++++-------- 2 files changed, 536 insertions(+), 64 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 3c82471e52..f8234ee0e8 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -4,7 +4,7 @@ import os from 'node:os' import { glob } from 'glob' import { describe, expect, onTestFinished, test } from 'vitest' -import { build, createConfig, createBlankStore } from './build-docs' +import { build, createConfig, createBlankStore, invalidateFile } from './build-docs' const tempConfig = { // Set to true to use local repo temp directory instead of system temp @@ -2640,3 +2640,290 @@ description: Test page with partial }) }) }) + +// MANIFEST VALIDATION TESTS + +test('should fail build with completely malformed manifest JSON', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: '{invalid json structure', + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +# Simple Test`, + }, + ]) + + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow('Failed to parse manifest:') +}) + +// COMPLEX HEADING SCENARIOS + +test('should error on duplicate headings', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Duplicate Headings', href: '/docs/duplicate-headings' }]], + }), + }, + { + path: './docs/duplicate-headings.mdx', + content: `--- +title: Duplicate Headings +--- + +# Heading {{ id: 'custom-id' }} + +## Another Heading {{ id: 'custom-id' }} + +[Link to first heading](#custom-id)`, + }, + ]) + + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Doc "/docs/duplicate-headings" contains a duplicate heading id "custom-id", please ensure all heading ids are unique', + ) +}) + +test('should not error on duplicate headings if they are in different components', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], + }), + }, + { + path: './docs/quickstart.mdx', + content: `--- +title: Quickstart +description: Quickstart page +sdk: react, nextjs +--- + + + # Title {{ id: 'title' }} + + + + # Title {{ id: 'title' }} +`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) + + expect(output).toBe('') +}) + +test('should error on duplicate headings if they are in different components but with the same sdk', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], + }), + }, + { + path: './docs/quickstart.mdx', + content: `--- +title: Quickstart +description: Quickstart page +sdk: react, nextjs +--- + + + # Title {{ id: 'title' }} + + + + # Title {{ id: 'title' }} +`, + }, + ]) + + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', + ) +}) + +test('should error on duplicate headings if they are in different components but with the same sdk without sdk in frontmatter', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], + }), + }, + { + path: './docs/quickstart.mdx', + content: `--- +title: Quickstart +description: Quickstart page +--- + + + # Title {{ id: 'title' }} + + + + # Title {{ id: 'title' }} +`, + }, + ]) + + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', + ) +}) + +// HANDLING NON-MDX FILES + +test('should ignore non-MDX files in the docs folder', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'MDX Doc', href: '/docs/mdx-doc' }]], + }), + }, + { + path: './docs/mdx-doc.mdx', + content: `--- +title: MDX Doc +--- + +# MDX Document`, + }, + { + path: './docs/non-mdx-file.txt', + content: `This is a text file, not an MDX file.`, + }, + { + path: './docs/image.png', + content: `fake image content`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + // Verify only MDX files were processed + expect(await fileExists(pathJoin('./dist/mdx-doc.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/non-mdx-file.txt'))).toBe(false) + expect(await fileExists(pathJoin('./dist/image.png'))).toBe(false) +}) + +// CACHE INVALIDATION TESTS + +test('should update cached files when their content changes', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Cached Doc', href: '/docs/cached-doc' }]], + }), + }, + { + path: './docs/cached-doc.mdx', + content: `--- +title: Original Title +--- + +# Original Content`, + }, + ]) + + // Create store to maintain cache across builds + const store = createBlankStore() + const config = createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }) + const invalidate = invalidateFile(store, config) + + // First build + await build(store, config) + + // Check initial content + const initialContent = await readFile(pathJoin('./dist/cached-doc.mdx')) + expect(initialContent).toContain('Original Title') + expect(initialContent).toContain('Original Content') + + // Update file content + await fs.writeFile( + pathJoin('./docs/cached-doc.mdx'), + `--- +title: Updated Title +--- + +# Updated Content`, + 'utf-8', + ) + + invalidate(pathJoin('./docs/cached-doc.mdx')) + + // Second build with same store (should detect changes) + await build(store, config) + + // Check updated content + const updatedContent = await readFile(pathJoin('./dist/cached-doc.mdx')) + expect(updatedContent).toContain('Updated Title') + expect(updatedContent).toContain('Updated Content') +}) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 158370953b..02cfb61359 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -2,22 +2,28 @@ // Validates // - The manifest -// - The markdown files contents (including frontmatter) -// - Links (including hashes) between docs are valid +// - The markdown files contents (including required frontmatter fields) +// - Links (including hashes) between docs are valid and point to existing headings // - The sdk filtering in the manifest // - The sdk filtering in the frontmatter // - The sdk filtering in the component // - Checks that the sdk is available in the manifest // - Checks that the sdk is available in the frontmatter +// - Validates sdk values against the list of valid SDKs +// - URL encoding (prevents browser encoding issues) +// - File existence for both docs and partials +// - Path conflicts (prevents SDK name conflicts in paths) -// - Embeds the includes in the markdown files +// Transforms +// - Embeds the partials in the markdown files // - Updates the links in the content if they point to the sdk specific docs +// - Converts links to SDK-specific docs to use components // - Copies over "core" docs to the dist folder // - Generates "landing" pages for the sdk specific docs at the original url -// - Generates a manifest that is specific to each SDK -// - Duplicates out the sdk specific docs to their respective folders -// - stripping filtered out content +// - Generates out the sdk specific docs to their respective folders +// - Stripping filtered out content based on SDK // - Removes .mdx from the end of docs markdown links +// - Adds canonical links in frontmatter for SDK-specific docs import fs from 'node:fs/promises' import path from 'node:path' @@ -40,7 +46,7 @@ import watcher from '@parcel/watcher' const errorMessages = { // Manifest errors - 'manifest-parse-error': (error: ValidationError): string => `Failed to parse manifest: ${error}`, + 'manifest-parse-error': (error: ValidationError | Error): string => `Failed to parse manifest: ${error}`, // Component errors 'component-no-props': (componentName: string): string => `<${componentName} /> component has no props`, @@ -78,8 +84,11 @@ const errorMessages = { 'frontmatter-parse-failed': (href: string): string => `Frontmatter parsing failed for ${href}`, 'doc-not-found': (title: string, href: string): string => `Doc "${title}" in manifest.json not found in the docs folder at ${href}.mdx`, + 'doc-parse-failed': (href: string): string => `Doc "${href}" failed to parse`, 'sdk-path-conflict': (href: string, path: string): string => `Doc "${href}" is attempting to write out a doc to ${path} but the first part of the path is a valid SDK, this causes a file path conflict.`, + 'duplicate-heading-id': (href: string, id: string): string => + `Doc "${href}" contains a duplicate heading id "${id}", please ensure all heading ids are unique`, // Include component errors 'include-src-not-partials': (): string => ` prop "src" must start with "_partials/"`, @@ -305,14 +314,30 @@ const isValidSdks = return sdks.every(isValidSdk(config)) } +const parseJSON = (json: string) => { + try { + const output = JSON.parse(json) + + return [null, output as unknown] as const + } catch (error) { + return [new Error(`Failed to parse JSON`, { cause: error }), null] as const + } +} + const readManifest = (config: BuildConfig) => async (): Promise => { const { manifestSchema } = createManifestSchema(config) const unsafe_manifest = await fs.readFile(config.manifestFilePath, { encoding: 'utf-8' }) - const manifest = await manifestSchema.safeParseAsync(JSON.parse(unsafe_manifest).navigation) + const [error, json] = parseJSON(unsafe_manifest) + + if (error) { + throw new Error(errorMessages['manifest-parse-error'](error)) + } + + const manifest = await z.object({ navigation: manifestSchema }).safeParseAsync(json) if (manifest.success === true) { - return manifest.data + return manifest.data.navigation } throw new Error(errorMessages['manifest-parse-error'](fromError(manifest.error))) @@ -574,6 +599,19 @@ const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { return `/docs/${targetSDK}/${hrefSegments.slice(2).join('/')}` } +const findComponent = (node: Node, componentName: string) => { + // Check if it's an MDX component + if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') { + return undefined + } + + // Check if it's the correct component + if (!('name' in node)) return undefined + if (node.name !== componentName) return undefined + + return node +} + const extractComponentPropValueFromNode = ( config: BuildConfig, node: Node, @@ -583,36 +621,31 @@ const extractComponentPropValueFromNode = ( required = true, filePath: string, ): string | undefined => { - // Check if it's an MDX component - if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') { - return undefined - } + const component = findComponent(node, componentName) - // Check if it's the correct component - if (!('name' in node)) return undefined - if (node.name !== componentName) return undefined + if (component === undefined) return undefined // Check for attributes - if (!('attributes' in node)) { + if (!('attributes' in component)) { if (vfile) { - safeMessage(config, vfile, filePath, 'component-no-props', [componentName], node.position) + safeMessage(config, vfile, filePath, 'component-no-props', [componentName], component.position) } return undefined } - if (!Array.isArray(node.attributes)) { + if (!Array.isArray(component.attributes)) { if (vfile) { - safeMessage(config, vfile, filePath, 'component-attributes-not-array', [componentName], node.position) + safeMessage(config, vfile, filePath, 'component-attributes-not-array', [componentName], component.position) } return undefined } // Find the requested prop - const propAttribute = node.attributes.find((attribute) => attribute.name === propName) + const propAttribute = component.attributes.find((attribute) => attribute.name === propName) if (propAttribute === undefined) { if (required === true && vfile) { - safeMessage(config, vfile, filePath, 'component-missing-attribute', [componentName, propName], node.position) + safeMessage(config, vfile, filePath, 'component-missing-attribute', [componentName, propName], component.position) } return undefined } @@ -621,7 +654,14 @@ const extractComponentPropValueFromNode = ( if (value === undefined) { if (required === true && vfile) { - safeMessage(config, vfile, filePath, 'component-attribute-no-value', [componentName, propName], node.position) + safeMessage( + config, + vfile, + filePath, + 'component-attribute-no-value', + [componentName, propName], + component.position, + ) } return undefined } @@ -640,7 +680,7 @@ const extractComponentPropValueFromNode = ( filePath, 'component-attribute-unsupported-type', [componentName, propName], - node.position, + component.position, ) } return undefined @@ -672,10 +712,55 @@ const extractSDKsFromIfProp = } } +const extractHeadingFromHeadingNode = (node: Node) => { + // eg # test {{ id: 'my-heading' }} + // This is for remapping the hash to the custom id + const id = + ('children' in node && + Array.isArray(node.children) && + (node?.children + ?.find( + (child: unknown) => + typeof child === 'object' && child !== null && 'type' in child && child?.type === 'mdxTextExpression', + ) + ?.data?.estree?.body?.find( + (child: unknown) => + typeof child === 'object' && child !== null && 'type' in child && child?.type === 'ExpressionStatement', + ) + ?.expression?.properties?.find( + (prop: unknown) => + typeof prop === 'object' && + prop !== null && + 'key' in prop && + typeof prop.key === 'object' && + prop.key !== null && + 'name' in prop.key && + prop.key.name === 'id', + )?.value?.value as string | undefined)) || + undefined + + return id +} + +const documentHasIfComponents = (tree: Node) => { + let found = false + + mdastVisit(tree, (node) => { + const ifSrc = findComponent(node, 'If') + + if (ifSrc !== undefined) { + found = true + } + }) + + return found +} + const parseInMarkdownFile = (config: BuildConfig) => async (href: string, partials: { path: string; content: string; node: Node }[], inManifest: boolean) => { const readFile = readMarkdownFile(config) + const validateSDKs = isValidSdks(config) const [error, fileContent] = await readFile(`${href}.mdx`.replace('/docs/', '')) if (error !== null) { @@ -693,11 +778,14 @@ const parseInMarkdownFile = let frontmatter: Frontmatter | undefined = undefined const slugify = slugifyWithCounter() - const headingsHashs: Array = [] + const headingsHashes = new Set() const filePath = `${href}.mdx` + let node: Node | undefined = undefined const vfile = await markdownProcessor() .use(() => (tree, vfile) => { + node = tree + if (inManifest === false) { safeMessage(config, vfile, filePath, 'doc-not-in-manifest', []) } @@ -718,7 +806,7 @@ const parseInMarkdownFile = const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') - if (frontmatterSDKs !== undefined && isValidSdks(config)(frontmatterSDKs) === false) { + if (frontmatterSDKs !== undefined && validateSDKs(frontmatterSDKs) === false) { const invalidSDKs = frontmatterSDKs.filter((sdk) => isValidSdk(config)(sdk) === false) safeFail( config, @@ -776,42 +864,29 @@ const parseInMarkdownFile = }) }) // extract out the headings to check hashes in links - .use(() => (tree) => { + .use(() => (tree, vfile) => { + const documentContainsIfComponent = documentHasIfComponents(tree) + mdastVisit( tree, (node) => node.type === 'heading', (node) => { - // @ts-expect-error - If the heading has a id in it, this will pick it up - // eg # test {{ id: 'my-heading' }} - // This is for remapping the hash to the custom id - const id = node?.children - ?.find( - (child: unknown) => - typeof child === 'object' && child !== null && 'type' in child && child?.type === 'mdxTextExpression', - ) - ?.data?.estree?.body?.find( - (child: unknown) => - typeof child === 'object' && - child !== null && - 'type' in child && - child?.type === 'ExpressionStatement', - ) - ?.expression?.properties?.find( - (prop: unknown) => - typeof prop === 'object' && - prop !== null && - 'key' in prop && - typeof prop.key === 'object' && - prop.key !== null && - 'name' in prop.key && - prop.key.name === 'id', - )?.value?.value as string | undefined + const id = extractHeadingFromHeadingNode(node) if (id !== undefined) { - headingsHashs.push(id) + if (documentContainsIfComponent === false && headingsHashes.has(id)) { + safeFail(config, vfile, filePath, 'duplicate-heading-id', [href, id]) + } + + headingsHashes.add(id) } else { const slug = slugify(toString(node).trim()) - headingsHashs.push(slug) + + if (documentContainsIfComponent === false && headingsHashes.has(slug)) { + safeFail(config, vfile, filePath, 'duplicate-heading-id', [href, slug]) + } + + headingsHashes.add(slug) } }, ) @@ -821,6 +896,10 @@ const parseInMarkdownFile = value: fileContent, }) + if (node === undefined) { + throw new Error(errorMessages['doc-parse-failed'](href)) + } + if (frontmatter === undefined) { throw new Error(errorMessages['frontmatter-parse-failed'](href)) } @@ -829,8 +908,9 @@ const parseInMarkdownFile = href, sdk: (frontmatter as Frontmatter).sdk, vfile, - headingsHashs, + headingsHashes, frontmatter: frontmatter as Frontmatter, + node: node as Node, } } @@ -1063,7 +1143,7 @@ export const build = async (store: ReturnType, config: } if (hash !== undefined) { - const hasHash = doc.headingsHashs.includes(hash) + const hasHash = doc.headingsHashes.has(hash) if (hasHash === false) { safeMessage(config, vfile, partialPath, 'link-hash-not-found', [hash, url], node.position) @@ -1102,6 +1182,7 @@ export const build = async (store: ReturnType, config: const coreVFiles = await Promise.all( docsArray.map(async (doc) => { const filePath = `${doc.href}.mdx` + const vfile = await markdownProcessor() // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component .use(() => (tree: Node, vfile: VFile) => { @@ -1128,7 +1209,7 @@ export const build = async (store: ReturnType, config: } if (hash !== undefined) { - const hasHash = doc.headingsHashs.includes(hash) + const hasHash = doc.headingsHashes.has(hash) if (hasHash === false) { safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) @@ -1286,6 +1367,35 @@ template: wide return false }) }) + // Validate unique heading ids + .use(() => (tree, vfile) => { + const headingsHashes = new Set() + const slugify = slugifyWithCounter() + + mdastVisit( + tree, + (node) => node.type === 'heading', + (node) => { + const id = extractHeadingFromHeadingNode(node) + + if (id !== undefined) { + if (headingsHashes.has(id)) { + safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, id]) + } + + headingsHashes.add(id) + } else { + const slug = slugify(toString(node).trim()) + + if (headingsHashes.has(slug)) { + safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, slug]) + } + + headingsHashes.add(slug) + } + }, + ) + }) // scope urls so they point to the current sdk .use(() => (tree, vfile) => { return mdastMap(tree, (node) => { @@ -1346,13 +1456,81 @@ template: wide }), ) + console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific docs`) + return { targetSdk, vFiles } }), ) - sdkSpecificVFiles.forEach(({ targetSdk, vFiles }) => - console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific docs`), - ) + const docsWithOnlyIfComponents = docsArray.filter((doc) => doc.sdk === undefined && documentHasIfComponents(doc.node)) + const extractSDKsFromIfComponent = extractSDKsFromIfProp(config) + + for (const doc of docsWithOnlyIfComponents) { + const filePath = `${doc.href}.mdx` + + // Extract all SDK values from all components + const availableSDKs = new Set() + + mdastVisit(doc.node, (node) => { + const sdkProp = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', true, filePath) + + if (sdkProp === undefined) return + + const sdks = extractSDKsFromIfComponent(node, undefined, sdkProp, filePath) + + if (sdks === undefined) return + + sdks.forEach((sdk) => availableSDKs.add(sdk)) + }) + + // For each SDK, check heading uniqueness after filtering + for (const sdk of availableSDKs) { + const vfile = await markdownProcessor() + .use(() => (inputTree) => { + return mdastFilter(inputTree, (node) => { + const sdkProp = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', false, filePath) + if (!sdkProp) return true + + const ifSdks = extractSDKsFromIfComponent(node, undefined, sdkProp, filePath) + if (!ifSdks) return true + + return ifSdks.includes(sdk) + }) + }) + .use(() => (inputTree, vfile) => { + const headingsHashes = new Set() + const slugify = slugifyWithCounter() + + mdastVisit( + inputTree, + (node) => node.type === 'heading', + (node) => { + const id = extractHeadingFromHeadingNode(node) + + if (id !== undefined) { + if (headingsHashes.has(id)) { + safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, id]) + } + + headingsHashes.add(id) + } else { + const slug = slugify(toString(node).trim()) + + if (headingsHashes.has(slug)) { + safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, slug]) + } + + headingsHashes.add(slug) + } + }, + ) + }) + .process({ + path: filePath, + value: String(doc.vfile), + }) + } + } const flatSdkSpecificVFiles = sdkSpecificVFiles .flatMap(({ vFiles }) => vFiles) @@ -1361,7 +1539,15 @@ template: wide return reporter([...coreVFiles, ...partialsVFiles, ...flatSdkSpecificVFiles], { quiet: true }) } +export const invalidateFile = + (store: ReturnType, config: BuildConfig) => (filePath: string) => { + store.markdownFiles.delete(removeMdxSuffix(`/docs/${path.relative(config.docsPath, filePath)}`)) + store.partialsFiles.delete(path.relative(config.partialsPath, filePath)) + } + const watchAndRebuild = (store: ReturnType, config: BuildConfig) => { + const invalidate = invalidateFile(store, config) + watcher.subscribe(config.docsPath, async (error, events) => { if (error !== null) { console.error(error) @@ -1369,8 +1555,7 @@ const watchAndRebuild = (store: ReturnType, config: Bui } events.forEach((event) => { - store.markdownFiles.delete(removeMdxSuffix(`/docs/${path.relative(config.docsPath, event.path)}`)) - store.partialsFiles.delete(path.relative(config.partialsPath, event.path)) + invalidate(event.path) }) try { @@ -1380,7 +1565,7 @@ const watchAndRebuild = (store: ReturnType, config: Bui const after = performance.now() - console.log(`Rebuilt docs in ${after - now} milliseconds`) + console.info(`Rebuilt docs in ${after - now} milliseconds`) if (output !== '') { console.info(output) From e06459f489276eaaef6fd1c18165faaa94cda987 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Apr 2025 14:25:23 -0700 Subject: [PATCH 091/114] re-organize the tests in to better grouping --- scripts/build-docs.test.ts | 2804 ++++++++++++++++++------------------ 1 file changed, 1400 insertions(+), 1404 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index f8234ee0e8..db69ed1f1b 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -129,18 +129,19 @@ const baseConfig = { }, } -test('Basic build test with simple files', async () => { - // Create temp environment with minimal files array - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- +describe('Basic Functionality', () => { + test('Basic build test with simple files', async () => { + // Create temp environment with minimal files array + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- title: Simple Test description: This is a simple test page --- @@ -148,22 +149,22 @@ description: This is a simple test page # Simple Test Page Testing with a simple page.`, - }, - ]) + }, + ]) - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['nextjs', 'react'], - }), - ) + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react'], + }), + ) - expect(output).toBe('') + expect(output).toBe('') - expect(await fileExists(pathJoin('./dist/simple-test.mdx'))).toBe(true) - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(`--- + expect(await fileExists(pathJoin('./dist/simple-test.mdx'))).toBe(true) + expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(`--- title: Simple Test description: This is a simple test page --- @@ -172,408 +173,1056 @@ description: This is a simple test page Testing with a simple page.`) - expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( - JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - ) -}) - -test('Warning on missing description in frontmatter', async () => { - // Create temp environment with minimal files array - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ + expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) + expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( + JSON.stringify({ navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), - }, - { - path: './docs/simple-test.mdx', - content: `--- + ) + }) + + test('Warning on missing description in frontmatter', async () => { + // Create temp environment with minimal files array + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- title: Simple Test --- # Simple Test Page Testing with a simple page.`, - }, - ]) - - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['nextjs', 'react'], - }), - ) - - expect(output).toContain('warning Frontmatter should have a "description" property') -}) + }, + ]) -test('Two Docs, each grouped by a different SDK', async () => { - // Create temp environment with minimal files array - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'React', - sdk: ['react'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/react' }]], - }, - { - title: 'Vue', - sdk: ['vue'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue' }]], - }, - ], - ], + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react'], }), - }, - { - path: './docs/quickstart/react.mdx', - content: `--- -title: Quickstart ---- - -# React Quickstart`, - }, - { - path: './docs/quickstart/vue.mdx', - content: `--- -title: Quickstart ---- - -# Vue Quickstart`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'vue'], - }), - ) - - expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( - JSON.stringify({ - navigation: [ - [ - { - title: 'React', - sdk: ['react'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/react', sdk: ['react'] }]], - }, - { - title: 'Vue', - sdk: ['vue'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue', sdk: ['vue'] }]], - }, - ], - ], - }), - ) - - const distFiles = await treeDir(pathJoin('./dist')) + ) - expect(distFiles.length).toBe(3) - expect(distFiles).toContain('manifest.json') - expect(distFiles).toContain('quickstart/vue.mdx') - expect(distFiles).toContain('quickstart/react.mdx') -}) + expect(output).toContain('warning Frontmatter should have a "description" property') + }) -test('sdk in frontmatter filters the docs', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react + test('should ignore non-MDX files in the docs folder', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'MDX Doc', href: '/docs/mdx-doc' }]], + }), + }, + { + path: './docs/mdx-doc.mdx', + content: `--- +title: MDX Doc --- -# Simple Test Page - -Testing with a simple page.`, - }, - ]) +# MDX Document`, + }, + { + path: './docs/non-mdx-file.txt', + content: `This is a text file, not an MDX file.`, + }, + { + path: './docs/image.png', + content: `fake image content`, + }, + ]) - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) - expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( - JSON.stringify({ navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react'] }]] }), - ) + // Verify only MDX files were processed + expect(await fileExists(pathJoin('./dist/mdx-doc.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/non-mdx-file.txt'))).toBe(false) + expect(await fileExists(pathJoin('./dist/image.png'))).toBe(false) + }) +}) - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- +describe('Manifest Validation', () => { + test('should fail build with completely malformed manifest JSON', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: '{invalid json structure', + }, + { + path: './docs/simple-test.mdx', + content: `--- title: Simple Test -sdk: react -canonical: /docs/:sdk:/simple-test --- -# Simple Test Page - -Testing with a simple page.`) - - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe( - `---\ntemplate: wide\n---\n`, - ) - - const distFiles = await treeDir(pathJoin('./dist')) - - expect(distFiles.length).toBe(3) - expect(distFiles).toContain('simple-test.mdx') - expect(distFiles).toContain('manifest.json') - expect(distFiles).toContain('react/simple-test.mdx') -}) +# Simple Test`, + }, + ]) -test('3 sdks in frontmatter generates 3 variants', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, vue, astro ---- - -# Simple Test Page + ) -Testing with a simple page.`, - }, - ]) + await expect(promise).rejects.toThrow('Failed to parse manifest:') + }) - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'vue', 'astro'], - }), - ) - - expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( - JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react', 'vue', 'astro'] }]], - }), - ) - - const distFiles = await treeDir(pathJoin('./dist')) - - expect(distFiles.length).toBe(5) - expect(distFiles).toContain('simple-test.mdx') - expect(distFiles).toContain('manifest.json') - expect(distFiles).toContain('react/simple-test.mdx') - expect(distFiles).toContain('vue/simple-test.mdx') - expect(distFiles).toContain('astro/simple-test.mdx') -}) + test('should apply manifest options (wrapDefault, collapseDefault, hideTitleDefault) correctly', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'Group One', + items: [[{ title: 'Item One', href: '/docs/item-one' }]], + wrap: true, + collapse: true, + hideTitle: false, + }, + { + title: 'Group Two', + items: [[{ title: 'Item Two', href: '/docs/item-two' }]], + wrap: true, + collapse: false, + hideTitle: true, + }, + { + title: 'Group Three', + items: [[{ title: 'Item Three', href: '/docs/item-three' }]], + wrap: false, + collapse: true, + hideTitle: false, + }, + { + title: 'Group Four', + items: [[{ title: 'Item Four', href: '/docs/item-four' }]], + wrap: false, + collapse: false, + hideTitle: true, + }, + ], + ], + }), + }, + { path: './docs/item-one.mdx', content: `---\ntitle: Item One\n---\nItem One` }, + { path: './docs/item-two.mdx', content: `---\ntitle: Item Two\n---\nItem Two` }, + { path: './docs/item-three.mdx', content: `---\ntitle: Item Three\n---\nItem Three` }, + { path: './docs/item-four.mdx', content: `---\ntitle: Item Four\n---\nItem Four` }, + ]) -test(' content filtered out when sdk is in frontmatter', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs'], + manifestOptions: { + wrapDefault: false, + collapseDefault: false, + hideTitleDefault: false, + }, }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, expo ---- + ) -# Simple Test Page + const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) + const groups = manifest.navigation[0] - - React Content - + expect(groups[0].wrap).toBe(true) + expect(groups[0].collapse).toBe(true) + expect(groups[0].hideTitle).toBe(undefined) -Testing with a simple page.`, - }, - ]) + expect(groups[1].wrap).toBe(true) + expect(groups[1].collapse).toBe(undefined) + expect(groups[1].hideTitle).toBe(true) - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'expo'], - }), - ) + expect(groups[2].wrap).toBe(undefined) + expect(groups[2].collapse).toBe(true) + expect(groups[2].hideTitle).toBe(undefined) - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('React Content') + expect(groups[3].wrap).toBe(undefined) + expect(groups[3].collapse).toBe(undefined) + expect(groups[3].hideTitle).toBe(true) + }) - expect(await readFile(pathJoin('./dist/expo/simple-test.mdx'))).not.toContain('React Content') -}) + test('should properly inherit SDK filtering from parent groups to child items', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'SDK Group', + sdk: ['nextjs', 'react'], + items: [ + [ + { + title: 'Sub Group', + items: [ + [ + { title: 'SDK Item', href: '/docs/sdk-item' }, + { title: 'Nested Group', items: [[{ title: 'Nested Item', href: '/docs/nested-item' }]] }, + ], + ], + }, + ], + ], + }, + { + title: 'Generic Group', + items: [ + [ + { + title: 'Sub Group', + items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], + }, + ], + ], + }, + { + title: 'Vue Group', + sdk: ['vue'], + items: [ + [ + { + title: 'Sub Group', + items: [[{ title: 'Vue Item', href: '/docs/vue-item' }]], + }, + ], + ], + }, + ], + ], + }), + }, + { + path: './docs/sdk-item.mdx', + content: `---\ntitle: SDK Item\n---\nSDK specific content`, + }, + { + path: './docs/nested-item.mdx', + content: `---\ntitle: Nested Item\n---\nNested SDK specific content`, + }, + { + path: './docs/generic-item.mdx', + content: `---\ntitle: Generic Item\n---\nGeneric content`, + }, + { + path: './docs/vue-item.mdx', + content: `---\ntitle: Vue Item\n---\nVue specific content`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react', 'vue'], + }), + ) + + // Check manifest + const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) + + expect(manifest).toEqual({ + navigation: [ + [ + { + title: 'SDK Group', + sdk: ['nextjs', 'react'], + items: [ + [ + { + title: 'Sub Group', + sdk: ['nextjs', 'react'], + items: [ + [ + { title: 'SDK Item', sdk: ['nextjs', 'react'], href: '/docs/sdk-item' }, + { + title: 'Nested Group', + sdk: ['nextjs', 'react'], + items: [[{ title: 'Nested Item', sdk: ['nextjs', 'react'], href: '/docs/nested-item' }]], + }, + ], + ], + }, + ], + ], + }, + { + title: 'Generic Group', + items: [ + [ + { + title: 'Sub Group', + items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], + }, + ], + ], + }, + { + title: 'Vue Group', + sdk: ['vue'], + items: [ + [ + { + title: 'Sub Group', + sdk: ['vue'], + items: [[{ title: 'Vue Item', sdk: ['vue'], href: '/docs/vue-item' }]], + }, + ], + ], + }, + ], + ], + }) + }) + + test('Check link and hash in partial is valid', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Page 1', href: '/docs/page-1' }, + { title: 'Page 2', href: '/docs/page-2' }, + ], + ], + }), + }, + { + path: './docs/page-1.mdx', + content: `--- +title: Page 1 +--- + +`, + }, + { + path: './docs/_partials/links.mdx', + content: `--- +title: Links +--- + +[Page 2](/docs/page-2#my-heading) +[Page 2](/docs/page-3)`, + }, + { + path: './docs/page-2.mdx', + content: `--- +title: Page 2 +--- + +test`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + expect(output).toContain(`warning Hash "my-heading" not found in /docs/page-2`) + expect(output).toContain(`warning Doc /docs/page-3 not found`) + }) + + test('should process target="_blank" links in manifest correctly', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Normal Link', href: '/docs/normal-link' }, + { title: 'External Link', href: 'https://example.com', target: '_blank' }, + ], + ], + }), + }, + { + path: './docs/normal-link.mdx', + content: `--- +title: Normal Link +--- + +# Normal Link + +This is a normal document.`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + // Check that the manifest contains the target="_blank" attribute + const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) + expect(manifest).toEqual({ + navigation: [ + [ + { title: 'Normal Link', href: '/docs/normal-link' }, + { title: 'External Link', href: 'https://example.com', target: '_blank' }, + ], + ], + }) + }) +}) + +describe('SDK Processing', () => { + test('Two Docs, each grouped by a different SDK', async () => { + // Create temp environment with minimal files array + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'React', + sdk: ['react'], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/react' }]], + }, + { + title: 'Vue', + sdk: ['vue'], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue' }]], + }, + ], + ], + }), + }, + { + path: './docs/quickstart/react.mdx', + content: `--- +title: Quickstart +--- + +# React Quickstart`, + }, + { + path: './docs/quickstart/vue.mdx', + content: `--- +title: Quickstart +--- + +# Vue Quickstart`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'vue'], + }), + ) + + expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) + expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( + JSON.stringify({ + navigation: [ + [ + { + title: 'React', + sdk: ['react'], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/react', sdk: ['react'] }]], + }, + { + title: 'Vue', + sdk: ['vue'], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue', sdk: ['vue'] }]], + }, + ], + ], + }), + ) + + const distFiles = await treeDir(pathJoin('./dist')) + + expect(distFiles.length).toBe(3) + expect(distFiles).toContain('manifest.json') + expect(distFiles).toContain('quickstart/vue.mdx') + expect(distFiles).toContain('quickstart/react.mdx') + }) + + test('sdk in frontmatter filters the docs', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react +--- + +# Simple Test Page + +Testing with a simple page.`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( + JSON.stringify({ navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react'] }]] }), + ) + + expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- +title: Simple Test +sdk: react +canonical: /docs/:sdk:/simple-test +--- + +# Simple Test Page + +Testing with a simple page.`) + + expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe( + `---\ntemplate: wide\n---\n`, + ) + + const distFiles = await treeDir(pathJoin('./dist')) + + expect(distFiles.length).toBe(3) + expect(distFiles).toContain('simple-test.mdx') + expect(distFiles).toContain('manifest.json') + expect(distFiles).toContain('react/simple-test.mdx') + }) + + test('3 sdks in frontmatter generates 3 variants', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react, vue, astro +--- + +# Simple Test Page + +Testing with a simple page.`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'vue', 'astro'], + }), + ) + + expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( + JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react', 'vue', 'astro'] }]], + }), + ) + + const distFiles = await treeDir(pathJoin('./dist')) + + expect(distFiles.length).toBe(5) + expect(distFiles).toContain('simple-test.mdx') + expect(distFiles).toContain('manifest.json') + expect(distFiles).toContain('react/simple-test.mdx') + expect(distFiles).toContain('vue/simple-test.mdx') + expect(distFiles).toContain('astro/simple-test.mdx') + }) + + test(' content filtered out when sdk is in frontmatter', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react, expo +--- + +# Simple Test Page + + + React Content + + +Testing with a simple page.`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'expo'], + }), + ) + + expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('React Content') + + expect(await readFile(pathJoin('./dist/expo/simple-test.mdx'))).not.toContain('React Content') + }) + + test('Invalid SDK in frontmatter fails the build', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react, expo, coffeescript +--- + +# Simple Test Page + +Testing with a simple page.`, + }, + ]) + + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'expo'], + }), + ) + + await expect(promise).rejects.toThrow(`Invalid SDK ["coffeescript"], the valid SDKs are ["react","expo"]`) + }) + + test('Invalid SDK in fails the build', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +sdk: react, expo +--- + +# Simple Test Page + + + astro Content + + +Testing with a simple page.`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'expo'], + }), + ) + + expect(output).toContain(`warning sdk \"astro\" in is not a valid SDK`) + }) + + test('should fail when child SDK is not in parent SDK list', async () => { + const { tempDir } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'Authentication', + sdk: ['react'], + items: [ + [ + { + title: 'Login', + href: '/docs/auth/login', + sdk: ['react', 'python'], // python not in parent + }, + ], + ], + }, + ], + ], + }), + }, + { + path: './docs/auth/login.mdx', + content: `--- +title: Login +sdk: react, python +--- + +# Login Page + +Authentication login documentation.`, + }, + ]) + + const promise = build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'python', 'nextjs'], + }), + ) + + await expect(promise).rejects.toThrow( + 'Doc "Login" is attempting to use ["react","python"] But its being filtered down to ["react"] in the manifest.json', + ) + }) -test('Invalid SDK in frontmatter fails the build', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, expo, coffeescript + test('should generate appropriate landing pages for SDK-specific docs', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'SDK Document', href: '/docs/sdk-document' }]], + }), + }, + { + path: './docs/sdk-document.mdx', + content: `--- +title: SDK Document +description: This document is available for React and Next.js. +sdk: react, nextjs --- -# Simple Test Page +# SDK Document -Testing with a simple page.`, - }, - ]) +This document is available for React and Next.js.`, + }, + ]) - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'expo'], - }), - ) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) - await expect(promise).rejects.toThrow(`Invalid SDK ["coffeescript"], the valid SDKs are ["react","expo"]`) -}) + // Check that SDK-specific versions were created + expect(await fileExists(pathJoin('./dist/react/sdk-document.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/nextjs/sdk-document.mdx'))).toBe(true) -test('Invalid SDK in fails the build', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, expo ---- + // Check that a landing page was created at the original URL + expect(await fileExists(pathJoin('./dist/sdk-document.mdx'))).toBe(true) -# Simple Test Page + // Verify landing page content + const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')) + expect(landingPage).toBe( + `---\ntemplate: wide\n---\n`, + ) + }) - - astro Content - + test('should handle SDK filtering with deeply nested manifest structures', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'Top Level', + items: [ + [ + { + title: 'Mid Level', + sdk: ['react', 'nextjs'], + items: [ + [ + { + title: 'Deep Level', + sdk: ['nextjs'], + items: [[{ title: 'Deeply Nested Page', href: '/docs/deeply-nested-nextjs' }]], + }, + { + title: 'Deep Level', + sdk: ['react'], + items: [[{ title: 'Deeply Nested Page', href: '/docs/deeply-nested-react' }]], + }, + ], + ], + }, + ], + ], + }, + ], + ], + }), + }, + { + path: './docs/deeply-nested-nextjs.mdx', + content: `--- +title: Deeply Nested Page +sdk: nextjs +--- -Testing with a simple page.`, - }, - ]) +Content for Next.js users.`, + }, + { + path: './docs/deeply-nested-react.mdx', + content: `--- +title: Deeply Nested Page +sdk: react +--- - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'expo'], - }), - ) +Content for React users.`, + }, + ]) - expect(output).toContain(`warning sdk \"astro\" in is not a valid SDK`) -}) + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs', 'js-frontend'], + }), + ) -test('should fail when child SDK is not in parent SDK list', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'Authentication', - sdk: ['react'], - items: [ - [ - { - title: 'Login', - href: '/docs/auth/login', - sdk: ['react', 'python'], // python not in parent - }, - ], + expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + navigation: [ + [ + { + title: 'Top Level', + sdk: ['react', 'nextjs'], + items: [ + [ + { + title: 'Mid Level', + sdk: ['react', 'nextjs'], + items: [ + [ + { + title: 'Deep Level', + sdk: ['nextjs'], + items: [ + [ + { + href: '/docs/:sdk:/deeply-nested-nextjs', + sdk: ['nextjs'], + title: 'Deeply Nested Page', + }, + ], + ], + }, + { + title: 'Deep Level', + sdk: ['react'], + items: [ + [ + { + title: 'Deeply Nested Page', + sdk: ['react'], + href: '/docs/:sdk:/deeply-nested-react', + }, + ], + ], + }, + ], + ], + }, ], - }, - ], + ], + }, ], - }), - }, - { - path: './docs/auth/login.mdx', - content: `--- -title: Login -sdk: react, python + ], + }) + + // Page should be available in nextjs (from manifest deep nesting) + expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-react.mdx'))).toBe(false) + expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toContain('Content for Next.js users.') + expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).not.toContain('Content for React users.') + + // Page should be available in react (from parent manifest item) + expect(await fileExists(pathJoin('./dist/react/deeply-nested-react.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/react/deeply-nested-nextjs.mdx'))).toBe(false) + expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).toContain('Content for React users.') + expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).not.toContain('Content for Next.js users.') + + // Page should NOT be available in js-frontend (filtered out by manifest) + expect(await fileExists(pathJoin('./dist/js-frontend/deeply-nested-nextjs.mdx'))).toBe(false) + expect(await fileExists(pathJoin('./dist/js-frontend/deeply-nested-react.mdx'))).toBe(false) + }) + + test('should correctly process multiple blocks with different SDKs in a single document', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'Multiple SDK Blocks', + href: '/multiple-sdk-blocks', + }, + ], + ], + }), + }, + { + path: './docs/multiple-sdk-blocks.mdx', + content: `--- +title: Multiple SDK Blocks +sdk: react, nextjs, js-frontend --- -# Login Page +# Multiple SDK Blocks -Authentication login documentation.`, - }, - ]) + + This content is for React users only. + + + + This content is for Next.js users only. + + + + This content is for JavaScript Frontend users only. + + +Common content for all SDKs.`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs', 'js-frontend'], + }), + ) + + // Check React version + expect(await fileExists(pathJoin('./dist/react/multiple-sdk-blocks.mdx'))).toBe(true) + const reactContent = await readFile(pathJoin('./dist/react/multiple-sdk-blocks.mdx')) + expect(reactContent).toContain('This content is for React users only.') + expect(reactContent).not.toContain('This content is for Next.js users only.') + expect(reactContent).not.toContain('This content is for JavaScript Frontend users only.') + expect(reactContent).toContain('Common content for all SDKs.') - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'python', 'nextjs'], - }), - ) + // Check Next.js version + expect(await fileExists(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx'))).toBe(true) + const nextjsContent = await readFile(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx')) + expect(nextjsContent).not.toContain('This content is for React users only.') + expect(nextjsContent).toContain('This content is for Next.js users only.') + expect(nextjsContent).not.toContain('This content is for JavaScript Frontend users only.') + expect(nextjsContent).toContain('Common content for all SDKs.') - await expect(promise).rejects.toThrow( - 'Doc "Login" is attempting to use ["react","python"] But its being filtered down to ["react"] in the manifest.json', - ) -}) + // Check JavaScript Frontend version + expect(await fileExists(pathJoin('./dist/js-frontend/multiple-sdk-blocks.mdx'))).toBe(true) + const jsContent = await readFile(pathJoin('./dist/js-frontend/multiple-sdk-blocks.mdx')) + expect(jsContent).not.toContain('This content is for React users only.') + expect(jsContent).not.toContain('This content is for Next.js users only.') + expect(jsContent).toContain('This content is for JavaScript Frontend users only.') + expect(jsContent).toContain('Common content for all SDKs.') + }) -describe('Includes and Partials', () => { - test(' Component embeds content in to guide', async () => { + test('should handle nested components correctly', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + navigation: [ + [ + { + title: 'Parent Group', + sdk: ['react', 'nextjs'], + items: [[{ title: 'Nested SDK Page', href: '/docs/nested-sdk-page' }]], + }, + ], + ], }), }, { - path: './docs/_partials/test-partial.mdx', - content: `Test Partial Content`, - }, - { - path: './docs/simple-test.mdx', + path: './docs/nested-sdk-page.mdx', content: `--- -title: Simple Test +title: Nested SDK Page +sdk: react, nextjs --- - +# Nested SDK Filtering -# Simple Test Page`, + + This content is for React users. + + + This is nested content specifically for Next.js users who are also using React. + + + +Common content for all SDKs.`, }, ]) @@ -582,35 +1231,54 @@ title: Simple Test createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react'], + validSdks: ['react', 'nextjs'], }), ) - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toContain('Test Partial Content') + // Check React output has only React content + const reactOutput = await readFile(pathJoin('./dist/react/nested-sdk-page.mdx')) + expect(reactOutput).toContain('This content is for React users.') + expect(reactOutput).not.toContain('This is nested content specifically for Next.js users') + + // Check Next.js output has both React and Next.js content + const nextjsOutput = await readFile(pathJoin('./dist/nextjs/nested-sdk-page.mdx')) + expect(nextjsOutput).toContain('This content is for React users.') + expect(nextjsOutput).toContain('This is nested content specifically for Next.js users') }) - test(' Component embeds content in to sdk scoped guide', async () => { + test('should support components with array syntax for multiple SDKs', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + navigation: [ + [ + { + title: 'Multiple SDK Test', + href: '/docs/multiple-sdk-test', + }, + ], + ], }), }, { - path: './docs/_partials/test-partial.mdx', - content: `Test Partial Content`, - }, - { - path: './docs/simple-test.mdx', + path: './docs/multiple-sdk-test.mdx', content: `--- -title: Simple Test -sdk: react +title: Multiple SDK Test +sdk: react, nextjs, js-frontend --- - +# Multiple SDK Test -# Simple Test Page`, + + This content is for React and Next.js users. + + + + This content is for JavaScript Frontend users. + + +Common content for all SDKs.`, }, ]) @@ -619,70 +1287,86 @@ sdk: react createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react'], + validSdks: ['react', 'nextjs', 'js-frontend'], }), ) - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('Test Partial Content') + // Check React output has React content but not JavaScript Frontend content + const reactOutput = await readFile(pathJoin('./dist/react/multiple-sdk-test.mdx')) + expect(reactOutput).toContain('This content is for React and Next.js users.') + expect(reactOutput).not.toContain('This content is for JavaScript Frontend users.') + + // Check Next.js output has Next.js content but not JavaScript Frontend content + const nextjsOutput = await readFile(pathJoin('./dist/nextjs/multiple-sdk-test.mdx')) + expect(nextjsOutput).toContain('This content is for React and Next.js users.') + expect(nextjsOutput).not.toContain('This content is for JavaScript Frontend users.') + + // Check JavaScript Frontend output has JavaScript Frontend content but not React/Next.js content + const jsOutput = await readFile(pathJoin('./dist/js-frontend/multiple-sdk-test.mdx')) + expect(jsOutput).toContain('This content is for JavaScript Frontend users.') + expect(jsOutput).not.toContain('This content is for React and Next.js users.') }) - test('Invalid partial src fails the build', async () => { - const { tempDir } = await createTempFiles([ + test('should embed canonical link in frontmatter', async () => { + const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + navigation: [ + [ + { + title: 'Overview', + href: '/docs/overview', + }, + ], + ], }), }, { - path: './docs/simple-test.mdx', + path: './docs/overview.mdx', content: `--- -title: Simple Test +title: Overview +sdk: fastify, expressjs --- - - -# Simple Test Page`, +# Hello World`, }, ]) - const output = await build( + await build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react'], + validSdks: ['fastify', 'expressjs'], }), ) - expect(output).toContain(`warning Partial /docs/_partials/test-partial.mdx not found`) + expect(await readFile(pathJoin('./dist/fastify/overview.mdx'))).toContain('canonical: /docs/:sdk:/overview') + expect(await readFile(pathJoin('./dist/expressjs/overview.mdx'))).toContain('canonical: /docs/:sdk:/overview') }) +}) - test('Fail if partial is within a partial', async () => { - const { tempDir } = await createTempFiles([ +describe('Heading Validation', () => { + test('should error on duplicate headings', async () => { + const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + navigation: [[{ title: 'Duplicate Headings', href: '/docs/duplicate-headings' }]], }), }, { - path: './docs/_partials/test-partial-1.mdx', - content: ``, - }, - { - path: './docs/_partials/test-partial-2.mdx', - content: `Test Partial Content`, - }, - { - path: './docs/simple-test.mdx', + path: './docs/duplicate-headings.mdx', content: `--- -title: Simple Test +title: Duplicate Headings --- - +# Heading {{ id: 'custom-id' }} -# Simple Test Page`, +## Another Heading {{ id: 'custom-id' }} + +[Link to first heading](#custom-id)`, }, ]) @@ -695,26 +1379,34 @@ title: Simple Test }), ) - await expect(promise).rejects.toThrow(`Partials inside of partials is not yet supported`) + await expect(promise).rejects.toThrow( + 'Doc "/docs/duplicate-headings" contains a duplicate heading id "custom-id", please ensure all heading ids are unique', + ) }) - test(`Warning if src doesn't start with "_partials/"`, async () => { + test('should not error on duplicate headings if they are in different components', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], }), }, { - path: './docs/simple-test.mdx', + path: './docs/quickstart.mdx', content: `--- -title: Simple Test +title: Quickstart +description: Quickstart page +sdk: react, nextjs --- - + + # Title {{ id: 'title' }} + -# Simple Test Page`, + + # Title {{ id: 'title' }} +`, }, ]) @@ -723,76 +1415,79 @@ title: Simple Test createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react'], + validSdks: ['react', 'nextjs'], }), ) - expect(output).toContain(`warning prop "src" must start with "_partials/"`) + expect(output).toBe('') }) -}) -describe('Link Validation and Processing', () => { - test('Fail if link is to non-existent page', async () => { + test('should error on duplicate headings if they are in different components but with the same sdk', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], }), }, { - path: './docs/simple-test.mdx', + path: './docs/quickstart.mdx', content: `--- -title: Simple Test +title: Quickstart +description: Quickstart page +sdk: react, nextjs --- -[Non Existent Page](/docs/non-existent-page) + + # Title {{ id: 'title' }} + -# Simple Test Page`, + + # Title {{ id: 'title' }} +`, }, ]) - const output = await build( + const promise = build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react'], + validSdks: ['react', 'nextjs'], }), ) - expect(output).toContain(`warning Doc /docs/non-existent-page not found`) + await expect(promise).rejects.toThrow( + 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', + ) }) - test('Validate link between two pages is valid', async () => { + test('should error on duplicate headings if they are in different components but with the same sdk without sdk in frontmatter', async () => { const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], }), }, { - path: './docs/simple-test.mdx', + path: './docs/quickstart.mdx', content: `--- -title: Simple Test +title: Quickstart +description: Quickstart page --- -[Core Page](/docs/core-page) - -# Simple Test Page`, - }, - { - path: './docs/core-page.mdx', - content: `--- -title: Core Page ---- + + # Title {{ id: 'title' }} + -# Core Page`, + + # Title {{ id: 'title' }} +`, }, ]) - const output = await build( + const promise = build( createBlankStore(), createConfig({ ...baseConfig, @@ -801,30 +1496,38 @@ title: Core Page }), ) - expect(output).not.toContain(`warning Doc /docs/core-page not found`) + await expect(promise).rejects.toThrow( + 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', + ) }) +}) - test('Warn if link is to existent page but with invalid hash', async () => { - const { tempDir } = await createTempFiles([ +describe('Includes and Partials', () => { + test(' Component embeds content in to guide', async () => { + const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, + { + path: './docs/_partials/test-partial.mdx', + content: `Test Partial Content`, + }, { path: './docs/simple-test.mdx', content: `--- title: Simple Test --- -[Simple Test](/docs/simple-test#non-existent-hash) + # Simple Test Page`, }, ]) - const output = await build( + await build( createBlankStore(), createConfig({ ...baseConfig, @@ -833,41 +1536,35 @@ title: Simple Test }), ) - expect(output).toContain(`warning Hash "non-existent-hash" not found in /docs/simple-test`) + expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toContain('Test Partial Content') }) - test('Pick up on id in heading for hash alias', async () => { - const { tempDir } = await createTempFiles([ + test(' Component embeds content in to sdk scoped guide', async () => { + const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { title: 'Simple Test', href: '/docs/simple-test' }, - { title: 'Headings', href: '/docs/headings' }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/headings.mdx', - content: `--- -title: Headings ---- - -# test {{ toc: false, id: 'my-heading' }}`, + path: './docs/_partials/test-partial.mdx', + content: `Test Partial Content`, }, { path: './docs/simple-test.mdx', content: `--- title: Simple Test +sdk: react --- -[Headings](/docs/headings#my-heading)`, + + +# Simple Test Page`, }, ]) - const output = await build( + await build( createBlankStore(), createConfig({ ...baseConfig, @@ -876,569 +1573,294 @@ title: Simple Test }), ) - expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) + expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('Test Partial Content') }) - test('Swap out links for when a link points to an sdk generated guide', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('Invalid partial src fails the build', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { title: 'SDK Filtered Page', href: '/docs/sdk-filtered-page' }, - { title: 'Core Page', href: '/docs/core-page' }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/sdk-filtered-page.mdx', - content: `--- -title: SDK Filtered Page -sdk: react, nextjs ---- - -SDK filtered page`, - }, - { - path: './docs/core-page.mdx', + path: './docs/simple-test.mdx', content: `--- -title: Core Page +title: Simple Test --- -# Core page + -[SDK Filtered Page](/docs/sdk-filtered-page) -`, +# Simple Test Page`, }, ]) - await build( + const output = await build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs'], + validSdks: ['react'], }), ) - expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain( - `SDK Filtered Page`, - ) + expect(output).toContain(`warning Partial /docs/_partials/test-partial.mdx not found`) }) -}) -describe('SDK Filtering', () => { - test('should handle SDK filtering with deeply nested manifest structures', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('Fail if partial is within a partial', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { - title: 'Top Level', - items: [ - [ - { - title: 'Mid Level', - sdk: ['react', 'nextjs'], - items: [ - [ - { - title: 'Deep Level', - sdk: ['nextjs'], - items: [[{ title: 'Deeply Nested Page', href: '/docs/deeply-nested-nextjs' }]], - }, - { - title: 'Deep Level', - sdk: ['react'], - items: [[{ title: 'Deeply Nested Page', href: '/docs/deeply-nested-react' }]], - }, - ], - ], - }, - ], - ], - }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/deeply-nested-nextjs.mdx', - content: `--- -title: Deeply Nested Page -sdk: nextjs ---- - -Content for Next.js users.`, + path: './docs/_partials/test-partial-1.mdx', + content: ``, }, { - path: './docs/deeply-nested-react.mdx', + path: './docs/_partials/test-partial-2.mdx', + content: `Test Partial Content`, + }, + { + path: './docs/simple-test.mdx', content: `--- -title: Deeply Nested Page -sdk: react +title: Simple Test --- -Content for React users.`, + + +# Simple Test Page`, }, ]) - await build( + const promise = build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'js-frontend'], + validSdks: ['react'], }), ) - expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ - navigation: [ - [ - { - title: 'Top Level', - sdk: ['react', 'nextjs'], - items: [ - [ - { - title: 'Mid Level', - sdk: ['react', 'nextjs'], - items: [ - [ - { - title: 'Deep Level', - sdk: ['nextjs'], - items: [ - [ - { - href: '/docs/:sdk:/deeply-nested-nextjs', - sdk: ['nextjs'], - title: 'Deeply Nested Page', - }, - ], - ], - }, - { - title: 'Deep Level', - sdk: ['react'], - items: [ - [ - { - title: 'Deeply Nested Page', - sdk: ['react'], - href: '/docs/:sdk:/deeply-nested-react', - }, - ], - ], - }, - ], - ], - }, - ], - ], - }, - ], - ], - }) - - // Page should be available in nextjs (from manifest deep nesting) - expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-react.mdx'))).toBe(false) - expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toContain('Content for Next.js users.') - expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).not.toContain('Content for React users.') - - // Page should be available in react (from parent manifest item) - expect(await fileExists(pathJoin('./dist/react/deeply-nested-react.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/react/deeply-nested-nextjs.mdx'))).toBe(false) - expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).toContain('Content for React users.') - expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).not.toContain('Content for Next.js users.') - - // Page should NOT be available in js-frontend (filtered out by manifest) - expect(await fileExists(pathJoin('./dist/js-frontend/deeply-nested-nextjs.mdx'))).toBe(false) - expect(await fileExists(pathJoin('./dist/js-frontend/deeply-nested-react.mdx'))).toBe(false) + await expect(promise).rejects.toThrow(`Partials inside of partials is not yet supported`) }) - test('should correctly process multiple blocks with different SDKs in a single document', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test(`Warning if src doesn't start with "_partials/"`, async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { - title: 'Multiple SDK Blocks', - href: '/multiple-sdk-blocks', - }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/multiple-sdk-blocks.mdx', + path: './docs/simple-test.mdx', content: `--- -title: Multiple SDK Blocks -sdk: react, nextjs, js-frontend +title: Simple Test --- -# Multiple SDK Blocks - - - This content is for React users only. - - - - This content is for Next.js users only. - - - - This content is for JavaScript Frontend users only. - + -Common content for all SDKs.`, +# Simple Test Page`, }, ]) - await build( + const output = await build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'js-frontend'], + validSdks: ['react'], }), ) - // Check React version - expect(await fileExists(pathJoin('./dist/react/multiple-sdk-blocks.mdx'))).toBe(true) - const reactContent = await readFile(pathJoin('./dist/react/multiple-sdk-blocks.mdx')) - expect(reactContent).toContain('This content is for React users only.') - expect(reactContent).not.toContain('This content is for Next.js users only.') - expect(reactContent).not.toContain('This content is for JavaScript Frontend users only.') - expect(reactContent).toContain('Common content for all SDKs.') - - // Check Next.js version - expect(await fileExists(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx'))).toBe(true) - const nextjsContent = await readFile(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx')) - expect(nextjsContent).not.toContain('This content is for React users only.') - expect(nextjsContent).toContain('This content is for Next.js users only.') - expect(nextjsContent).not.toContain('This content is for JavaScript Frontend users only.') - expect(nextjsContent).toContain('Common content for all SDKs.') - - // Check JavaScript Frontend version - expect(await fileExists(pathJoin('./dist/js-frontend/multiple-sdk-blocks.mdx'))).toBe(true) - const jsContent = await readFile(pathJoin('./dist/js-frontend/multiple-sdk-blocks.mdx')) - expect(jsContent).not.toContain('This content is for React users only.') - expect(jsContent).not.toContain('This content is for Next.js users only.') - expect(jsContent).toContain('This content is for JavaScript Frontend users only.') - expect(jsContent).toContain('Common content for all SDKs.') + expect(output).toContain(`warning prop "src" must start with "_partials/"`) }) +}) - test('should handle nested components correctly', async () => { - const { tempDir, pathJoin } = await createTempFiles([ +describe('Link Validation and Processing', () => { + test('Fail if link is to non-existent page', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { - title: 'Parent Group', - sdk: ['react', 'nextjs'], - items: [[{ title: 'Nested SDK Page', href: '/docs/nested-sdk-page' }]], - }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/nested-sdk-page.mdx', + path: './docs/simple-test.mdx', content: `--- -title: Nested SDK Page -sdk: react, nextjs +title: Simple Test --- -# Nested SDK Filtering - - - This content is for React users. - - - This is nested content specifically for Next.js users who are also using React. - - +[Non Existent Page](/docs/non-existent-page) -Common content for all SDKs.`, +# Simple Test Page`, }, ]) - await build( + const output = await build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs'], + validSdks: ['react'], }), ) - // Check React output has only React content - const reactOutput = await readFile(pathJoin('./dist/react/nested-sdk-page.mdx')) - expect(reactOutput).toContain('This content is for React users.') - expect(reactOutput).not.toContain('This is nested content specifically for Next.js users') - - // Check Next.js output has both React and Next.js content - const nextjsOutput = await readFile(pathJoin('./dist/nextjs/nested-sdk-page.mdx')) - expect(nextjsOutput).toContain('This content is for React users.') - expect(nextjsOutput).toContain('This is nested content specifically for Next.js users') + expect(output).toContain(`warning Doc /docs/non-existent-page not found`) }) - test('should support components with array syntax for multiple SDKs', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('Validate link between two pages is valid', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { - title: 'Multiple SDK Test', - href: '/docs/multiple-sdk-test', - }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/multiple-sdk-test.mdx', + path: './docs/simple-test.mdx', content: `--- -title: Multiple SDK Test -sdk: react, nextjs, js-frontend +title: Simple Test --- -# Multiple SDK Test - - - This content is for React and Next.js users. - +[Core Page](/docs/core-page) - - This content is for JavaScript Frontend users. - +# Simple Test Page`, + }, + { + path: './docs/core-page.mdx', + content: `--- +title: Core Page +--- -Common content for all SDKs.`, +# Core Page`, }, ]) - await build( + const output = await build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'js-frontend'], + validSdks: ['react'], }), ) - // Check React output has React content but not JavaScript Frontend content - const reactOutput = await readFile(pathJoin('./dist/react/multiple-sdk-test.mdx')) - expect(reactOutput).toContain('This content is for React and Next.js users.') - expect(reactOutput).not.toContain('This content is for JavaScript Frontend users.') - - // Check Next.js output has Next.js content but not JavaScript Frontend content - const nextjsOutput = await readFile(pathJoin('./dist/nextjs/multiple-sdk-test.mdx')) - expect(nextjsOutput).toContain('This content is for React and Next.js users.') - expect(nextjsOutput).not.toContain('This content is for JavaScript Frontend users.') - - // Check JavaScript Frontend output has JavaScript Frontend content but not React/Next.js content - const jsOutput = await readFile(pathJoin('./dist/js-frontend/multiple-sdk-test.mdx')) - expect(jsOutput).toContain('This content is for JavaScript Frontend users.') - expect(jsOutput).not.toContain('This content is for React and Next.js users.') + expect(output).not.toContain(`warning Doc /docs/core-page not found`) }) - test('should embed canonical link in frontmatter', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('Warn if link is to existent page but with invalid hash', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { - title: 'Overview', - href: '/docs/overview', - }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/overview.mdx', + path: './docs/simple-test.mdx', content: `--- -title: Overview -sdk: fastify, expressjs +title: Simple Test --- -# Hello World`, +[Simple Test](/docs/simple-test#non-existent-hash) + +# Simple Test Page`, }, ]) - await build( + const output = await build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['fastify', 'expressjs'], + validSdks: ['react'], }), ) - expect(await readFile(pathJoin('./dist/fastify/overview.mdx'))).toContain('canonical: /docs/:sdk:/overview') - expect(await readFile(pathJoin('./dist/expressjs/overview.mdx'))).toContain('canonical: /docs/:sdk:/overview') + expect(output).toContain(`warning Hash "non-existent-hash" not found in /docs/simple-test`) }) -}) -describe('Manifest Handling', () => { - test('should apply manifest options (wrapDefault, collapseDefault, hideTitleDefault) correctly', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('Pick up on id in heading for hash alias', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [ [ - { - title: 'Group One', - items: [[{ title: 'Item One', href: '/docs/item-one' }]], - wrap: true, - collapse: true, - hideTitle: false, - }, - { - title: 'Group Two', - items: [[{ title: 'Item Two', href: '/docs/item-two' }]], - wrap: true, - collapse: false, - hideTitle: true, - }, - { - title: 'Group Three', - items: [[{ title: 'Item Three', href: '/docs/item-three' }]], - wrap: false, - collapse: true, - hideTitle: false, - }, - { - title: 'Group Four', - items: [[{ title: 'Item Four', href: '/docs/item-four' }]], - wrap: false, - collapse: false, - hideTitle: true, - }, + { title: 'Simple Test', href: '/docs/simple-test' }, + { title: 'Headings', href: '/docs/headings' }, ], ], }), }, - { path: './docs/item-one.mdx', content: `---\ntitle: Item One\n---\nItem One` }, - { path: './docs/item-two.mdx', content: `---\ntitle: Item Two\n---\nItem Two` }, - { path: './docs/item-three.mdx', content: `---\ntitle: Item Three\n---\nItem Three` }, - { path: './docs/item-four.mdx', content: `---\ntitle: Item Four\n---\nItem Four` }, + { + path: './docs/headings.mdx', + content: `--- +title: Headings +--- + +# test {{ toc: false, id: 'my-heading' }}`, + }, + { + path: './docs/simple-test.mdx', + content: `--- +title: Simple Test +--- + +[Headings](/docs/headings#my-heading)`, + }, ]) - await build( + const output = await build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['nextjs'], - manifestOptions: { - wrapDefault: false, - collapseDefault: false, - hideTitleDefault: false, - }, + validSdks: ['react'], }), ) - const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) - const groups = manifest.navigation[0] - - expect(groups[0].wrap).toBe(true) - expect(groups[0].collapse).toBe(true) - expect(groups[0].hideTitle).toBe(undefined) - - expect(groups[1].wrap).toBe(true) - expect(groups[1].collapse).toBe(undefined) - expect(groups[1].hideTitle).toBe(true) - - expect(groups[2].wrap).toBe(undefined) - expect(groups[2].collapse).toBe(true) - expect(groups[2].hideTitle).toBe(undefined) - - expect(groups[3].wrap).toBe(undefined) - expect(groups[3].collapse).toBe(undefined) - expect(groups[3].hideTitle).toBe(true) - }) - - test('should properly inherit SDK filtering from parent groups to child items', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'SDK Group', - sdk: ['nextjs', 'react'], - items: [ - [ - { - title: 'Sub Group', - items: [ - [ - { title: 'SDK Item', href: '/docs/sdk-item' }, - { title: 'Nested Group', items: [[{ title: 'Nested Item', href: '/docs/nested-item' }]] }, - ], - ], - }, - ], - ], - }, - { - title: 'Generic Group', - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], - }, - ], - ], - }, - { - title: 'Vue Group', - sdk: ['vue'], - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Vue Item', href: '/docs/vue-item' }]], - }, - ], - ], - }, + expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) + }) + + test('Swap out links for when a link points to an sdk generated guide', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'SDK Filtered Page', href: '/docs/sdk-filtered-page' }, + { title: 'Core Page', href: '/docs/core-page' }, ], ], }), }, { - path: './docs/sdk-item.mdx', - content: `---\ntitle: SDK Item\n---\nSDK specific content`, - }, - { - path: './docs/nested-item.mdx', - content: `---\ntitle: Nested Item\n---\nNested SDK specific content`, - }, - { - path: './docs/generic-item.mdx', - content: `---\ntitle: Generic Item\n---\nGeneric content`, + path: './docs/sdk-filtered-page.mdx', + content: `--- +title: SDK Filtered Page +sdk: react, nextjs +--- + +SDK filtered page`, }, { - path: './docs/vue-item.mdx', - content: `---\ntitle: Vue Item\n---\nVue specific content`, + path: './docs/core-page.mdx', + content: `--- +title: Core Page +--- + +# Core page + +[SDK Filtered Page](/docs/sdk-filtered-page) +`, }, ]) @@ -1447,104 +1869,55 @@ describe('Manifest Handling', () => { createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['nextjs', 'react', 'vue'], + validSdks: ['react', 'nextjs'], }), ) - // Check manifest - const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) - - expect(manifest).toEqual({ - navigation: [ - [ - { - title: 'SDK Group', - sdk: ['nextjs', 'react'], - items: [ - [ - { - title: 'Sub Group', - sdk: ['nextjs', 'react'], - items: [ - [ - { title: 'SDK Item', sdk: ['nextjs', 'react'], href: '/docs/sdk-item' }, - { - title: 'Nested Group', - sdk: ['nextjs', 'react'], - items: [[{ title: 'Nested Item', sdk: ['nextjs', 'react'], href: '/docs/nested-item' }]], - }, - ], - ], - }, - ], - ], - }, - { - title: 'Generic Group', - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], - }, - ], - ], - }, - { - title: 'Vue Group', - sdk: ['vue'], - items: [ - [ - { - title: 'Sub Group', - sdk: ['vue'], - items: [[{ title: 'Vue Item', sdk: ['vue'], href: '/docs/vue-item' }]], - }, - ], - ], - }, - ], - ], - }) + expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain( + `SDK Filtered Page`, + ) }) - test('Check link and hash in partial is valid', async () => { - const { tempDir } = await createTempFiles([ + test('should correctly handle links with anchors to specific sections of documents', async () => { + const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [ [ - { title: 'Page 1', href: '/docs/page-1' }, - { title: 'Page 2', href: '/docs/page-2' }, + { title: 'Source Document', href: '/docs/source-document' }, + { title: 'Target Document', href: '/docs/target-document' }, ], ], }), }, { - path: './docs/page-1.mdx', + path: './docs/source-document.mdx', content: `--- -title: Page 1 +title: Source Document --- -`, - }, - { - path: './docs/_partials/links.mdx', - content: `--- -title: Links ---- +# Source Document -[Page 2](/docs/page-2#my-heading) -[Page 2](/docs/page-3)`, +[Link to Section 1](/docs/target-document#section-1) +[Link to Section 2](/docs/target-document#section-2) +[Link to Invalid Section](/docs/target-document#invalid-section)`, }, { - path: './docs/page-2.mdx', + path: './docs/target-document.mdx', content: `--- -title: Page 2 +title: Target Document --- -test`, +# Target Document + +## Section 1 + +Content for section 1. + +## Section 2 + +Content for section 2.`, }, ]) @@ -1557,8 +1930,12 @@ test`, }), ) - expect(output).toContain(`warning Hash "my-heading" not found in /docs/page-2`) - expect(output).toContain(`warning Doc /docs/page-3 not found`) + // Valid links should work without warnings + expect(output).not.toContain('warning Hash "section-1" not found') + expect(output).not.toContain('warning Hash "section-2" not found') + + // Invalid link should produce a warning + expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document') }) }) @@ -1973,150 +2350,34 @@ This page has an invalid SDK in frontmatter.`, navigation: [ [ { title: 'Valid Document', href: '/docs/valid-document' }, - { title: 'Invalid Reference', href: '/docs/invalid-reference' }, - ], - ], - }), - }, - { - path: './docs/valid-document.mdx', - content: `--- -title: Valid Document ---- - -# Valid Document - -[Link to Invalid Reference](/docs/invalid-reference#non-existent-header)`, - }, - { - path: './docs/invalid-reference.mdx', - content: `--- -title: Invalid Reference ---- - -# Invalid Reference - -This document doesn't have the referenced header.`, - }, - ]) - - // Should complete with warnings - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // Should report warning about missing hash - expect(output).toContain('warning Hash "non-existent-header" not found in /docs/invalid-reference') - }) - - test('should complete build workflow when errors are present in some files', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Valid Document', href: '/docs/valid-document' }, - { title: 'Document with Warnings', href: '/docs/document-with-warnings' }, - ], - ], - }), - }, - { - path: './docs/valid-document.mdx', - content: `--- -title: Valid Document ---- - -# Valid Document - -This is a completely valid document.`, - }, - { - path: './docs/document-with-warnings.mdx', - content: `--- -title: Document with Warnings ---- - -# Document with Warnings - -[Broken Link](/docs/non-existent-document) - - - This content has an invalid SDK. -`, - }, - ]) - - // Should complete with warnings - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // Check that the build completed and valid files were created - expect(await fileExists(pathJoin('./dist/valid-document.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/document-with-warnings.mdx'))).toBe(true) - - // Check that warnings were reported - expect(output).toContain('warning Doc /docs/non-existent-document not found') - expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK') - }) -}) - -describe('Advanced Features', () => { - test('should correctly handle links with anchors to specific sections of documents', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Source Document', href: '/docs/source-document' }, - { title: 'Target Document', href: '/docs/target-document' }, + { title: 'Invalid Reference', href: '/docs/invalid-reference' }, ], ], }), }, { - path: './docs/source-document.mdx', + path: './docs/valid-document.mdx', content: `--- -title: Source Document +title: Valid Document --- -# Source Document +# Valid Document -[Link to Section 1](/docs/target-document#section-1) -[Link to Section 2](/docs/target-document#section-2) -[Link to Invalid Section](/docs/target-document#invalid-section)`, +[Link to Invalid Reference](/docs/invalid-reference#non-existent-header)`, }, { - path: './docs/target-document.mdx', + path: './docs/invalid-reference.mdx', content: `--- -title: Target Document +title: Invalid Reference --- -# Target Document - -## Section 1 - -Content for section 1. - -## Section 2 +# Invalid Reference -Content for section 2.`, +This document doesn't have the referenced header.`, }, ]) + // Should complete with warnings const output = await build( createBlankStore(), createConfig({ @@ -2126,40 +2387,51 @@ Content for section 2.`, }), ) - // Valid links should work without warnings - expect(output).not.toContain('warning Hash "section-1" not found') - expect(output).not.toContain('warning Hash "section-2" not found') - - // Invalid link should produce a warning - expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document') + // Should report warning about missing hash + expect(output).toContain('warning Hash "non-existent-header" not found in /docs/invalid-reference') }) - test('should process target="_blank" links in manifest correctly', async () => { + test('should complete build workflow when errors are present in some files', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [ [ - { title: 'Normal Link', href: '/docs/normal-link' }, - { title: 'External Link', href: 'https://example.com', target: '_blank' }, + { title: 'Valid Document', href: '/docs/valid-document' }, + { title: 'Document with Warnings', href: '/docs/document-with-warnings' }, ], ], }), }, { - path: './docs/normal-link.mdx', + path: './docs/valid-document.mdx', content: `--- -title: Normal Link +title: Valid Document --- -# Normal Link +# Valid Document -This is a normal document.`, +This is a completely valid document.`, + }, + { + path: './docs/document-with-warnings.mdx', + content: `--- +title: Document with Warnings +--- + +# Document with Warnings + +[Broken Link](/docs/non-existent-document) + + + This content has an invalid SDK. +`, }, ]) - await build( + // Should complete with warnings + const output = await build( createBlankStore(), createConfig({ ...baseConfig, @@ -2168,65 +2440,76 @@ This is a normal document.`, }), ) - // Check that the manifest contains the target="_blank" attribute - const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) - expect(manifest).toEqual({ - navigation: [ - [ - { title: 'Normal Link', href: '/docs/normal-link' }, - { title: 'External Link', href: 'https://example.com', target: '_blank' }, - ], - ], - }) + // Check that the build completed and valid files were created + expect(await fileExists(pathJoin('./dist/valid-document.mdx'))).toBe(true) + expect(await fileExists(pathJoin('./dist/document-with-warnings.mdx'))).toBe(true) + + // Check that warnings were reported + expect(output).toContain('warning Doc /docs/non-existent-document not found') + expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK') }) +}) - test('should generate appropriate landing pages for SDK-specific docs', async () => { +describe('Cache Handling', () => { + test('should update cached files when their content changes', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'SDK Document', href: '/docs/sdk-document' }]], + navigation: [[{ title: 'Cached Doc', href: '/docs/cached-doc' }]], }), }, { - path: './docs/sdk-document.mdx', + path: './docs/cached-doc.mdx', content: `--- -title: SDK Document -description: This document is available for React and Next.js. -sdk: react, nextjs +title: Original Title --- -# SDK Document - -This document is available for React and Next.js.`, +# Original Content`, }, ]) - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'nextjs'], - }), - ) + // Create store to maintain cache across builds + const store = createBlankStore() + const config = createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }) + const invalidate = invalidateFile(store, config) - // Check that SDK-specific versions were created - expect(await fileExists(pathJoin('./dist/react/sdk-document.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/nextjs/sdk-document.mdx'))).toBe(true) + // First build + await build(store, config) - // Check that a landing page was created at the original URL - expect(await fileExists(pathJoin('./dist/sdk-document.mdx'))).toBe(true) + // Check initial content + const initialContent = await readFile(pathJoin('./dist/cached-doc.mdx')) + expect(initialContent).toContain('Original Title') + expect(initialContent).toContain('Original Content') - // Verify landing page content - const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')) - expect(landingPage).toBe( - `---\ntemplate: wide\n---\n`, + // Update file content + await fs.writeFile( + pathJoin('./docs/cached-doc.mdx'), + `--- +title: Updated Title +--- + +# Updated Content`, + 'utf-8', ) + + invalidate(pathJoin('./docs/cached-doc.mdx')) + + // Second build with same store (should detect changes) + await build(store, config) + + // Check updated content + const updatedContent = await readFile(pathJoin('./dist/cached-doc.mdx')) + expect(updatedContent).toContain('Updated Title') + expect(updatedContent).toContain('Updated Content') }) }) -describe('configuration', () => { +describe('Configuration Options', () => { describe('ignoreWarnings', () => { test('Should ignore certain warnings for a file when set', async () => { const { tempDir } = await createTempFiles([ @@ -2640,290 +2923,3 @@ description: Test page with partial }) }) }) - -// MANIFEST VALIDATION TESTS - -test('should fail build with completely malformed manifest JSON', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: '{invalid json structure', - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test ---- - -# Simple Test`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - await expect(promise).rejects.toThrow('Failed to parse manifest:') -}) - -// COMPLEX HEADING SCENARIOS - -test('should error on duplicate headings', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Duplicate Headings', href: '/docs/duplicate-headings' }]], - }), - }, - { - path: './docs/duplicate-headings.mdx', - content: `--- -title: Duplicate Headings ---- - -# Heading {{ id: 'custom-id' }} - -## Another Heading {{ id: 'custom-id' }} - -[Link to first heading](#custom-id)`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - await expect(promise).rejects.toThrow( - 'Doc "/docs/duplicate-headings" contains a duplicate heading id "custom-id", please ensure all heading ids are unique', - ) -}) - -test('should not error on duplicate headings if they are in different components', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], - }), - }, - { - path: './docs/quickstart.mdx', - content: `--- -title: Quickstart -description: Quickstart page -sdk: react, nextjs ---- - - - # Title {{ id: 'title' }} - - - - # Title {{ id: 'title' }} -`, - }, - ]) - - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'nextjs'], - }), - ) - - expect(output).toBe('') -}) - -test('should error on duplicate headings if they are in different components but with the same sdk', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], - }), - }, - { - path: './docs/quickstart.mdx', - content: `--- -title: Quickstart -description: Quickstart page -sdk: react, nextjs ---- - - - # Title {{ id: 'title' }} - - - - # Title {{ id: 'title' }} -`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'nextjs'], - }), - ) - - await expect(promise).rejects.toThrow( - 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', - ) -}) - -test('should error on duplicate headings if they are in different components but with the same sdk without sdk in frontmatter', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], - }), - }, - { - path: './docs/quickstart.mdx', - content: `--- -title: Quickstart -description: Quickstart page ---- - - - # Title {{ id: 'title' }} - - - - # Title {{ id: 'title' }} -`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - await expect(promise).rejects.toThrow( - 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', - ) -}) - -// HANDLING NON-MDX FILES - -test('should ignore non-MDX files in the docs folder', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'MDX Doc', href: '/docs/mdx-doc' }]], - }), - }, - { - path: './docs/mdx-doc.mdx', - content: `--- -title: MDX Doc ---- - -# MDX Document`, - }, - { - path: './docs/non-mdx-file.txt', - content: `This is a text file, not an MDX file.`, - }, - { - path: './docs/image.png', - content: `fake image content`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // Verify only MDX files were processed - expect(await fileExists(pathJoin('./dist/mdx-doc.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/non-mdx-file.txt'))).toBe(false) - expect(await fileExists(pathJoin('./dist/image.png'))).toBe(false) -}) - -// CACHE INVALIDATION TESTS - -test('should update cached files when their content changes', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Cached Doc', href: '/docs/cached-doc' }]], - }), - }, - { - path: './docs/cached-doc.mdx', - content: `--- -title: Original Title ---- - -# Original Content`, - }, - ]) - - // Create store to maintain cache across builds - const store = createBlankStore() - const config = createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }) - const invalidate = invalidateFile(store, config) - - // First build - await build(store, config) - - // Check initial content - const initialContent = await readFile(pathJoin('./dist/cached-doc.mdx')) - expect(initialContent).toContain('Original Title') - expect(initialContent).toContain('Original Content') - - // Update file content - await fs.writeFile( - pathJoin('./docs/cached-doc.mdx'), - `--- -title: Updated Title ---- - -# Updated Content`, - 'utf-8', - ) - - invalidate(pathJoin('./docs/cached-doc.mdx')) - - // Second build with same store (should detect changes) - await build(store, config) - - // Check updated content - const updatedContent = await readFile(pathJoin('./dist/cached-doc.mdx')) - expect(updatedContent).toContain('Updated Title') - expect(updatedContent).toContain('Updated Content') -}) From ca134b90aca708d01fbae25252ba2313e3fe4875 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Apr 2025 17:23:00 -0700 Subject: [PATCH 092/114] Add config to clean the dist folder, and set it to true in --dev mode --- scripts/build-docs.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 02cfb61359..19e958ae24 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1079,6 +1079,11 @@ export const build = async (store: ReturnType, config: ) console.info('✔️ Applied manifest sdk scoping') + if (config.cleanDist) { + await fs.rm(config.distPath, { recursive: true }) + console.info('✔️ Removed dist folder') + } + await writeFile( 'manifest.json', JSON.stringify({ @@ -1592,6 +1597,7 @@ type BuildConfigOptions = { collapseDefault: boolean hideTitleDefault: boolean } + cleanDist: boolean } type BuildConfig = ReturnType @@ -1625,6 +1631,7 @@ export function createConfig(config: BuildConfigOptions) { collapseDefault: false, hideTitleDefault: false, }, + cleanDist: config.cleanDist, } } @@ -1664,6 +1671,7 @@ const main = async () => { collapseDefault: false, hideTitleDefault: false, }, + cleanDist: false, }) const store = createBlankStore() @@ -1680,7 +1688,7 @@ const main = async () => { if (watchFlag) { console.info(`Watching for changes...`) - watchAndRebuild(store, config) + watchAndRebuild(store, { ...config, cleanDist: true }) } else if (output !== '') { process.exit(1) } From 2049b18ddfa1e92b7a20d5bfe216cab9067f15e2 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 10 Apr 2025 18:30:37 -0700 Subject: [PATCH 093/114] Send info up to parent process when --controlled --- scripts/build-docs.ts | 49 ++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 19e958ae24..eaba82f847 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -933,13 +933,14 @@ export const build = async (store: ReturnType, config: await ensureDir(config.distPath) const userManifest = await getManifest() - console.info('✔️ Read Manifest') + console.info('✓ Read Manifest') const docsFiles = await getDocsFolder() - console.info('✔️ Read Docs Folder') + console.info('✓ Read Docs Folder') + const cachedPartialsSize = store.partialsFiles.size const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) - console.info(`✔️ Read ${partials.length} Partials`) + console.info(`✓ Loaded in ${partials.length} partials (${cachedPartialsSize} cached)`) const docsMap = new Map>>() const docsInManifest = new Set() @@ -956,8 +957,9 @@ export const build = async (store: ReturnType, config: return item }) - console.info('✔️ Parsed in Manifest') + console.info('✓ Parsed in Manifest') + const cachedDocsSize = store.markdownFiles.size // Read in all the docs const docsArray = await Promise.all( docsFiles.map(async (file) => { @@ -982,7 +984,7 @@ export const build = async (store: ReturnType, config: return markdownFile }), ) - console.info(`✔️ Loaded in ${docsArray.length} docs`) + console.info(`✓ Loaded in ${docsArray.length} docs (${cachedDocsSize} cached)`) // Goes through and grabs the sdk scoping out of the manifest const sdkScopedManifest = await traverseTree( @@ -1077,11 +1079,11 @@ export const build = async (store: ReturnType, config: throw error }, ) - console.info('✔️ Applied manifest sdk scoping') + console.info('✓ Applied manifest sdk scoping') if (config.cleanDist) { await fs.rm(config.distPath, { recursive: true }) - console.info('✔️ Removed dist folder') + console.info('✓ Removed dist folder') } await writeFile( @@ -1182,7 +1184,7 @@ export const build = async (store: ReturnType, config: .process(partial.vfile) }), ) - console.info(`✔️ Validated all partials`) + console.info(`✓ Validated all partials`) const coreVFiles = await Promise.all( docsArray.map(async (doc) => { @@ -1340,7 +1342,7 @@ template: wide }), ) - console.info(`✔️ Validated and wrote out all docs`) + console.info(`✓ Validated and wrote out all docs`) const sdkSpecificVFiles = await Promise.all( config.validSdks.map(async (targetSdk) => { @@ -1461,7 +1463,7 @@ template: wide }), ) - console.info(`✔️ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific docs`) + console.info(`✓ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific docs`) return { targetSdk, vFiles } }), @@ -1568,6 +1570,10 @@ const watchAndRebuild = (store: ReturnType, config: Bui const output = await build(store, config) + if (config.flags.controlled) { + console.info('---rebuild-complete---') + } + const after = performance.now() console.info(`Rebuilt docs in ${after - now} milliseconds`) @@ -1598,6 +1604,10 @@ type BuildConfigOptions = { hideTitleDefault: boolean } cleanDist: boolean + flags?: { + watch?: boolean + controlled?: boolean + } } type BuildConfig = ReturnType @@ -1632,10 +1642,16 @@ export function createConfig(config: BuildConfigOptions) { hideTitleDefault: false, }, cleanDist: config.cleanDist, + flags: { + watch: config.flags?.watch ?? false, + controlled: config.flags?.controlled ?? false, + }, } } const main = async () => { + const args = process.argv.slice(2) + const config = createConfig({ basePath: __dirname, docsPath: '../docs', @@ -1672,20 +1688,25 @@ const main = async () => { hideTitleDefault: false, }, cleanDist: false, + flags: { + watch: args.includes('--watch'), + controlled: args.includes('--controlled'), + }, }) const store = createBlankStore() const output = await build(store, config) + if (config.flags.controlled) { + console.info('---initial-build-complete---') + } + if (output !== '') { console.info(output) } - const args = process.argv.slice(2) - const watchFlag = args.includes('--watch') - - if (watchFlag) { + if (config.flags.watch) { console.info(`Watching for changes...`) watchAndRebuild(store, { ...config, cleanDist: true }) From 51fbd157c1cec5910c2db445c1ef99146196b569 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 11 Apr 2025 10:33:15 -0700 Subject: [PATCH 094/114] Swap out links in partials if they point to an sdk scoped page --- scripts/build-docs.test.ts | 108 +++++++++++++++++++++++++++++++++++++ scripts/build-docs.ts | 30 +++++++++-- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index db69ed1f1b..89f5557878 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -127,6 +127,7 @@ const baseConfig = { collapseDefault: false, hideTitleDefault: false, }, +cleanDist: false, } describe('Basic Functionality', () => { @@ -1878,6 +1879,113 @@ title: Core Page ) }) + test('Should swap out links for in partials', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'SDK Filtered Page', href: '/docs/sdk-filtered-page' }, + { title: 'Core Page', href: '/docs/core-page' }, + ], + ], + }), + }, + { + path: './docs/sdk-filtered-page.mdx', + content: `--- +title: SDK Filtered Page +sdk: react, nextjs +--- + +SDK filtered page`, + }, + { + path: './docs/_partials/links.mdx', + content: `[SDK Filtered Page](/docs/sdk-filtered-page)`, + }, + { + path: './docs/core-page.mdx', + content: `--- +title: Core Page +--- + +# Core page + + +`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) + + expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain( + `SDK Filtered Page`, + ) + }) + + test('Should swap out links for inside a component', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'SDK Filtered Page', href: '/docs/sdk-filtered-page' }, + { title: 'Core Page', href: '/docs/core-page' }, + ], + ], + }), + }, + { + path: './docs/sdk-filtered-page.mdx', + content: `--- +title: SDK Filtered Page +sdk: react, nextjs +--- + +SDK filtered page`, + }, + { + path: './docs/core-page.mdx', + content: `--- +title: Core Page +--- + +# Core page + + + - afterMultiSessionSingleSignOutUrl + - string + + go use [SDK Filtered Page](/docs/sdk-filtered-page) + +`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) + + expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain( + `SDK Filtered Page`, + ) + }) + test('should correctly handle links with anchors to specific sections of documents', async () => { const { tempDir, pathJoin } = await createTempFiles([ { diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index eaba82f847..feb86cebd2 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1121,10 +1121,13 @@ export const build = async (store: ReturnType, config: const flatSDKScopedManifest = flattenTree(sdkScopedManifest) - const partialsVFiles = await Promise.all( + const validatedPartials = await Promise.all( partials.map(async (partial) => { const partialPath = `/docs/_partials/${partial.path}` - return await markdownProcessor() + + let node: Node | null = null + + const vfile = await markdownProcessor() // validate links in partials to docs are valid and replace the links to sdk scoped pages with the sdk link component .use(() => (tree, vfile) => { return mdastMap(tree, (node) => { @@ -1157,9 +1160,15 @@ export const build = async (store: ReturnType, config: } } + console.log({ + doc, + }) + if (doc.sdk !== undefined) { // we are going to swap it for the sdk link component to give the users a great experience + console.log('swapping for sdk link component') + return mdastBuilder('mdxJsxTextElement', { name: 'SDKLink', attributes: [ @@ -1181,7 +1190,20 @@ export const build = async (store: ReturnType, config: return node }) }) + .use(() => (tree, vfile) => { + node = tree + }) .process(partial.vfile) + + if (node === null) { + throw new Error(errorMessages['partial-parse-error'](partial.path)) + } + + return { + ...partial, + node: node as Node, + vfile, + } }), ) console.info(`✓ Validated all partials`) @@ -1302,7 +1324,7 @@ export const build = async (store: ReturnType, config: if (partialSrc === undefined) return node - const partial = partials.find( + const partial = validatedPartials.find( (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, ) @@ -1543,6 +1565,8 @@ template: wide .flatMap(({ vFiles }) => vFiles) .filter((item): item is NonNullable => item !== null) + const partialsVFiles = validatedPartials.map((partial) => partial.vfile) + return reporter([...coreVFiles, ...partialsVFiles, ...flatSdkSpecificVFiles], { quiet: true }) } From 786ec57c77339c1486fa0fa24b473c942d50814e Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 11 Apr 2025 12:32:12 -0700 Subject: [PATCH 095/114] explicitly catch code wrapped links --- scripts/build-docs.test.ts | 112 ++++++++++++++++++++++++++++++++++++- scripts/build-docs.ts | 62 ++++++++++++++++++-- 2 files changed, 168 insertions(+), 6 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 89f5557878..a4d70bb7eb 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -127,7 +127,7 @@ const baseConfig = { collapseDefault: false, hideTitleDefault: false, }, -cleanDist: false, + cleanDist: false, } describe('Basic Functionality', () => { @@ -2045,6 +2045,116 @@ Content for section 2.`, // Invalid link should produce a warning expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document') }) + + test('if the contents of a link starts with a ` and ends with a ` it should inject the code prop', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Link with code', href: '/docs/link-with-code' }, + { title: 'Sign In', href: '/docs/components/sign-in' }, + ], + ], + }), + }, + { + path: './docs/components/sign-in.mdx', + content: `--- +title: Sign In +description: Sign In component +sdk: react, nextjs +--- + +\`\`\`js +const x = 'y' +\`\`\` +`, + }, + { + path: './docs/link-with-code.mdx', + content: `--- +title: Link with code +description: Link with code +--- +- [\`\`](/docs/components/sign-in) +`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) + + expect(output).toBe('') + + expect(await readFile(pathJoin('./dist/link-with-code.mdx'))).toContain( + `\\`, + ) + }) + + test('if the contents of a link starts with a ` and ends with a ` it should inject the code prop (when in a partial)', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { title: 'Link with code', href: '/docs/link-with-code' }, + { title: 'Sign In', href: '/docs/components/sign-in' }, + ], + ], + }), + }, + { + path: './docs/components/sign-in.mdx', + content: `--- +title: Sign In +description: Sign In component +sdk: react, nextjs +--- + +\`\`\`js +const x = 'y' +\`\`\` +`, + }, + { + path: './docs/_partials/links.mdx', + content: `[\`\`](/docs/components/sign-in)`, + }, + { + path: './docs/link-with-code.mdx', + content: `--- +title: Link with code +description: Link with code +--- +- +`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react', 'nextjs'], + }), + ) + + expect(output).toBe('') + + expect(await readFile(pathJoin('./dist/link-with-code.mdx'))).toContain( + `\\`, + ) + }) }) describe('Path and File Handling', () => { diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index feb86cebd2..24c1fc6e2a 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1160,14 +1160,37 @@ export const build = async (store: ReturnType, config: } } - console.log({ - doc, - }) - if (doc.sdk !== undefined) { // we are going to swap it for the sdk link component to give the users a great experience - console.log('swapping for sdk link component') + const firstChild = node.children?.[0] + const childIsCodeBlock = firstChild?.type === 'inlineCode' + + if (childIsCodeBlock) { + firstChild.type = 'text' + + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: scopeHrefToSDK(url, ':sdk:'), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(doc.sdk), + }), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'code', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: childIsCodeBlock, + }), + }), + ], + }) + } return mdastBuilder('mdxJsxTextElement', { name: 'SDKLink', @@ -1248,6 +1271,35 @@ export const build = async (store: ReturnType, config: if (doc.sdk !== undefined) { // we are going to swap it for the sdk link component to give the users a great experience + const firstChild = node.children?.[0] + const childIsCodeBlock = firstChild?.type === 'inlineCode' + + if (childIsCodeBlock) { + firstChild.type = 'text' + + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: scopeHrefToSDK(url, ':sdk:'), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(doc.sdk), + }), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'code', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: childIsCodeBlock, + }), + }), + ], + }) + } + return mdastBuilder('mdxJsxTextElement', { name: 'SDKLink', attributes: [ From eadae1ddac4bdca545684d82abf20b5e83466f02 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 11 Apr 2025 17:23:15 -0700 Subject: [PATCH 096/114] Fix the icons partials to be processed correctly --- clerk-typedoc | 1 + docs/index.mdx | 38 +++++++++++++++++------------------ docs/quickstarts/overview.mdx | 28 +++++++++++++------------- docs/references/overview.mdx | 38 +++++++++++++++++------------------ 4 files changed, 53 insertions(+), 52 deletions(-) create mode 160000 clerk-typedoc diff --git a/clerk-typedoc b/clerk-typedoc new file mode 160000 index 0000000000..f280518930 --- /dev/null +++ b/clerk-typedoc @@ -0,0 +1 @@ +Subproject commit f2805189301b56a47f884f47026134b1ecd965da diff --git a/docs/index.mdx b/docs/index.mdx index be2868c462..030cf52c3d 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -37,73 +37,73 @@ Find all the guides and resources you need to develop with Clerk. - [Next.js](/docs/quickstarts/nextjs) - Easily add secure, beautiful, and fast authentication to Next.js with Clerk. - - {} + - --- - [React](/docs/quickstarts/react) - Get started installing and initializing Clerk in a new React + Vite app. - - {} + - --- - [Astro](/docs/quickstarts/astro) - Easily add secure and SSR-friendly authentication to your Astro application with Clerk. - - {} + - --- - [Chrome Extension](/docs/quickstarts/chrome-extension) - Use the Chrome Extension SDK to authenticate users in your Chrome extension. - - {} + - --- - [Expo](/docs/quickstarts/expo) - Use Clerk with Expo to authenticate users in your React Native application. - - {} + - --- - [iOS](/docs/quickstarts/ios) - Use the Clerk iOS SDK to authenticate users in your native Apple applications. - - {} + - --- - [JavaScript](/docs/quickstarts/javascript) - The Clerk JavaScript SDK gives you access to prebuilt components and helpers to make user authentication easier. - - {} + - --- - [Nuxt](/docs/quickstarts/nuxt) - Easily add secure, beautiful, and fast authentication to Nuxt with Clerk. - - {} + - --- - [React Router](/docs/quickstarts/react-router) - Easily add secure, edge- and SSR-friendly authentication to React Router with Clerk. - - {} + - --- - [Remix](/docs/quickstarts/remix) - Easily add secure, edge- and SSR-friendly authentication to Remix with Clerk. - - {} + - --- - [TanStack React Start (beta)](/docs/quickstarts/tanstack-react-start) - Easily add secure and SSR-friendly authentication to your TanStack React Start application with Clerk. - - {} + - --- - [Vue](/docs/quickstarts/vue) - Get started installing and initializing Clerk in a new Vue + Vite app. - - {} + - ## Explore by backend framework @@ -113,43 +113,43 @@ Find all the guides and resources you need to develop with Clerk. - [JS Backend SDK](/docs/references/backend/overview) - The Clerk Backend SDK exposes our Backend API resources and low-level authentication utilities for JavaScript environments. - - {} + - --- - [C#](https://github.com/clerk/clerk-sdk-csharp/blob/main/README.md) - The Clerk C# SDK is a wrapper around our Backend API to make it easier to integrate Clerk into your backend. - - {} + - --- - [Express](/docs/quickstarts/express) - Quickly add authentication and user management to your Express application. - - {} + - --- - [Go](/docs/references/go/overview) - The Clerk Go SDK is a wrapper around the Backend API written in Golang to make it easier to integrate Clerk into your backend. - - {} + - --- - [Fastify](/docs/quickstarts/fastify) - Build secure authentication and user management flows for your Fastify server. - - {} + - --- - [Python](https://github.com/clerk/clerk-sdk-python/blob/main/README.md) - The Clerk Python SDK is a wrapper around the Backend API written in Python to make it easier to integrate Clerk into your backend. - - {} + - --- - [Ruby on Rails](/docs/quickstarts/ruby) - Integrate authentication and user management into your Ruby application. - - {} + - ## Explore by feature diff --git a/docs/quickstarts/overview.mdx b/docs/quickstarts/overview.mdx index 0c3d1dc944..28d31de4b1 100644 --- a/docs/quickstarts/overview.mdx +++ b/docs/quickstarts/overview.mdx @@ -8,37 +8,37 @@ description: See the getting started guides and tutorials. - [Next.js](/docs/quickstarts/nextjs) - Easily add secure, beautiful, and fast authentication to your Next.js application with Clerk. - - {} + - --- - [Astro](/docs/quickstarts/astro) - Easily add secure and SSR-friendly authentication to your Astro application with Clerk. - - {} + - --- - [Nuxt](/docs/quickstarts/nuxt) - Easily add secure, beautiful, and fast authentication to Nuxt with Clerk. - - {} + - --- - [React Router (Beta)](/docs/quickstarts/react-router) - The Clerk React Router SDK provides prebuilt components, hooks, and stores to make it easy to integrate authentication and user management in your React Router app. - - {} + - --- - [Remix](/docs/quickstarts/remix) - Easily add secure, edge- and SSR-friendly authentication to your Remix application with Clerk. - - {} + - --- - [TanStack React Start (beta)](/docs/quickstarts/tanstack-react-start) - Easily add secure and SSR-friendly authentication to your TanStack React Start application with Clerk. - - {} + - ## Frontend @@ -46,37 +46,37 @@ description: See the getting started guides and tutorials. - [React](/docs/quickstarts/react) - Easily add secure, beautiful, and fast authentication to your React application with Clerk. - - {} + - --- - [Chrome Extension](/docs/quickstarts/chrome-extension) - Use the Chrome Extension SDK to authenticate users in your Chrome extension. - - {} + - --- - [Expo](/docs/quickstarts/expo) - Use Clerk with Expo to authenticate users in your React Native application. - - {} + - --- - [iOS](/docs/quickstarts/ios) - Use the Clerk iOS SDK to authenticate users in your native Apple applications. - - {} + - --- - [JavaScript](/docs/quickstarts/javascript) - Easily add secure, beautiful, and fast authentication to your JavaScript application with Clerk. - - {} + - --- - [Vue](/docs/quickstarts/vue) - Easily add secure, beautiful, and fast authentication to your Vue application with Clerk. - - {} + - ## Backend @@ -84,13 +84,13 @@ description: See the getting started guides and tutorials. - [Express](/docs/quickstarts/express) - Easily add secure, beautiful, and fast authentication to your Express application with Clerk. - - {} + - --- - [Fastify](/docs/quickstarts/fastify) - Easily add secure, beautiful, and fast authentication to your Fastify application with Clerk. - - {} + - diff --git a/docs/references/overview.mdx b/docs/references/overview.mdx index 3dba2cb406..795f8a19a0 100644 --- a/docs/references/overview.mdx +++ b/docs/references/overview.mdx @@ -10,73 +10,73 @@ description: Learn about the Clerk and community SDK's available for integrating - [Next.js](/docs/references/nextjs/overview) - Easily add secure, beautiful, and fast authentication to Next.js with Clerk. - - {} + - --- - [React](/docs/references/react/overview) - Get started installing and initializing Clerk in a new React + Vite app. - - {} + - --- - [Astro](/docs/references/astro/overview) - Easily add secure and SSR-friendly authentication to your Astro application with Clerk. - - {} + - --- - [Chrome Extension](/docs/references/chrome-extension/overview) - Use the Chrome Extension SDK to authenticate users in your Chrome extension. - - {} + - --- - [Expo](/docs/references/expo/overview) - Use Clerk with Expo to authenticate users in your React Native application. - - {} + - --- - [iOS](/docs/references/ios/overview) - Use the Clerk iOS SDK to authenticate users in your native Apple applications. - - {} + - --- - [JavaScript](/docs/references/javascript/overview) - The Clerk JavaScript SDK gives you access to prebuilt components and helpers to make user authentication easier. - - {} + - --- - [Nuxt](/docs/references/nuxt/overview) - Easily add secure, beautiful, and fast authentication to Nuxt with Clerk. - - {} + - --- - [React Router](/docs/references/react-router/overview) - Easily add secure, edge- and SSR-friendly authentication to React Router with Clerk. - - {} + - --- - [Remix](/docs/references/remix/overview) - Easily add secure, edge- and SSR-friendly authentication to Remix with Clerk. - - {} + - --- - [TanStack React Start (beta)](/docs/references/tanstack-react-start/overview) - Easily add secure and SSR-friendly authentication to your TanStack React Start application with Clerk. - - {} + - --- - [Vue](/docs/references/vue/overview) - Get started installing and initializing Clerk in a new Vue + Vite app. - - {} + - ## Backend SDKs @@ -84,43 +84,43 @@ description: Learn about the Clerk and community SDK's available for integrating - [JS Backend SDK](/docs/references/backend/overview) - The Clerk Backend SDK exposes our Backend API resources and low-level authentication utilities for JavaScript environments. - - {} + - --- - [C#](https://github.com/clerk/clerk-sdk-csharp/blob/main/README.md) - The Clerk C# SDK is a wrapper around our Backend API to make it easier to integrate Clerk into your backend. - - {} + - --- - [Express](/docs/references/express/overview) - Quickly add authentication and user management to your Express application. - - {} + - --- - [Go](/docs/references/go/overview) - The Clerk Go SDK is a wrapper around the Backend API written in Golang to make it easier to integrate Clerk into your backend. - - {} + - --- - [Fastify](/docs/references/fastify/overview) - Build secure authentication and user management flows for your Fastify server. - - {} + - --- - [Python](https://github.com/clerk/clerk-sdk-python/blob/main/README.md) - The Clerk Python SDK is a wrapper around the Backend API written in Python to make it easier to integrate Clerk into your backend. - - {} + - --- - [Ruby on Rails](/docs/references/ruby/overview) - Integrate authentication and user management into your Ruby application. - - {} + - ## Build with community-maintained SDKs From e4fba71efd64863a73274375ca769fca39c8b077 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 14 Apr 2025 19:02:33 -0700 Subject: [PATCH 097/114] Check if we should ignore the link --- scripts/build-docs.test.ts | 82 +++++++++++++++----------------------- scripts/build-docs.ts | 3 ++ 2 files changed, 35 insertions(+), 50 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index a4d70bb7eb..418b0b4775 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -523,11 +523,7 @@ title: Page 1 }, { path: './docs/_partials/links.mdx', - content: `--- -title: Links ---- - -[Page 2](/docs/page-2#my-heading) + content: `[Page 2](/docs/page-2#my-heading) [Page 2](/docs/page-3)`, }, { @@ -1556,7 +1552,7 @@ title: Simple Test path: './docs/simple-test.mdx', content: `--- title: Simple Test -sdk: react +sdk: react, nextjs --- @@ -1570,11 +1566,18 @@ sdk: react createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react'], + validSdks: ['react', 'nextjs'], }), ) expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('Test Partial Content') + expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).not.toContain( + '', + ) + expect(await readFile(pathJoin('./dist/nextjs/simple-test.mdx'))).toContain('Test Partial Content') + expect(await readFile(pathJoin('./dist/nextjs/simple-test.mdx'))).not.toContain( + '', + ) }) test('Invalid partial src fails the build', async () => { @@ -2159,68 +2162,55 @@ description: Link with code describe('Path and File Handling', () => { test('should ignore paths specified in ignorePaths during processing', async () => { - const { tempDir, pathJoin, listFiles } = await createTempFiles([ + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [ [ - { title: 'Regular Guide', href: '/docs/regular-guide' }, - { title: 'Ignored Guide', href: '/docs/ignored/ignored-guide' }, + { title: 'Core Guide', href: '/docs/core-guide' }, + { title: 'Scoped Guide', href: '/docs/scoped-guide' }, ], ], }), }, { - path: './docs/regular-guide.mdx', + path: './docs/_partials/ignored-partial.mdx', + content: `[Ignored Guide](/docs/ignored/ignored-guide)`, + }, + { + path: './docs/core-guide.mdx', content: `--- -title: Regular Guide +title: Core Guide +description: Not sdk specific guide --- -# Regular Guide Content`, + +[Ignored Guide](/docs/ignored/ignored-guide)`, }, { - path: './docs/ignored/ignored-guide.mdx', + path: './docs/scoped-guide.mdx', content: `--- -title: Ignored Guide +title: Scoped Guide +description: guide specific to react +sdk: react --- -# Ignored Guide Content`, +[Ignored Guide](/docs/ignored/ignored-guide)`, }, ]) - await build( + const output = await build( createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, validSdks: ['react'], - ignorePaths: ['/docs/ignored'], + ignorePaths: ['/docs/_partials', '/docs/ignored'], }), ) - // Check that only the regular guide was processed - const distFiles = (await listFiles()).filter((file) => file.startsWith('dist/')) - - expect(distFiles).toContain('dist/regular-guide.mdx') - expect(distFiles).toContain('dist/manifest.json') - expect(distFiles).not.toContain('dist/ignored/ignored-guide.mdx') - - // Verify that the manifest was filtered correctly - expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ - navigation: [ - [ - { - title: 'Regular Guide', - href: '/docs/regular-guide', - }, - { - title: 'Ignored Guide', - href: '/docs/ignored/ignored-guide', - }, - ], - ], - }) + expect(output).toBe('') }) test('should detect file path conflicts when a core doc path matches an SDK path', async () => { @@ -2333,11 +2323,7 @@ title: Target Page }, { path: './docs/_partials/links.mdx', - content: `--- -title: Links ---- - -[Link to Target with .mdx](/docs/target-page.mdx) + content: `[Link to Target with .mdx](/docs/target-page.mdx) [Link to Target without .mdx](/docs/target-page) [Link to Target with hash](/docs/target-page#target-page-content) [Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, @@ -2393,11 +2379,7 @@ title: Target Page }, { path: './docs/_partials/links.mdx', - content: `--- -title: Links ---- - -[Link to Target with .mdx](/docs/target-page.mdx) + content: `[Link to Target with .mdx](/docs/target-page.mdx) [Link to Target without .mdx](/docs/target-page) [Link to Target with hash](/docs/target-page#target-page-content) [Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 24c1fc6e2a..eb3b169fe7 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1498,6 +1498,9 @@ template: wide const [url, hash] = (node.url as string).split('#') + const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return node + const doc = docsMap.get(url) if (doc === undefined) { From 2e708586c68a5121b65da3207ef985b3bda412b6 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 15 Apr 2025 10:59:25 -0700 Subject: [PATCH 098/114] double pass the manifest for complete sdk filtering --- scripts/build-docs.test.ts | 189 +++++++++++++++++++++++++++++++------ scripts/build-docs.ts | 101 +++++++++++++++++++- 2 files changed, 258 insertions(+), 32 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 418b0b4775..7616bb0e19 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -175,11 +175,9 @@ description: This is a simple test page Testing with a simple page.`) expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( - JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - ) + expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }) }) test('Warning on missing description in frontmatter', async () => { @@ -365,7 +363,7 @@ title: Simple Test expect(groups[3].hideTitle).toBe(true) }) - test('should properly inherit SDK filtering from parent groups to child items', async () => { + test('should properly pass down SDK filtering from parent groups to child items', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', @@ -500,6 +498,139 @@ title: Simple Test }) }) + test('should properly inherit SDK filtering from child items up to parent groups', async () => { + const { tempDir, pathJoin } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'SDK Group', + items: [ + [ + { + title: 'Sub Group', + items: [ + [ + { title: 'SDK Item', href: '/docs/sdk-item' }, + { title: 'Nested Group', items: [[{ title: 'Nested Item', href: '/docs/nested-item' }]] }, + ], + ], + }, + ], + ], + }, + { + title: 'Generic Group', + items: [ + [ + { + title: 'Sub Group', + items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], + }, + ], + ], + }, + { + title: 'Vue Group', + items: [ + [ + { + title: 'Sub Group', + items: [[{ title: 'Vue Item', href: '/docs/vue-item' }]], + }, + ], + ], + }, + ], + ], + }), + }, + { + path: './docs/sdk-item.mdx', + content: `---\ntitle: SDK Item\nsdk: react\n---\nSDK specific content`, + }, + { + path: './docs/nested-item.mdx', + content: `---\ntitle: Nested Item\nsdk: nextjs\n---\nNested SDK specific content`, + }, + { + path: './docs/generic-item.mdx', + content: `---\ntitle: Generic Item\n---\nGeneric content`, + }, + { + path: './docs/vue-item.mdx', + content: `---\ntitle: Vue Item\nsdk: vue\n---\nVue specific content`, + }, + ]) + + await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['nextjs', 'react', 'vue'], + }), + ) + + // Check manifest + const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) + + expect(manifest).toEqual({ + navigation: [ + [ + { + title: 'SDK Group', + sdk: ['react', 'nextjs'], + items: [ + [ + { + title: 'Sub Group', + sdk: ['react', 'nextjs'], + items: [ + [ + { title: 'SDK Item', sdk: ['react'], href: '/docs/:sdk:/sdk-item' }, + { + title: 'Nested Group', + sdk: ['nextjs'], + items: [[{ title: 'Nested Item', sdk: ['nextjs'], href: '/docs/:sdk:/nested-item' }]], + }, + ], + ], + }, + ], + ], + }, + { + title: 'Generic Group', + items: [ + [ + { + title: 'Sub Group', + items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], + }, + ], + ], + }, + { + title: 'Vue Group', + sdk: ['vue'], + items: [ + [ + { + title: 'Sub Group', + sdk: ['vue'], + items: [[{ title: 'Vue Item', sdk: ['vue'], href: '/docs/:sdk:/vue-item' }]], + }, + ], + ], + }, + ], + ], + }) + }) + test('Check link and hash in partial is valid', async () => { const { tempDir } = await createTempFiles([ { @@ -647,24 +778,22 @@ title: Quickstart ) expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) - expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( - JSON.stringify({ - navigation: [ - [ - { - title: 'React', - sdk: ['react'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/react', sdk: ['react'] }]], - }, - { - title: 'Vue', - sdk: ['vue'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue', sdk: ['vue'] }]], - }, - ], + expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + navigation: [ + [ + { + title: 'React', + sdk: ['react'], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/react', sdk: ['react'] }]], + }, + { + title: 'Vue', + sdk: ['vue'], + items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue', sdk: ['vue'] }]], + }, ], - }), - ) + ], + }) const distFiles = await treeDir(pathJoin('./dist')) @@ -704,9 +833,9 @@ Testing with a simple page.`, }), ) - expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( - JSON.stringify({ navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react'] }]] }), - ) + expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react'] }]], + }) expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- title: Simple Test @@ -760,11 +889,9 @@ Testing with a simple page.`, }), ) - expect(await readFile(pathJoin('./dist/manifest.json'))).toBe( - JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react', 'vue', 'astro'] }]], - }), - ) + expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ + navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react', 'vue', 'astro'] }]], + }) const distFiles = await treeDir(pathJoin('./dist')) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index eb3b169fe7..34eee32d79 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -561,6 +561,56 @@ const traverseTree = async < ) as unknown as OutTree } +const traverseTreeItemsFirst = async < + Tree extends { items: BlankTree }, + InItem extends Extract, + InGroup extends Extract }>, + OutItem extends { href: string }, + OutGroup extends { items: BlankTree }, + OutTree extends BlankTree, +>( + tree: Tree, + itemCallback: (item: InItem, tree: Tree) => Promise = async (item) => item, + groupCallback: (group: InGroup, tree: Tree) => Promise = async (group) => group, + errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, +): Promise => { + const result = await Promise.all( + tree.items.map(async (group) => { + return await Promise.all( + group.map(async (item) => { + try { + if ('href' in item) { + return await itemCallback(item, tree) + } + + if ('items' in item && Array.isArray(item.items)) { + const newItems = (await traverseTreeItemsFirst(item, itemCallback, groupCallback, errorCallback)).map( + (group) => group.filter((item): item is NonNullable => item !== null), + ) + + const newGroup = await groupCallback({ ...item, items: newItems }, tree) + + return newGroup + } + + return item as OutItem + } catch (error) { + if (error instanceof Error && errorCallback !== undefined) { + errorCallback(item, error) + } else { + throw error + } + } + }), + ) + }), + ) + + return result.map((group) => + group.filter((item): item is NonNullable => item !== null), + ) as unknown as OutTree +} + function flattenTree< Tree extends BlankTree, InItem extends Extract, @@ -987,7 +1037,7 @@ export const build = async (store: ReturnType, config: console.info(`✓ Loaded in ${docsArray.length} docs (${cachedDocsSize} cached)`) // Goes through and grabs the sdk scoping out of the manifest - const sdkScopedManifest = await traverseTree( + const sdkScopedManifestFirstPass = await traverseTree( { items: userManifest, sdk: undefined as undefined | SDK[] }, async (item, tree) => { if (!item.href?.startsWith('/docs/')) return item @@ -1079,6 +1129,55 @@ export const build = async (store: ReturnType, config: throw error }, ) + + const sdkScopedManifest = await traverseTreeItemsFirst( + { items: sdkScopedManifestFirstPass, sdk: undefined as undefined | SDK[] }, + async (item, tree) => item, + async ({ items, ...details }, tree) => { + // This takes all the children items, grabs the sdks out of them, and combines that in to a list + const groupsItemsCombinedSDKs = (() => { + const sdks = items?.flatMap((item) => item.flatMap((item) => item.sdk)) + + if (sdks === undefined) return [] + + const uniqueSDKs = Array.from(new Set(sdks)).filter((sdk): sdk is SDK => sdk !== undefined) + return uniqueSDKs + })() + + // This is the sdk of the group + const groupSDK = details.sdk + + // This is the sdk of the parent group + const parentSDK = tree.sdk + + // If there are no children items, then we either use the group we are looking at sdks if its defined, or its parent group + if (groupsItemsCombinedSDKs.length === 0) { + return { ...details, sdk: groupSDK ?? parentSDK, items } as ManifestGroup + } + + if (groupSDK !== undefined && groupSDK.length > 0) { + return { + ...details, + sdk: groupSDK, + items, + } as ManifestGroup + } + + const combinedSDKs = Array.from(new Set([...(groupSDK ?? []), ...groupsItemsCombinedSDKs])) ?? [] + + return { + ...details, + // If there are children items, then we combine the sdks of the group and the children items sdks + sdk: combinedSDKs, + items, + } as ManifestGroup + }, + (item, error) => { + console.error('[DEBUG] Error processing item:', item.title) + console.error(error) + throw error + }, + ) console.info('✓ Applied manifest sdk scoping') if (config.cleanDist) { From 0f17c4effe62c1075fe2621b3f3f4dda0df63d91 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 23 Apr 2025 11:41:20 -0700 Subject: [PATCH 099/114] temp disable mdx parsing of typedoc, embed typedoc content in to the output --- scripts/build-docs.test.ts | 43 ++++- scripts/build-docs.ts | 375 +++++++++++++++++++++---------------- 2 files changed, 249 insertions(+), 169 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index dea25680a3..94e480aaf2 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -3403,7 +3403,6 @@ interface Client { const output = await build( createBlankStore(), - createConfig({ ...baseConfig, basePath: tempDir, @@ -3451,7 +3450,6 @@ interface Client { const output = await build( createBlankStore(), - createConfig({ ...baseConfig, basePath: tempDir, @@ -3495,7 +3493,7 @@ description: Generated API docs }) // Should fail due to missing typedoc folder - const promise = build(createBlankStore(), configWithMissingFolder) + const promise = build(createBlankStore(), configWithMissingFolder) await expect(promise).rejects.toThrow('Typedoc folder') }) @@ -3750,4 +3748,43 @@ description: Generated API docs expect(output).toContain('Hash "non-existent-hash" not found in /docs/overview') }) + + test('should embed typedoc into the doc', async () => { + const { tempDir, readFile } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'API Doc', href: '/docs/api-doc' }]], + }), + }, + { + path: './typedoc/api/client.mdx', + content: `# Client API`, + }, + { + path: './docs/api-doc.mdx', + content: `--- +title: API Documentation +description: Generated API docs +--- + +# API Documentation + + +`, + }, + ]) + + const output = await build( + createBlankStore(), + createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + expect(await readFile('./dist/api-doc.mdx')).toContain('Client API') + expect(output).toBe('') + }) }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index d110bf90b4..06e6a04f69 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -391,7 +391,9 @@ const readPartial = (config: BuildConfig) => async (filePath: string) => { let partialNode: Node | null = null - const partialContentVFile = await markdownProcessor() + const partialContentVFile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) .use(() => (tree) => { partialNode = tree }) @@ -466,7 +468,7 @@ const readPartialsMarkdown = }), ) } - + const readTypedocsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.typedocPath, { type: 'files', @@ -487,34 +489,38 @@ const readTypedocsMarkdown = (config: BuildConfig) => async (paths: string[]) => throw new Error(errorMessages['typedoc-read-error'](typedocPath), { cause: error }) } - let node: Node | null = null + try { + let node: Node | null = null - const vfile = await remark() - .use(() => (tree) => { - node = tree - }) - .process({ - path: typedocPath, - value: content, - }) + const vfile = await remark() + // .use(remarkMdx) + .use(() => (tree) => { + node = tree + }) + .process({ + path: typedocPath, + value: content, + }) - if (node === null) { - throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) - } + if (node === null) { + throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) + } - return { - path: `${removeMdxSuffix(filePath)}.mdx`, - content, - vfile, - node: node as Node, + return { + path: `${removeMdxSuffix(filePath)}.mdx`, + content, + vfile, + node: node as Node, + } + } catch (error) { + console.error(`✗ Error parsing typedoc: ${typedocPath}`) + throw error } }), ) } -const markdownProcessor = remark().use(remarkFrontmatter).use(remarkMdx).freeze() - -type VFile = Awaited> +type VFile = Awaited> const ensureDirectory = (config: BuildConfig) => @@ -890,7 +896,9 @@ const parseInMarkdownFile = const filePath = `${href}.mdx` let node: Node | undefined = undefined - const vfile = await markdownProcessor() + const vfile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) .use(() => (tree, vfile) => { node = tree @@ -1008,7 +1016,9 @@ const parseInMarkdownFile = // This needs to be done separately as some further validation expects the partials to not be embedded // but we need to embed it to get all the headings to check - await markdownProcessor() + await remark() + .use(remarkFrontmatter) + .use(remarkMdx) // Embed the partial .use(() => (tree, vfile) => { return mdastMap(tree, (node) => { @@ -1139,7 +1149,7 @@ export const build = async (store: ReturnType, config: console.info(`✓ Loaded in ${partials.length} partials (${cachedPartialsSize} cached)`) const typedocs = await getTypedocsMarkdown((await getTypedocsFolder()).map((item) => item.path)) - console.info(`✔️ Read ${typedocs.length} Typedocs`) + console.info(`✓ Read ${typedocs.length} Typedocs`) const docsMap = new Map>>() const docsInManifest = new Set() @@ -1373,49 +1383,75 @@ export const build = async (store: ReturnType, config: partials.map(async (partial) => { const partialPath = `/docs/_partials/${partial.path}` - let node: Node | null = null + try { + let node: Node | null = null - const vfile = await markdownProcessor() - // validate links in partials to docs are valid and replace the links to sdk scoped pages with the sdk link component - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node - if (!('children' in node)) return node + const vfile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) + // validate links in partials to docs are valid and replace the links to sdk scoped pages with the sdk link component + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node - // we are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) + // we are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) - const [url, hash] = (node.url as string).split('#') + const [url, hash] = (node.url as string).split('#') - const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return node + const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return node - const doc = docsMap.get(url) + const doc = docsMap.get(url) - if (doc === undefined) { - safeMessage(config, vfile, partialPath, 'link-doc-not-found', [url], node.position) - return node - } + if (doc === undefined) { + safeMessage(config, vfile, partialPath, 'link-doc-not-found', [url], node.position) + return node + } - if (hash !== undefined) { - const hasHash = doc.headingsHashes.has(hash) + if (hash !== undefined) { + const hasHash = doc.headingsHashes.has(hash) - if (hasHash === false) { - safeMessage(config, vfile, partialPath, 'link-hash-not-found', [hash, url], node.position) + if (hasHash === false) { + safeMessage(config, vfile, partialPath, 'link-hash-not-found', [hash, url], node.position) + } } - } - if (doc.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience + if (doc.sdk !== undefined) { + // we are going to swap it for the sdk link component to give the users a great experience - const firstChild = node.children?.[0] - const childIsCodeBlock = firstChild?.type === 'inlineCode' + const firstChild = node.children?.[0] + const childIsCodeBlock = firstChild?.type === 'inlineCode' - if (childIsCodeBlock) { - firstChild.type = 'text' + if (childIsCodeBlock) { + firstChild.type = 'text' + + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: scopeHrefToSDK(url, ':sdk:'), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(doc.sdk), + }), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'code', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: childIsCodeBlock, + }), + }), + ], + }) + } return mdastBuilder('mdxJsxTextElement', { name: 'SDKLink', @@ -1430,50 +1466,31 @@ export const build = async (store: ReturnType, config: value: JSON.stringify(doc.sdk), }), }), - mdastBuilder('mdxJsxAttribute', { - name: 'code', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: childIsCodeBlock, - }), - }), ], + children: node.children, }) } - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(doc.sdk), - }), - }), - ], - children: node.children, - }) - } - - return node + return node + }) }) - }) - .use(() => (tree, vfile) => { - node = tree - }) - .process(partial.vfile) + .use(() => (tree, vfile) => { + node = tree + }) + .process(partial.vfile) - if (node === null) { - throw new Error(errorMessages['partial-parse-error'](partial.path)) - } + if (node === null) { + throw new Error(errorMessages['partial-parse-error'](partial.path)) + } - return { - ...partial, - node: node as Node, - vfile, + return { + ...partial, + node: node as Node, + vfile, + } + } catch (error) { + console.error(`✗ Error validating partial: ${partial.path}`) + throw error } }), ) @@ -1483,49 +1500,74 @@ export const build = async (store: ReturnType, config: typedocs.map(async (typedoc) => { const filePath = path.join(config.typedocRelativePath, typedoc.path) - let node: Node | null = null + try { + let node: Node | null = null - const vfile = await markdownProcessor() - // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component - .use(() => (tree: Node, vfile: VFile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node - if (!('children' in node)) return node + const vfile = await remark() + // .use(remarkMdx) + // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component + .use(() => (tree: Node, vfile: VFile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node - // we are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) + // we are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) - const [url, hash] = (node.url as string).split('#') + const [url, hash] = (node.url as string).split('#') - const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return node + const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return node - const doc = docsMap.get(url) + const doc = docsMap.get(url) - if (doc === undefined) { - safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) - return node - } + if (doc === undefined) { + safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) + return node + } - if (hash !== undefined) { - const hasHash = doc.headingsHashes.has(hash) + if (hash !== undefined) { + const hasHash = doc.headingsHashes.has(hash) - if (hasHash === false) { - safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) + if (hasHash === false) { + safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) + } } - } - if (doc.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience + if (doc.sdk !== undefined) { + // we are going to swap it for the sdk link component to give the users a great experience - const firstChild = node.children?.[0] - const childIsCodeBlock = firstChild?.type === 'inlineCode' + const firstChild = node.children?.[0] + const childIsCodeBlock = firstChild?.type === 'inlineCode' - if (childIsCodeBlock) { - firstChild.type = 'text' + if (childIsCodeBlock) { + firstChild.type = 'text' + + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: scopeHrefToSDK(url, ':sdk:'), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(doc.sdk), + }), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'code', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: childIsCodeBlock, + }), + }), + ], + }) + } return mdastBuilder('mdxJsxTextElement', { name: 'SDKLink', @@ -1540,60 +1582,43 @@ export const build = async (store: ReturnType, config: value: JSON.stringify(doc.sdk), }), }), - mdastBuilder('mdxJsxAttribute', { - name: 'code', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: childIsCodeBlock, - }), - }), ], + children: node.children, }) } - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(doc.sdk), - }), - }), - ], - children: node.children, - }) - } - - return node + return node + }) }) - }) - .use(() => (tree, vfile) => { - node = tree - }) - .process(typedoc.vfile) + .use(() => (tree, vfile) => { + node = tree + }) + .process(typedoc.vfile) - if (node === null) { - throw new Error(errorMessages['typedoc-parse-error'](typedoc.path)) - } + if (node === null) { + throw new Error(errorMessages['typedoc-parse-error'](typedoc.path)) + } - return { - ...typedoc, - vfile, - node: node as Node, + return { + ...typedoc, + vfile, + node: node as Node, + } + } catch (error) { + console.error(`✗ Error validating typedoc: ${typedoc.path}`) + throw error } - }) + }), ) - console.info(`✔️ Validated all typedocs`) + console.info(`✓ Validated all typedocs`) const coreVFiles = await Promise.all( docsArray.map(async (doc) => { const filePath = `${doc.href}.mdx` - const vfile = await markdownProcessor() + const vfile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component .use(() => (tree: Node, vfile: VFile) => { return mdastMap(tree, (node) => { @@ -1748,6 +1773,20 @@ export const build = async (store: ReturnType, config: return Object.assign(node, partial.node) }) }) + // embed the typedoc into the doc + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + const typedocSrc = extractComponentPropValueFromNode(config, node, vfile, 'Typedoc', 'src', true, filePath) + + if (typedocSrc === undefined) return node + + const typedoc = validatedTypedocs.find((typedoc) => typedoc.path === `${removeMdxSuffix(typedocSrc)}.mdx`) + + if (typedoc === undefined) return node // a warning will have already been reported + + return Object.assign(node, typedoc.node) + }) + }) .process(doc.vfile) const distFilePath = `${doc.href.replace('/docs/', '')}.mdx` @@ -1789,7 +1828,9 @@ template: wide if (doc.sdk.includes(targetSdk) === false) return null // skip docs that are not for the target sdk const filePath = `${doc.href}.mdx` - const vfile = await markdownProcessor() + const vfile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) // filter out content that is only available to other sdk's .use(() => (tree, vfile) => { return mdastFilter(tree, (node) => { @@ -1932,7 +1973,9 @@ template: wide // For each SDK, check heading uniqueness after filtering for (const sdk of availableSDKs) { - const vfile = await markdownProcessor() + await remark() + .use(remarkFrontmatter) + .use(remarkMdx) .use(() => (inputTree) => { return mdastFilter(inputTree, (node) => { const sdkProp = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', false, filePath) From 6c3912a49e7a7190f4aaf8f18496f448cd3c3e29 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 23 Apr 2025 13:49:46 -0700 Subject: [PATCH 100/114] cache and invalidate typedoc markdown on file changes --- scripts/build-docs.ts | 202 ++++++++++++++++++++++++------------------ 1 file changed, 115 insertions(+), 87 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 06e6a04f69..3ce805bc72 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -391,60 +391,65 @@ const readPartial = (config: BuildConfig) => async (filePath: string) => { let partialNode: Node | null = null - const partialContentVFile = await remark() - .use(remarkFrontmatter) - .use(remarkMdx) - .use(() => (tree) => { - partialNode = tree - }) - .use(() => (tree, vfile) => { - mdastVisit( - tree, - (node) => - (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && - 'name' in node && - node.name === 'Include', - (node) => { - safeFail(config, vfile, fullPath, 'partials-inside-partials', [], node.position) - }, - ) - }) - // Process links in partials and remove the .mdx suffix - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node - if (!('children' in node)) return node - - // We are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - return node + try { + const partialContentVFile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) + .use(() => (tree) => { + partialNode = tree }) - }) - .process({ - path: `docs/_partials/${filePath}`, - value: content, - }) + .use(() => (tree, vfile) => { + mdastVisit( + tree, + (node) => + (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && + 'name' in node && + node.name === 'Include', + (node) => { + safeFail(config, vfile, fullPath, 'partials-inside-partials', [], node.position) + }, + ) + }) + // Process links in partials and remove the .mdx suffix + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node - const partialContentReport = reporter([partialContentVFile], { quiet: true }) + // We are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) - if (partialContentReport !== '') { - console.error(partialContentReport) - process.exit(1) - } + return node + }) + }) + .process({ + path: `docs/_partials/${filePath}`, + value: content, + }) - if (partialNode === null) { - throw new Error(errorMessages['partial-parse-error'](filePath)) - } + const partialContentReport = reporter([partialContentVFile], { quiet: true }) - return { - path: filePath, - content, - vfile: partialContentVFile, - node: partialNode as Node, + if (partialContentReport !== '') { + console.error(partialContentReport) + process.exit(1) + } + + if (partialNode === null) { + throw new Error(errorMessages['partial-parse-error'](filePath)) + } + + return { + path: filePath, + content, + vfile: partialContentVFile, + node: partialNode as Node, + } + } catch (error) { + console.error(`✗ Error parsing partial: ${filePath}`) + throw error } } @@ -476,50 +481,67 @@ const readTypedocsFolder = (config: BuildConfig) => async () => { }) } -const readTypedocsMarkdown = (config: BuildConfig) => async (paths: string[]) => { +const readTypedoc = (config: BuildConfig) => async (filePath: string) => { const readFile = readMarkdownFile(config) - return Promise.all( - paths.map(async (filePath) => { - const typedocPath = path.join(config.typedocRelativePath, filePath) + const typedocPath = path.join(config.typedocRelativePath, filePath) - const [error, content] = await readFile(typedocPath) + const [error, content] = await readFile(typedocPath) - if (error) { - throw new Error(errorMessages['typedoc-read-error'](typedocPath), { cause: error }) - } + if (error) { + throw new Error(errorMessages['typedoc-read-error'](typedocPath), { cause: error }) + } - try { - let node: Node | null = null + try { + let node: Node | null = null - const vfile = await remark() - // .use(remarkMdx) - .use(() => (tree) => { - node = tree - }) - .process({ - path: typedocPath, - value: content, - }) + const vfile = await remark() + // .use(remarkMdx) + .use(() => (tree) => { + node = tree + }) + .process({ + path: typedocPath, + value: content, + }) - if (node === null) { - throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) - } + if (node === null) { + throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) + } - return { - path: `${removeMdxSuffix(filePath)}.mdx`, - content, - vfile, - node: node as Node, - } - } catch (error) { - console.error(`✗ Error parsing typedoc: ${typedocPath}`) - throw error - } - }), - ) + return { + path: `${removeMdxSuffix(filePath)}.mdx`, + content, + vfile, + node: node as Node, + } + } catch (error) { + console.error(`✗ Error parsing typedoc: ${typedocPath}`) + throw error + } } +const readTypedocsMarkdown = + (config: BuildConfig, store: ReturnType) => async (paths: string[]) => { + const read = readTypedoc(config) + + return Promise.all( + paths.map(async (filePath) => { + const cachedValue = store.typedocsFiles.get(filePath) + + if (cachedValue !== undefined) { + return cachedValue + } + + const typedoc = await read(filePath) + + store.typedocsFiles.set(filePath, typedoc) + + return typedoc + }), + ) + } + type VFile = Awaited> const ensureDirectory = @@ -1121,6 +1143,7 @@ const parseInMarkdownFile = export const createBlankStore = () => ({ markdownFiles: new Map>>>(), partialsFiles: new Map>>>(), + typedocsFiles: new Map>>>(), }) export const build = async (store: ReturnType, config: BuildConfig) => { @@ -1131,7 +1154,7 @@ export const build = async (store: ReturnType, config: const getPartialsFolder = readPartialsFolder(config) const getPartialsMarkdown = readPartialsMarkdown(config, store) const getTypedocsFolder = readTypedocsFolder(config) - const getTypedocsMarkdown = readTypedocsMarkdown(config) + const getTypedocsMarkdown = readTypedocsMarkdown(config, store) const parseMarkdownFile = parseInMarkdownFile(config) const writeFile = writeDistFile(config) const writeSdkFile = writeSDKFile(config) @@ -1148,8 +1171,9 @@ export const build = async (store: ReturnType, config: const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) console.info(`✓ Loaded in ${partials.length} partials (${cachedPartialsSize} cached)`) + const cachedTypedocsSize = store.typedocsFiles.size const typedocs = await getTypedocsMarkdown((await getTypedocsFolder()).map((item) => item.path)) - console.info(`✓ Read ${typedocs.length} Typedocs`) + console.info(`✓ Read ${typedocs.length} Typedocs (${cachedTypedocsSize} cached)`) const docsMap = new Map>>() const docsInManifest = new Set() @@ -2036,12 +2060,13 @@ export const invalidateFile = (store: ReturnType, config: BuildConfig) => (filePath: string) => { store.markdownFiles.delete(removeMdxSuffix(`/docs/${path.relative(config.docsPath, filePath)}`)) store.partialsFiles.delete(path.relative(config.partialsPath, filePath)) + store.typedocsFiles.delete(path.relative(config.typedocPath, filePath)) } const watchAndRebuild = (store: ReturnType, config: BuildConfig) => { const invalidate = invalidateFile(store, config) - watcher.subscribe(config.docsPath, async (error, events) => { + const handleFileChange: watcher.SubscribeCallback = async (error, events) => { if (error !== null) { console.error(error) return @@ -2072,7 +2097,10 @@ const watchAndRebuild = (store: ReturnType, config: Bui return } - }) + } + + watcher.subscribe(config.docsPath, handleFileChange) + watcher.subscribe(config.typedocPath, handleFileChange) } type BuildConfigOptions = { From 2b58dcfeb0a05a2e6d0aaeae09198bc44828bf77 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 23 Apr 2025 14:22:50 -0700 Subject: [PATCH 101/114] fallback to parsing the typedoc markdown without parsing out the mdx if it errors --- scripts/build-docs.ts | 161 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 6 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 3ce805bc72..9667fc37ed 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -496,10 +496,25 @@ const readTypedoc = (config: BuildConfig) => async (filePath: string) => { let node: Node | null = null const vfile = await remark() - // .use(remarkMdx) + .use(remarkMdx) .use(() => (tree) => { node = tree }) + // Process links in typedocs and remove the .mdx suffix + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node + + // We are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + return node + }) + }) .process({ path: typedocPath, value: content, @@ -516,8 +531,42 @@ const readTypedoc = (config: BuildConfig) => async (filePath: string) => { node: node as Node, } } catch (error) { - console.error(`✗ Error parsing typedoc: ${typedocPath}`) - throw error + let node: Node | null = null + + const vfile = await remark() + .use(() => (tree) => { + node = tree + }) + // Process links in typedocs and remove the .mdx suffix + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node + + // We are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + return node + }) + }) + .process({ + path: typedocPath, + value: content, + }) + + if (node === null) { + throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) + } + + return { + path: `${removeMdxSuffix(filePath)}.mdx`, + content, + vfile, + node: node as Node, + } } } @@ -1528,7 +1577,7 @@ export const build = async (store: ReturnType, config: let node: Node | null = null const vfile = await remark() - // .use(remarkMdx) + .use(remarkMdx) // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component .use(() => (tree: Node, vfile: VFile) => { return mdastMap(tree, (node) => { @@ -1629,8 +1678,108 @@ export const build = async (store: ReturnType, config: node: node as Node, } } catch (error) { - console.error(`✗ Error validating typedoc: ${typedoc.path}`) - throw error + let node: Node | null = null + + const vfile = await remark() + // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component + .use(() => (tree: Node, vfile: VFile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node + + // we are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + const [url, hash] = (node.url as string).split('#') + + const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return node + + const doc = docsMap.get(url) + + if (doc === undefined) { + safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) + return node + } + + if (hash !== undefined) { + const hasHash = doc.headingsHashes.has(hash) + + if (hasHash === false) { + safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) + } + } + + if (doc.sdk !== undefined) { + // we are going to swap it for the sdk link component to give the users a great experience + + const firstChild = node.children?.[0] + const childIsCodeBlock = firstChild?.type === 'inlineCode' + + if (childIsCodeBlock) { + firstChild.type = 'text' + + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: scopeHrefToSDK(url, ':sdk:'), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(doc.sdk), + }), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'code', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: childIsCodeBlock, + }), + }), + ], + }) + } + + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: scopeHrefToSDK(url, ':sdk:'), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(doc.sdk), + }), + }), + ], + children: node.children, + }) + } + + return node + }) + }) + .use(() => (tree, vfile) => { + node = tree + }) + .process(typedoc.vfile) + + if (node === null) { + throw new Error(errorMessages['typedoc-parse-error'](typedoc.path)) + } + + return { + ...typedoc, + vfile, + node: node as Node, + } } }), ) From 3aa51c24a9923fc71c64e353a2511d675dbe5475 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 23 Apr 2025 15:01:55 -0700 Subject: [PATCH 102/114] remove typedoc fake submodule --- clerk-typedoc | 1 - 1 file changed, 1 deletion(-) delete mode 160000 clerk-typedoc diff --git a/clerk-typedoc b/clerk-typedoc deleted file mode 160000 index dfc9376582..0000000000 --- a/clerk-typedoc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dfc937658213713eaedc3c5ddc505716bc70b96f From 158b28baeb718ed9a42730fc5dd8f217dbfffae7 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 23 Apr 2025 16:36:35 -0700 Subject: [PATCH 103/114] Extract out some re-used code in to functions --- scripts/build-docs.ts | 723 ++++++++++++++---------------------------- 1 file changed, 246 insertions(+), 477 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 9667fc37ed..5629433bff 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -981,6 +981,7 @@ const parseInMarkdownFile = safeFail(config, vfile, filePath, 'invalid-href-encoding', [href]) } }) + // validate and extract out the frontmatter .use(() => (tree, vfile) => { mdastVisit( tree, @@ -1028,58 +1029,8 @@ const parseInMarkdownFile = return } }) - // Validate the - .use(() => (tree, vfile) => { - return mdastVisit(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) - - if (partialSrc === undefined) return - - if (partialSrc.startsWith('_partials/') === false) { - safeMessage(config, vfile, filePath, 'include-src-not-partials', [], node.position) - return - } - - const partial = partials.find( - (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, - ) - - if (partial === undefined) { - safeMessage(config, vfile, filePath, 'partial-not-found', [removeMdxSuffix(partialSrc)], node.position) - return - } - }) - }) - // Validate the - .use(() => (tree, vfile) => { - return mdastVisit(tree, (node) => { - const typedocSrc = extractComponentPropValueFromNode(config, node, vfile, 'Typedoc', 'src', true, filePath) - - if (typedocSrc === undefined) return - - const typedocFolderExists = existsSync(config.typedocPath) - - if (typedocFolderExists === false) { - throw new Error(errorMessages['typedoc-folder-not-found'](config.typedocPath)) - } - - const typedoc = typedocs.find((typedoc) => typedoc.path === `${removeMdxSuffix(typedocSrc)}.mdx`) - - if (typedoc === undefined) { - safeMessage( - config, - vfile, - filePath, - 'typedoc-not-found', - [`${removeMdxSuffix(typedocSrc)}.mdx`], - node.position, - ) - return - } - - return - }) - }) + .use(checkPartials(config, partials, filePath, { reportWarnings: true, embed: false })) + .use(checkTypedoc(config, typedocs, filePath, { reportWarnings: true, embed: false })) .process({ path: `${href.substring(1)}.mdx`, value: fileContent, @@ -1090,54 +1041,8 @@ const parseInMarkdownFile = await remark() .use(remarkFrontmatter) .use(remarkMdx) - // Embed the partial - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) - - if (partialSrc === undefined) return node - - if (partialSrc.startsWith('_partials/') === false) { - safeMessage(config, vfile, filePath, 'include-src-not-partials', [], node.position) - return node - } - - const partial = partials.find( - (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, - ) - - if (partial === undefined) { - safeMessage(config, vfile, filePath, 'partial-not-found', [removeMdxSuffix(partialSrc)], node.position) - return node - } - - return Object.assign(node, partial.node) - }) - }) - // Embed the typedoc - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - const typedocSrc = extractComponentPropValueFromNode(config, node, vfile, 'Typedoc', 'src', true, filePath) - - if (typedocSrc === undefined) return node - - const typedoc = typedocs.find((typedoc) => typedoc.path === `${removeMdxSuffix(typedocSrc)}.mdx`) - - if (typedoc === undefined) { - safeMessage( - config, - vfile, - filePath, - 'typedoc-not-found', - [`${removeMdxSuffix(typedocSrc)}.mdx`], - node.position, - ) - return node - } - - return Object.assign(node, typedoc.node) - }) - }) + .use(checkPartials(config, partials, filePath, { reportWarnings: true, embed: true })) + .use(checkTypedoc(config, typedocs, filePath, { reportWarnings: true, embed: true })) // extract out the headings to check hashes in links .use(() => (tree, vfile) => { const documentContainsIfComponent = documentHasIfComponents(tree) @@ -1195,6 +1100,8 @@ export const createBlankStore = () => ({ typedocsFiles: new Map>>>(), }) +type DocsMap = Map>>> + export const build = async (store: ReturnType, config: BuildConfig) => { // Apply currying to create functions pre-configured with config const ensureDir = ensureDirectory(config) @@ -1224,7 +1131,7 @@ export const build = async (store: ReturnType, config: const typedocs = await getTypedocsMarkdown((await getTypedocsFolder()).map((item) => item.path)) console.info(`✓ Read ${typedocs.length} Typedocs (${cachedTypedocsSize} cached)`) - const docsMap = new Map>>() + const docsMap: DocsMap = new Map() const docsInManifest = new Set() // Grab all the docs links in the manifest @@ -1462,91 +1369,7 @@ export const build = async (store: ReturnType, config: const vfile = await remark() .use(remarkFrontmatter) .use(remarkMdx) - // validate links in partials to docs are valid and replace the links to sdk scoped pages with the sdk link component - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node - if (!('children' in node)) return node - - // we are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - const [url, hash] = (node.url as string).split('#') - - const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return node - - const doc = docsMap.get(url) - - if (doc === undefined) { - safeMessage(config, vfile, partialPath, 'link-doc-not-found', [url], node.position) - return node - } - - if (hash !== undefined) { - const hasHash = doc.headingsHashes.has(hash) - - if (hasHash === false) { - safeMessage(config, vfile, partialPath, 'link-hash-not-found', [hash, url], node.position) - } - } - - if (doc.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience - - const firstChild = node.children?.[0] - const childIsCodeBlock = firstChild?.type === 'inlineCode' - - if (childIsCodeBlock) { - firstChild.type = 'text' - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(doc.sdk), - }), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'code', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: childIsCodeBlock, - }), - }), - ], - }) - } - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(doc.sdk), - }), - }), - ], - children: node.children, - }) - } - - return node - }) - }) + .use(validateAndEmbedLinks(config, docsMap, partialPath)) .use(() => (tree, vfile) => { node = tree }) @@ -1578,91 +1401,7 @@ export const build = async (store: ReturnType, config: const vfile = await remark() .use(remarkMdx) - // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component - .use(() => (tree: Node, vfile: VFile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node - if (!('children' in node)) return node - - // we are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - const [url, hash] = (node.url as string).split('#') - - const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return node - - const doc = docsMap.get(url) - - if (doc === undefined) { - safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) - return node - } - - if (hash !== undefined) { - const hasHash = doc.headingsHashes.has(hash) - - if (hasHash === false) { - safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) - } - } - - if (doc.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience - - const firstChild = node.children?.[0] - const childIsCodeBlock = firstChild?.type === 'inlineCode' - - if (childIsCodeBlock) { - firstChild.type = 'text' - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(doc.sdk), - }), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'code', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: childIsCodeBlock, - }), - }), - ], - }) - } - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(doc.sdk), - }), - }), - ], - children: node.children, - }) - } - - return node - }) - }) + .use(validateAndEmbedLinks(config, docsMap, filePath)) .use(() => (tree, vfile) => { node = tree }) @@ -1681,91 +1420,7 @@ export const build = async (store: ReturnType, config: let node: Node | null = null const vfile = await remark() - // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component - .use(() => (tree: Node, vfile: VFile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node - if (!('children' in node)) return node - - // we are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - const [url, hash] = (node.url as string).split('#') - - const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) - if (ignore === true) return node - - const doc = docsMap.get(url) - - if (doc === undefined) { - safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) - return node - } - - if (hash !== undefined) { - const hasHash = doc.headingsHashes.has(hash) - - if (hasHash === false) { - safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) - } - } - - if (doc.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience - - const firstChild = node.children?.[0] - const childIsCodeBlock = firstChild?.type === 'inlineCode' - - if (childIsCodeBlock) { - firstChild.type = 'text' - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(doc.sdk), - }), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'code', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: childIsCodeBlock, - }), - }), - ], - }) - } - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(doc.sdk), - }), - }), - ], - children: node.children, - }) - } - - return node - }) - }) + .use(validateAndEmbedLinks(config, docsMap, filePath)) .use(() => (tree, vfile) => { node = tree }) @@ -1838,43 +1493,17 @@ export const build = async (store: ReturnType, config: if (childIsCodeBlock) { firstChild.type = 'text' - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(linkedDoc.sdk), - }), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'code', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: childIsCodeBlock, - }), - }), - ], + return SDKLink({ + href: scopeHrefToSDK(url, ':sdk:'), + sdks: linkedDoc.sdk, + code: true, }) } - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: scopeHrefToSDK(url, ':sdk:'), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(linkedDoc.sdk), - }), - }), - ], + return SDKLink({ + href: scopeHrefToSDK(url, ':sdk:'), + sdks: linkedDoc.sdk, + code: false, children: node.children, }) } @@ -1930,36 +1559,8 @@ export const build = async (store: ReturnType, config: }) }) }) - // embed the partials into the doc - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) - - if (partialSrc === undefined) return node - - const partial = validatedPartials.find( - (partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`, - ) - - if (partial === undefined) return node // a warning will have already been reported - - return Object.assign(node, partial.node) - }) - }) - // embed the typedoc into the doc - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - const typedocSrc = extractComponentPropValueFromNode(config, node, vfile, 'Typedoc', 'src', true, filePath) - - if (typedocSrc === undefined) return node - - const typedoc = validatedTypedocs.find((typedoc) => typedoc.path === `${removeMdxSuffix(typedocSrc)}.mdx`) - - if (typedoc === undefined) return node // a warning will have already been reported - - return Object.assign(node, typedoc.node) - }) - }) + .use(checkPartials(config, validatedPartials, filePath, { reportWarnings: false, embed: true })) + .use(checkTypedoc(config, validatedTypedocs, filePath, { reportWarnings: false, embed: true })) .process(doc.vfile) const distFilePath = `${doc.href.replace('/docs/', '')}.mdx` @@ -2025,35 +1626,7 @@ template: wide return false }) }) - // Validate unique heading ids - .use(() => (tree, vfile) => { - const headingsHashes = new Set() - const slugify = slugifyWithCounter() - - mdastVisit( - tree, - (node) => node.type === 'heading', - (node) => { - const id = extractHeadingFromHeadingNode(node) - - if (id !== undefined) { - if (headingsHashes.has(id)) { - safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, id]) - } - - headingsHashes.add(id) - } else { - const slug = slugify(toString(node).trim()) - - if (headingsHashes.has(slug)) { - safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, slug]) - } - - headingsHashes.add(slug) - } - }, - ) - }) + .use(validateUniqueHeadings(config, filePath)) // scope urls so they point to the current sdk .use(() => (tree, vfile) => { return mdastMap(tree, (node) => { @@ -2160,34 +1733,7 @@ template: wide return ifSdks.includes(sdk) }) }) - .use(() => (inputTree, vfile) => { - const headingsHashes = new Set() - const slugify = slugifyWithCounter() - - mdastVisit( - inputTree, - (node) => node.type === 'heading', - (node) => { - const id = extractHeadingFromHeadingNode(node) - - if (id !== undefined) { - if (headingsHashes.has(id)) { - safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, id]) - } - - headingsHashes.add(id) - } else { - const slug = slugify(toString(node).trim()) - - if (headingsHashes.has(slug)) { - safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, slug]) - } - - headingsHashes.add(slug) - } - }, - ) - }) + .use(validateUniqueHeadings(config, filePath)) .process({ path: filePath, value: String(doc.vfile), @@ -2391,3 +1937,226 @@ const main = async () => { if (require.main === module) { main() } + +const SDKLink = ( + props: { href: string; sdks: SDK[]; code: true } | { href: string; sdks: SDK[]; code: false; children: unknown }, +) => { + if (props.code) { + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: props.href, + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(props.sdks), + }), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'code', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: props.code, + }), + }), + ], + }) + } + + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: props.href, + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(props.sdks), + }), + }), + ], + children: props.children, + }) +} + +// Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component +const validateAndEmbedLinks = + (config: BuildConfig, docsMap: DocsMap, filePath: string) => () => (tree: Node, vfile: VFile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith('/docs/')) return node + if (!('children' in node)) return node + + // we are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + const [url, hash] = (node.url as string).split('#') + + const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + if (ignore === true) return node + + const doc = docsMap.get(url) + + if (doc === undefined) { + safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) + return node + } + + if (hash !== undefined) { + const hasHash = doc.headingsHashes.has(hash) + + if (hasHash === false) { + safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) + } + } + + if (doc.sdk !== undefined) { + // we are going to swap it for the sdk link component to give the users a great experience + + const firstChild = node.children?.[0] + const childIsCodeBlock = firstChild?.type === 'inlineCode' + + if (childIsCodeBlock) { + firstChild.type = 'text' + + return SDKLink({ + href: scopeHrefToSDK(url, ':sdk:'), + sdks: doc.sdk, + code: true, + }) + } + + return SDKLink({ + href: scopeHrefToSDK(url, ':sdk:'), + sdks: doc.sdk, + code: false, + children: node.children, + }) + } + + return node + }) + } + +const checkPartials = + ( + config: BuildConfig, + partials: { + node: Node + path: string + }[], + filePath: string, + options: { + reportWarnings: boolean + embed: boolean + }, + ) => + () => + (tree: Node, vfile: VFile) => { + mdastMap(tree, (node) => { + const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) + + if (partialSrc === undefined) return node + + if (partialSrc.startsWith('_partials/') === false) { + if (options.reportWarnings === true) { + safeMessage(config, vfile, filePath, 'include-src-not-partials', [], node.position) + } + return node + } + + const partial = partials.find((partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`) + + if (partial === undefined) { + if (options.reportWarnings === true) { + safeMessage(config, vfile, filePath, 'partial-not-found', [removeMdxSuffix(partialSrc)], node.position) + } + return node + } + + if (options.embed === true) { + return Object.assign(node, partial.node) + } + + return node + }) + } + +const checkTypedoc = + ( + config: BuildConfig, + typedocs: { path: string; node: Node }[], + filePath: string, + options: { reportWarnings: boolean; embed: boolean }, + ) => + () => + (tree: Node, vfile: VFile) => { + mdastMap(tree, (node) => { + const typedocSrc = extractComponentPropValueFromNode(config, node, vfile, 'Typedoc', 'src', true, filePath) + + if (typedocSrc === undefined) return node + + const typedocFolderExists = existsSync(config.typedocPath) + + if (typedocFolderExists === false && options.reportWarnings === true) { + throw new Error(errorMessages['typedoc-folder-not-found'](config.typedocPath)) + } + + const typedoc = typedocs.find(({ path }) => path === `${removeMdxSuffix(typedocSrc)}.mdx`) + + if (typedoc === undefined) { + if (options.reportWarnings === true) { + safeMessage( + config, + vfile, + filePath, + 'typedoc-not-found', + [`${removeMdxSuffix(typedocSrc)}.mdx`], + node.position, + ) + } + return node + } + + if (options.embed === true) { + return Object.assign(node, typedoc.node) + } + + return node + }) + } + +const validateUniqueHeadings = (config: BuildConfig, filePath: string) => () => (tree: Node, vfile: VFile) => { + const headingsHashes = new Set() + const slugify = slugifyWithCounter() + + mdastVisit( + tree, + (node) => node.type === 'heading', + (node) => { + const id = extractHeadingFromHeadingNode(node) + + if (id !== undefined) { + if (headingsHashes.has(id)) { + safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, id]) + } + + headingsHashes.add(id) + } else { + const slug = slugify(toString(node).trim()) + + if (headingsHashes.has(slug)) { + safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, slug]) + } + + headingsHashes.add(slug) + } + }, + ) +} From 622f6b61cac353a3953261b31b4c38ca16f61987 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 23 Apr 2025 18:15:27 -0700 Subject: [PATCH 104/114] wip break down monolithic build script --- scripts/build-docs.test.ts | 86 +++- scripts/build-docs.ts | 799 +++++++++--------------------- scripts/lib/components/SDKLink.ts | 47 ++ scripts/lib/config.ts | 79 +++ scripts/lib/error-messages.ts | 134 +++++ scripts/lib/manifest.ts | 94 ++++ scripts/lib/validators.ts | 104 ++++ 7 files changed, 759 insertions(+), 584 deletions(-) create mode 100644 scripts/lib/components/SDKLink.ts create mode 100644 scripts/lib/config.ts create mode 100644 scripts/lib/error-messages.ts create mode 100644 scripts/lib/manifest.ts create mode 100644 scripts/lib/validators.ts diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 94e480aaf2..7c9af0da9a 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -4,7 +4,8 @@ import os from 'node:os' import { glob } from 'glob' import { describe, expect, onTestFinished, test } from 'vitest' -import { build, createConfig, createBlankStore, invalidateFile } from './build-docs' +import { build, createBlankStore, invalidateFile } from './build-docs' +import { createConfig } from './lib/config' const tempConfig = { // Set to true to use local repo temp directory instead of system temp @@ -117,12 +118,17 @@ function treeDir(baseDir: string) { const baseConfig = { docsPath: '../docs', + baseDocsLink: '/docs/', manifestPath: '../docs/manifest.json', partialsPath: '../docs/_partials', - distPath: '../dist', typedocPath: '../typedoc', - ignorePaths: ['/docs/_partials'], - ignoreWarnings: {}, + distPath: '../dist', + ignoreLinks: [], + ignoreWarnings: { + docs: {}, + partials: {}, + typedoc: {}, + }, manifestOptions: { wrapDefault: true, collapseDefault: false, @@ -2448,7 +2454,7 @@ sdk: react ...baseConfig, basePath: tempDir, validSdks: ['react'], - ignorePaths: ['/docs/_partials', '/docs/ignored'], + ignoreLinks: ['/docs/ignored'], }), ) @@ -2979,7 +2985,11 @@ description: This page has a description basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/index.mdx': ['doc-not-in-manifest'], + docs: { + 'index.mdx': ['doc-not-in-manifest'], + }, + partials: {}, + typedoc: {}, }, }), ) @@ -3023,7 +3033,11 @@ description: This page has a description basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/problem-file.mdx': ['doc-not-in-manifest', 'link-doc-not-found', 'invalid-sdk-in-if'], + docs: { + 'problem-file.mdx': ['doc-not-in-manifest', 'link-doc-not-found', 'invalid-sdk-in-if'], + }, + partials: {}, + typedoc: {}, }, }), ) @@ -3070,8 +3084,12 @@ description: This page has a description basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/file1.mdx': ['doc-not-in-manifest', 'link-doc-not-found'], - '/docs/file2.mdx': ['doc-not-in-manifest', 'link-doc-not-found'], + docs: { + 'file1.mdx': ['doc-not-in-manifest', 'link-doc-not-found'], + 'file2.mdx': ['doc-not-in-manifest', 'link-doc-not-found'], + }, + partials: {}, + typedoc: {}, }, }), ) @@ -3121,7 +3139,11 @@ description: This page has a description basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/partial-ignore.mdx': ['link-doc-not-found'], + docs: { + 'partial-ignore.mdx': ['link-doc-not-found'], + }, + partials: {}, + typedoc: {}, }, }), ) @@ -3166,11 +3188,15 @@ description: This page has a description basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/component-issues.mdx': [ - 'doc-not-in-manifest', - 'component-missing-attribute', - 'include-src-not-partials', - ], + docs: { + 'component-issues.mdx': [ + 'doc-not-in-manifest', + 'component-missing-attribute', + 'include-src-not-partials', + ], + }, + partials: {}, + typedoc: {}, }, }), ) @@ -3207,7 +3233,11 @@ title: Missing Description basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/missing-description.mdx': ['frontmatter-missing-description'], + docs: { + 'missing-description.mdx': ['frontmatter-missing-description'], + }, + partials: {}, + typedoc: {}, }, }), ) @@ -3259,7 +3289,11 @@ description: The page being linked to basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/source-page.mdx': ['link-hash-not-found'], + docs: { + 'source-page.mdx': ['link-hash-not-found'], + }, + partials: {}, + typedoc: {}, }, }), ) @@ -3313,7 +3347,11 @@ description: This page has a description basePath: tempDir, validSdks: ['react', 'nodejs'], ignoreWarnings: { - '/docs/sdk-doc.mdx': ['doc-sdk-filtered-by-parent'], + docs: { + 'sdk-doc.mdx': ['doc-sdk-filtered-by-parent'], + }, + partials: {}, + typedoc: {}, }, }), ) @@ -3354,7 +3392,11 @@ description: Test page with partial basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/_partials/test-partial.mdx': ['link-doc-not-found'], + partials: { + 'test-partial.mdx': ['link-doc-not-found'], + }, + docs: {}, + typedoc: {}, }, }), ) @@ -3538,7 +3580,11 @@ interface Client { basePath: tempDir, validSdks: ['react'], ignoreWarnings: { - '/docs/api-doc.mdx': ['typedoc-not-found'], + docs: { + 'api-doc.mdx': ['typedoc-not-found'], + }, + partials: {}, + typedoc: {}, }, }), ) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 5629433bff..fe7a10d3f8 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -25,331 +25,28 @@ // - Removes .mdx from the end of docs markdown links // - Adds canonical links in frontmatter for SDK-specific docs +import watcher from '@parcel/watcher' +import { slugifyWithCounter } from '@sindresorhus/slugify' +import { toString } from 'mdast-util-to-string' +import { existsSync } from 'node:fs' import fs from 'node:fs/promises' import path from 'node:path' -import remarkMdx from 'remark-mdx' +import readdirp from 'readdirp' import { remark } from 'remark' -import { visit as mdastVisit } from 'unist-util-visit' +import remarkFrontmatter from 'remark-frontmatter' +import remarkMdx from 'remark-mdx' +import { Node } from 'unist' import { filter as mdastFilter } from 'unist-util-filter' import { map as mdastMap } from 'unist-util-map' -import { u as mdastBuilder } from 'unist-builder' -import remarkFrontmatter from 'remark-frontmatter' -import yaml from 'yaml' -import { slugifyWithCounter } from '@sindresorhus/slugify' -import { toString } from 'mdast-util-to-string' +import { visit as mdastVisit } from 'unist-util-visit' +import type { VFile } from 'vfile' import reporter from 'vfile-reporter' -import readdirp from 'readdirp' -import { z } from 'zod' -import { fromError, type ValidationError } from 'zod-validation-error' -import { Node, Position } from 'unist' -import watcher from '@parcel/watcher' -import { existsSync } from 'node:fs' - -const errorMessages = { - // Manifest errors - 'manifest-parse-error': (error: ValidationError | Error): string => `Failed to parse manifest: ${error}`, - - // Component errors - 'component-no-props': (componentName: string): string => `<${componentName} /> component has no props`, - 'component-attributes-not-array': (componentName: string): string => - `<${componentName} /> node attributes is not an array (this is a bug with the build script, please report)`, - 'component-missing-attribute': (componentName: string, propName: string): string => - `<${componentName} /> component has no "${propName}" attribute`, - 'component-attribute-no-value': (componentName: string, propName: string): string => - `<${componentName} /> attribute "${propName}" has no value (this is a bug with the build script, please report)`, - 'component-attribute-unsupported-type': (componentName: string, propName: string): string => - `<${componentName} /> attribute "${propName}" has an unsupported value type`, - - // SDK errors - 'invalid-sdks-in-if': (invalidSDKs: string[]): string => - `sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, - 'invalid-sdk-in-if': (sdk: string): string => `sdk "${sdk}" in is not a valid SDK`, - 'invalid-sdk-in-frontmatter': (invalidSDKs: string[], validSdks: SDK[]): string => - `Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(validSdks)}`, - 'if-component-sdk-not-in-frontmatter': (sdk: SDK, docSdk: SDK[]): string => - ` component is attempting to filter to sdk "${sdk}" but it is not available in the docs frontmatter ["${docSdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, - 'if-component-sdk-not-in-manifest': (sdk: SDK, href: string): string => - ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, - 'doc-sdk-filtered-by-parent': (title: string, docSDK: SDK[], parentSDK: SDK[]): string => - `Doc "${title}" is attempting to use ${JSON.stringify(docSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, - 'group-sdk-filtered-by-parent': (title: string, groupSDK: SDK[], parentSDK: SDK[]): string => - `Group "${title}" is attempting to use ${JSON.stringify(groupSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, - - // Document structure errors - 'doc-not-in-manifest': (): string => - 'This doc is not in the manifest.json, but will still be publicly accessible and other docs can link to it', - 'invalid-href-encoding': (href: string): string => - `Href "${href}" contains characters that will be encoded by the browser, please remove them`, - 'frontmatter-missing-title': (): string => 'Frontmatter must have a "title" property', - 'frontmatter-missing-description': (): string => 'Frontmatter should have a "description" property', - 'frontmatter-parse-failed': (href: string): string => `Frontmatter parsing failed for ${href}`, - 'doc-not-found': (title: string, href: string): string => - `Doc "${title}" in manifest.json not found in the docs folder at ${href}.mdx`, - 'doc-parse-failed': (href: string): string => `Doc "${href}" failed to parse`, - 'sdk-path-conflict': (href: string, path: string): string => - `Doc "${href}" is attempting to write out a doc to ${path} but the first part of the path is a valid SDK, this causes a file path conflict.`, - 'duplicate-heading-id': (href: string, id: string): string => - `Doc "${href}" contains a duplicate heading id "${id}", please ensure all heading ids are unique`, - - // Include component errors - 'include-src-not-partials': (): string => ` prop "src" must start with "_partials/"`, - 'partial-not-found': (src: string): string => `Partial /docs/${src}.mdx not found`, - 'partials-inside-partials': (): string => - 'Partials inside of partials is not yet supported (this is a bug with the build script, please report)', - - // Link validation errors - 'link-doc-not-found': (url: string): string => `Doc ${url} not found`, - 'link-hash-not-found': (hash: string, url: string): string => `Hash "${hash}" not found in ${url}`, - - // File reading errors - 'file-read-error': (filePath: string): string => `file ${filePath} doesn't exist`, - 'partial-read-error': (path: string): string => `Failed to read in ${path} from partials file`, - 'markdown-read-error': (href: string): string => `Attempting to read in ${href}.mdx failed`, - 'partial-parse-error': (path: string): string => `Failed to parse the content of ${path}`, - - // Typedoc errors - 'typedoc-folder-not-found': (path: string): string => - `Typedoc folder ${path} not found, run "npm run typedoc:download"`, - 'typedoc-read-error': (filePath: string): string => `Failed to read in ${filePath} from typedoc file`, - 'typedoc-parse-error': (filePath: string): string => `Failed to parse ${filePath} from typedoc file`, - 'typedoc-not-found': (filePath: string): string => `Typedoc ${filePath} not found`, -} as const - -type WarningCode = keyof typeof errorMessages - -// Helper function to check if a warning should be ignored -const shouldIgnoreWarning = (config: BuildConfig, filePath: string, warningCode: WarningCode): boolean => { - if (!config.ignoreWarnings) { - return false - } - - const ignoreList = config.ignoreWarnings[filePath] - if (!ignoreList) { - return false - } - - return ignoreList.includes(warningCode) -} - -const safeMessage = >( - config: BuildConfig, - vfile: VFile, - filePath: string, - warningCode: TCode, - args: TArgs, - position?: Position, -) => { - if (!shouldIgnoreWarning(config, filePath, warningCode)) { - // @ts-expect-error - TypeScript has trouble with spreading args into the function - const message = errorMessages[warningCode](...args) - vfile.message(message, position) - } -} - -const safeFail = >( - config: BuildConfig, - vfile: VFile, - filePath: string, - warningCode: TCode, - args: TArgs, - position?: Position, -) => { - if (!shouldIgnoreWarning(config, filePath, warningCode)) { - // @ts-expect-error - TypeScript has trouble with spreading args into the function - const message = errorMessages[warningCode](...args) - vfile.fail(message, position) - } -} - -const VALID_SDKS = [ - 'nextjs', - 'react', - 'js-frontend', - 'chrome-extension', - 'expo', - 'ios', - 'nodejs', - 'expressjs', - 'fastify', - 'react-router', - 'remix', - 'tanstack-react-start', - 'go', - 'astro', - 'nuxt', - 'vue', - 'ruby', - 'python', - 'js-backend', - 'sdk-development', - 'community-sdk', -] as const - -type SDK = (typeof VALID_SDKS)[number] - -const sdk = z.enum(VALID_SDKS) - -const icon = z.enum([ - 'apple', - 'application-2', - 'arrow-up-circle', - 'astro', - 'angular', - 'block', - 'bolt', - 'book', - 'box', - 'c-sharp', - 'chart', - 'checkmark-circle', - 'chrome', - 'clerk', - 'code-bracket', - 'cog-6-teeth', - 'door', - 'elysia', - 'expressjs', - 'globe', - 'go', - 'home', - 'hono', - 'javascript', - 'koa', - 'link', - 'linkedin', - 'lock', - 'nextjs', - 'nodejs', - 'plug', - 'plus-circle', - 'python', - 'react', - 'redwood', - 'remix', - 'react-router', - 'rocket', - 'route', - 'ruby', - 'rust', - 'speedometer', - 'stacked-rectangle', - 'solid', - 'svelte', - 'tanstack', - 'user-circle', - 'user-dotted-circle', - 'vue', - 'x', - 'expo', - 'nuxt', - 'fastify', -]) - -type Icon = z.infer - -const tag = z.enum(['(Beta)', '(Community)']) - -type Tag = z.infer - -type ManifestItem = { - title: string - href: string - tag?: Tag - wrap?: boolean - icon?: Icon - target?: '_blank' - sdk?: SDK[] -} - -type ManifestGroup = { - title: string - items: Manifest - collapse?: boolean - tag?: Tag - wrap?: boolean - icon?: Icon - hideTitle?: boolean - sdk?: SDK[] -} - -type Manifest = (ManifestItem | ManifestGroup)[][] - -// Create manifest schema based on config -const createManifestSchema = (config: BuildConfig) => { - const manifestItem: z.ZodType = z - .object({ - title: z.string(), - href: z.string(), - tag: tag.optional(), - wrap: z.boolean().default(config.manifestOptions.wrapDefault), - icon: icon.optional(), - target: z.enum(['_blank']).optional(), - sdk: z.array(sdk).optional(), - }) - .strict() - - const manifestGroup: z.ZodType = z - .object({ - title: z.string(), - items: z.lazy(() => manifestSchema), - collapse: z.boolean().default(config.manifestOptions.collapseDefault), - tag: tag.optional(), - wrap: z.boolean().default(config.manifestOptions.wrapDefault), - icon: icon.optional(), - hideTitle: z.boolean().default(config.manifestOptions.hideTitleDefault), - sdk: z.array(sdk).optional(), - }) - .strict() - - const manifestSchema: z.ZodType = z.array(z.array(z.union([manifestItem, manifestGroup]))) - - return { - manifestItem, - manifestGroup, - manifestSchema, - } -} - -const isValidSdk = - (config: BuildConfig) => - (sdk: string): sdk is SDK => { - return config.validSdks.includes(sdk as SDK) - } - -const isValidSdks = - (config: BuildConfig) => - (sdks: string[]): sdks is SDK[] => { - return sdks.every(isValidSdk(config)) - } - -const parseJSON = (json: string) => { - try { - const output = JSON.parse(json) - - return [null, output as unknown] as const - } catch (error) { - return [new Error(`Failed to parse JSON`, { cause: error }), null] as const - } -} - -const readManifest = (config: BuildConfig) => async (): Promise => { - const { manifestSchema } = createManifestSchema(config) - const unsafe_manifest = await fs.readFile(config.manifestFilePath, { encoding: 'utf-8' }) - - const [error, json] = parseJSON(unsafe_manifest) - - if (error) { - throw new Error(errorMessages['manifest-parse-error'](error)) - } - - const manifest = await z.object({ navigation: manifestSchema }).safeParseAsync(json) - - if (manifest.success === true) { - return manifest.data.navigation - } - - throw new Error(errorMessages['manifest-parse-error'](fromError(manifest.error))) -} +import yaml from 'yaml' +import { SDKLink } from './lib/components/SDKLink' +import { createConfig, type BuildConfig } from './lib/config' +import { errorMessages, safeFail, safeMessage, shouldIgnoreWarning, type WarningsSection } from './lib/error-messages' +import { ManifestGroup, readManifest } from './lib/manifest' +import { isValidSdk, isValidSdks, VALID_SDKS, type SDK } from './lib/validators' const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { const filePath = path.join(config.docsPath, docPath) @@ -366,7 +63,8 @@ const readDocsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.docsPath, { type: 'files', fileFilter: (entry) => - config.ignorePaths.some((ignoreItem) => `/docs/${entry.path}`.startsWith(ignoreItem)) === false && + // Partials are inside the docs folder, so we need to exclude them + `${config.docsRelativePath}/${entry.path}`.startsWith(config.partialsRelativePath) === false && entry.path.endsWith('.mdx'), }) } @@ -406,7 +104,7 @@ const readPartial = (config: BuildConfig) => async (filePath: string) => { 'name' in node && node.name === 'Include', (node) => { - safeFail(config, vfile, fullPath, 'partials-inside-partials', [], node.position) + safeFail(config, vfile, fullPath, 'partials', 'partials-inside-partials', [], node.position) }, ) }) @@ -416,7 +114,7 @@ const readPartial = (config: BuildConfig) => async (filePath: string) => { if (node.type !== 'link') return node if (!('url' in node)) return node if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node + if (!node.url.startsWith(config.baseDocsLink)) return node if (!('children' in node)) return node // We are overwriting the url with the mdx suffix removed @@ -500,13 +198,12 @@ const readTypedoc = (config: BuildConfig) => async (filePath: string) => { .use(() => (tree) => { node = tree }) - // Process links in typedocs and remove the .mdx suffix .use(() => (tree, vfile) => { return mdastMap(tree, (node) => { if (node.type !== 'link') return node if (!('url' in node)) return node if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node + if (!node.url.startsWith(config.baseDocsLink)) return node if (!('children' in node)) return node // We are overwriting the url with the mdx suffix removed @@ -537,13 +234,12 @@ const readTypedoc = (config: BuildConfig) => async (filePath: string) => { .use(() => (tree) => { node = tree }) - // Process links in typedocs and remove the .mdx suffix .use(() => (tree, vfile) => { return mdastMap(tree, (node) => { if (node.type !== 'link') return node if (!('url' in node)) return node if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node + if (!node.url.startsWith(config.baseDocsLink)) return node if (!('children' in node)) return node // We are overwriting the url with the mdx suffix removed @@ -591,8 +287,6 @@ const readTypedocsMarkdown = ) } -type VFile = Awaited> - const ensureDirectory = (config: BuildConfig) => async (dirPath: string): Promise => { @@ -763,7 +457,7 @@ function flattenTree< return result } -const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { +const scopeHrefToSDK = (config: BuildConfig) => (href: string, targetSDK: SDK | ':sdk:') => { // This is external so can't change it if (href.startsWith('/docs') === false) return href @@ -776,7 +470,7 @@ const scopeHrefToSDK = (href: string, targetSDK: SDK | ':sdk:') => { } // Add the sdk to the url - return `/docs/${targetSDK}/${hrefSegments.slice(2).join('/')}` + return `${config.baseDocsLink}${targetSDK}/${hrefSegments.slice(2).join('/')}` } const findComponent = (node: Node, componentName: string) => { @@ -799,6 +493,7 @@ const extractComponentPropValueFromNode = ( componentName: string, propName: string, required = true, + section: WarningsSection, filePath: string, ): string | undefined => { const component = findComponent(node, componentName) @@ -808,14 +503,22 @@ const extractComponentPropValueFromNode = ( // Check for attributes if (!('attributes' in component)) { if (vfile) { - safeMessage(config, vfile, filePath, 'component-no-props', [componentName], component.position) + safeMessage(config, vfile, filePath, section, 'component-no-props', [componentName], component.position) } return undefined } if (!Array.isArray(component.attributes)) { if (vfile) { - safeMessage(config, vfile, filePath, 'component-attributes-not-array', [componentName], component.position) + safeMessage( + config, + vfile, + filePath, + section, + 'component-attributes-not-array', + [componentName], + component.position, + ) } return undefined } @@ -825,7 +528,15 @@ const extractComponentPropValueFromNode = ( if (propAttribute === undefined) { if (required === true && vfile) { - safeMessage(config, vfile, filePath, 'component-missing-attribute', [componentName, propName], component.position) + safeMessage( + config, + vfile, + filePath, + section, + 'component-missing-attribute', + [componentName, propName], + component.position, + ) } return undefined } @@ -838,6 +549,7 @@ const extractComponentPropValueFromNode = ( config, vfile, filePath, + section, 'component-attribute-no-value', [componentName, propName], component.position, @@ -858,6 +570,7 @@ const extractComponentPropValueFromNode = ( config, vfile, filePath, + section, 'component-attribute-unsupported-type', [componentName, propName], component.position, @@ -867,7 +580,8 @@ const extractComponentPropValueFromNode = ( } const extractSDKsFromIfProp = - (config: BuildConfig) => (node: Node, vfile: VFile | undefined, sdkProp: string, filePath: string) => { + (config: BuildConfig) => + (node: Node, vfile: VFile | undefined, sdkProp: string, section: WarningsSection, filePath: string) => { const isValidItem = isValidSdk(config) const isValidItems = isValidSdks(config) @@ -878,7 +592,7 @@ const extractSDKsFromIfProp = } else { const invalidSDKs = sdks.filter((sdk) => !isValidItem(sdk)) if (vfile) { - safeMessage(config, vfile, filePath, 'invalid-sdks-in-if', [invalidSDKs], node.position) + safeMessage(config, vfile, filePath, section, 'invalid-sdks-in-if', [invalidSDKs], node.position) } } } else { @@ -886,7 +600,7 @@ const extractSDKsFromIfProp = return [sdkProp] } else { if (vfile) { - safeMessage(config, vfile, filePath, 'invalid-sdk-in-if', [sdkProp], node.position) + safeMessage(config, vfile, filePath, section, 'invalid-sdk-in-if', [sdkProp], node.position) } } } @@ -943,10 +657,11 @@ const parseInMarkdownFile = partials: { path: string; content: string; node: Node }[], typedocs: { path: string; content: string; node: Node }[], inManifest: boolean, + section: WarningsSection, ) => { const readFile = readMarkdownFile(config) const validateSDKs = isValidSdks(config) - const [error, fileContent] = await readFile(`${href}.mdx`.replace('/docs/', '')) + const [error, fileContent] = await readFile(`${href}.mdx`.replace(config.baseDocsLink, '')) if (error !== null) { throw new Error(errorMessages['markdown-read-error'](href), { @@ -974,11 +689,11 @@ const parseInMarkdownFile = node = tree if (inManifest === false) { - safeMessage(config, vfile, filePath, 'doc-not-in-manifest', []) + safeMessage(config, vfile, filePath, section, 'doc-not-in-manifest', []) } if (href !== encodeURI(href)) { - safeFail(config, vfile, filePath, 'invalid-href-encoding', [href]) + safeFail(config, vfile, filePath, section, 'invalid-href-encoding', [href]) } }) // validate and extract out the frontmatter @@ -1000,6 +715,7 @@ const parseInMarkdownFile = config, vfile, filePath, + section, 'invalid-sdk-in-frontmatter', [invalidSDKs, config.validSdks as SDK[]], node.position, @@ -1008,12 +724,12 @@ const parseInMarkdownFile = } if (frontmatterYaml.title === undefined) { - safeFail(config, vfile, filePath, 'frontmatter-missing-title', [], node.position) + safeFail(config, vfile, filePath, section, 'frontmatter-missing-title', [], node.position) return } if (frontmatterYaml.description === undefined) { - safeMessage(config, vfile, filePath, 'frontmatter-missing-description', [], node.position) + safeMessage(config, vfile, filePath, section, 'frontmatter-missing-description', [], node.position) } frontmatter = { @@ -1025,7 +741,7 @@ const parseInMarkdownFile = ) if (frontmatter === undefined) { - safeFail(config, vfile, filePath, 'frontmatter-parse-failed', [href]) + safeFail(config, vfile, filePath, section, 'frontmatter-parse-failed', [href]) return } }) @@ -1055,7 +771,7 @@ const parseInMarkdownFile = if (id !== undefined) { if (documentContainsIfComponent === false && headingsHashes.has(id)) { - safeFail(config, vfile, filePath, 'duplicate-heading-id', [href, id]) + safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [href, id]) } headingsHashes.add(id) @@ -1063,7 +779,7 @@ const parseInMarkdownFile = const slug = slugify(toString(node).trim()) if (documentContainsIfComponent === false && headingsHashes.has(slug)) { - safeFail(config, vfile, filePath, 'duplicate-heading-id', [href, slug]) + safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [href, slug]) } headingsHashes.add(slug) @@ -1136,10 +852,10 @@ export const build = async (store: ReturnType, config: // Grab all the docs links in the manifest await traverseTree({ items: userManifest }, async (item) => { - if (!item.href?.startsWith('/docs/')) return item + if (!item.href?.startsWith(config.baseDocsLink)) return item if (item.target !== undefined) return item - const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) + const ignore = config.ignoredLink(item.href) if (ignore === true) return item docsInManifest.add(item.href) @@ -1152,7 +868,7 @@ export const build = async (store: ReturnType, config: // Read in all the docs const docsArray = await Promise.all( docsFiles.map(async (file) => { - const href = removeMdxSuffix(`/docs/${file.path}`) + const href = removeMdxSuffix(`${config.baseDocsLink}${file.path}`) const inManifest = docsInManifest.has(href) @@ -1163,7 +879,7 @@ export const build = async (store: ReturnType, config: if (cachedMarkdownFile) { markdownFile = structuredClone(cachedMarkdownFile) } else { - markdownFile = await parseMarkdownFile(href, partials, typedocs, inManifest) + markdownFile = await parseMarkdownFile(href, partials, typedocs, inManifest, 'docs') store.markdownFiles.set(href, structuredClone(markdownFile)) } @@ -1179,17 +895,17 @@ export const build = async (store: ReturnType, config: const sdkScopedManifestFirstPass = await traverseTree( { items: userManifest, sdk: undefined as undefined | SDK[] }, async (item, tree) => { - if (!item.href?.startsWith('/docs/')) return item + if (!item.href?.startsWith(config.baseDocsLink)) return item if (item.target !== undefined) return item - const ignore = config.ignorePaths.some((ignoreItem) => item.href.startsWith(ignoreItem)) + const ignore = config.ignoredLink(item.href) if (ignore === true) return item // even thou we are not processing them, we still need to keep them const doc = docsMap.get(item.href) if (doc === undefined) { const filePath = `${item.href}.mdx` - if (!shouldIgnoreWarning(config, filePath, 'doc-not-found')) { + if (!shouldIgnoreWarning(config, filePath, 'docs', 'doc-not-found')) { throw new Error(errorMessages['doc-not-found'](item.title, item.href)) } return item @@ -1207,7 +923,7 @@ export const build = async (store: ReturnType, config: if (docSDK !== undefined && parentSDK !== undefined) { if (docSDK.every((sdk) => parentSDK?.includes(sdk)) === false) { const filePath = `${item.href}.mdx` - if (!shouldIgnoreWarning(config, filePath, 'doc-sdk-filtered-by-parent')) { + if (!shouldIgnoreWarning(config, filePath, 'docs', 'doc-sdk-filtered-by-parent')) { throw new Error(errorMessages['doc-sdk-filtered-by-parent'](item.title, docSDK, parentSDK)) } } @@ -1234,15 +950,6 @@ export const build = async (store: ReturnType, config: // This is the sdk of the parent group const parentSDK = tree.sdk - if (groupSDK !== undefined && parentSDK !== undefined) { - if (groupSDK.every((sdk) => parentSDK?.includes(sdk)) === false) { - const filePath = `/docs/groups/${details.title}.mdx` - if (!shouldIgnoreWarning(config, filePath, 'group-sdk-filtered-by-parent')) { - throw new Error(errorMessages['group-sdk-filtered-by-parent'](details.title, groupSDK, parentSDK)) - } - } - } - // If there are no children items, then the we either use the group we are looking at sdks if its defined, or its parent group if (groupsItemsCombinedSDKs.length === 0) { return { ...details, sdk: groupSDK ?? parentSDK, items } as ManifestGroup @@ -1332,7 +1039,7 @@ export const build = async (store: ReturnType, config: async (item) => { return { title: item.title, - href: docsMap.get(item.href)?.sdk !== undefined ? scopeHrefToSDK(item.href, ':sdk:') : item.href, + href: docsMap.get(item.href)?.sdk !== undefined ? scopeHrefToSDK(config)(item.href, ':sdk:') : item.href, tag: item.tag, wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, icon: item.icon, @@ -1361,7 +1068,7 @@ export const build = async (store: ReturnType, config: const validatedPartials = await Promise.all( partials.map(async (partial) => { - const partialPath = `/docs/_partials/${partial.path}` + const partialPath = `${config.partialsRelativePath}/${partial.path}` try { let node: Node | null = null @@ -1369,7 +1076,7 @@ export const build = async (store: ReturnType, config: const vfile = await remark() .use(remarkFrontmatter) .use(remarkMdx) - .use(validateAndEmbedLinks(config, docsMap, partialPath)) + .use(validateAndEmbedLinks(config, docsMap, partialPath, 'partials')) .use(() => (tree, vfile) => { node = tree }) @@ -1401,7 +1108,7 @@ export const build = async (store: ReturnType, config: const vfile = await remark() .use(remarkMdx) - .use(validateAndEmbedLinks(config, docsMap, filePath)) + .use(validateAndEmbedLinks(config, docsMap, filePath, 'typedoc')) .use(() => (tree, vfile) => { node = tree }) @@ -1417,23 +1124,28 @@ export const build = async (store: ReturnType, config: node: node as Node, } } catch (error) { - let node: Node | null = null + try { + let node: Node | null = null - const vfile = await remark() - .use(validateAndEmbedLinks(config, docsMap, filePath)) - .use(() => (tree, vfile) => { - node = tree - }) - .process(typedoc.vfile) + const vfile = await remark() + .use(validateAndEmbedLinks(config, docsMap, filePath, 'typedoc')) + .use(() => (tree, vfile) => { + node = tree + }) + .process(typedoc.vfile) - if (node === null) { - throw new Error(errorMessages['typedoc-parse-error'](typedoc.path)) - } + if (node === null) { + throw new Error(errorMessages['typedoc-parse-error'](typedoc.path)) + } - return { - ...typedoc, - vfile, - node: node as Node, + return { + ...typedoc, + vfile, + node: node as Node, + } + } catch (error) { + console.error(error) + throw new Error(errorMessages['typedoc-parse-error'](typedoc.path)) } } }), @@ -1453,7 +1165,7 @@ export const build = async (store: ReturnType, config: if (node.type !== 'link') return node if (!('url' in node)) return node if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/') && !node.url.startsWith('#')) return node + if (!node.url.startsWith(config.baseDocsLink) && !node.url.startsWith('#')) return node if (!('children' in node)) return node // we are overwriting the url with the mdx suffix removed @@ -1466,13 +1178,13 @@ export const build = async (store: ReturnType, config: url = doc.href } - const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + const ignore = config.ignoredLink(url) if (ignore === true) return node const linkedDoc = docsMap.get(url) if (linkedDoc === undefined) { - safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) + safeMessage(config, vfile, filePath, 'docs', 'link-doc-not-found', [url], node.position) return node } @@ -1480,7 +1192,7 @@ export const build = async (store: ReturnType, config: const hasHash = linkedDoc.headingsHashes.has(hash) if (hasHash === false) { - safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) + safeMessage(config, vfile, filePath, 'docs', 'link-hash-not-found', [hash, url], node.position) } } @@ -1494,14 +1206,14 @@ export const build = async (store: ReturnType, config: firstChild.type = 'text' return SDKLink({ - href: scopeHrefToSDK(url, ':sdk:'), + href: scopeHrefToSDK(config)(url, ':sdk:'), sdks: linkedDoc.sdk, code: true, }) } return SDKLink({ - href: scopeHrefToSDK(url, ':sdk:'), + href: scopeHrefToSDK(config)(url, ':sdk:'), sdks: linkedDoc.sdk, code: false, children: node.children, @@ -1514,11 +1226,11 @@ export const build = async (store: ReturnType, config: // Validate the components .use(() => (tree, vfile) => { mdastVisit(tree, (node) => { - const sdk = extractComponentPropValueFromNode(config, node, vfile, 'If', 'sdk', false, filePath) + const sdk = extractComponentPropValueFromNode(config, node, vfile, 'If', 'sdk', false, 'docs', filePath) if (sdk === undefined) return - const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk, filePath) + const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk, 'docs', filePath) if (sdksFilter === undefined) return @@ -1540,6 +1252,7 @@ export const build = async (store: ReturnType, config: config, vfile, filePath, + 'docs', 'if-component-sdk-not-in-frontmatter', [sdk, doc.sdk], node.position, @@ -1553,7 +1266,15 @@ export const build = async (store: ReturnType, config: const available = availableSDKs.includes(sdk) if (available === false) { - safeFail(config, vfile, filePath, 'if-component-sdk-not-in-manifest', [sdk, doc.href], node.position) + safeFail( + config, + vfile, + filePath, + 'docs', + 'if-component-sdk-not-in-manifest', + [sdk, doc.href], + node.position, + ) } })() }) @@ -1563,10 +1284,10 @@ export const build = async (store: ReturnType, config: .use(checkTypedoc(config, validatedTypedocs, filePath, { reportWarnings: false, embed: true })) .process(doc.vfile) - const distFilePath = `${doc.href.replace('/docs/', '')}.mdx` + const distFilePath = `${doc.href.replace(config.baseDocsLink, '')}.mdx` if (isValidSdk(config)(distFilePath.split('/')[0])) { - if (!shouldIgnoreWarning(config, filePath, 'sdk-path-conflict')) { + if (!shouldIgnoreWarning(config, filePath, 'docs', 'sdk-path-conflict')) { throw new Error(errorMessages['sdk-path-conflict'](doc.href, distFilePath)) } } @@ -1580,7 +1301,7 @@ export const build = async (store: ReturnType, config: `--- template: wide --- -`, +`, ) return vfile @@ -1611,11 +1332,20 @@ template: wide // We aren't passing the vfile here as the as the warning // should have already been reported above when we initially // parsed the file - const sdk = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', true, filePath) + const sdk = extractComponentPropValueFromNode( + config, + node, + undefined, + 'If', + 'sdk', + true, + 'docs', + filePath, + ) if (sdk === undefined) return true - const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk, filePath) + const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk, 'docs', filePath) if (sdksFilter === undefined) return true @@ -1626,20 +1356,28 @@ template: wide return false }) }) - .use(validateUniqueHeadings(config, filePath)) + .use(validateUniqueHeadings(config, filePath, 'docs')) // scope urls so they point to the current sdk .use(() => (tree, vfile) => { return mdastMap(tree, (node) => { if (node.type !== 'link') return node if (!('url' in node)) { - safeFail(config, vfile, filePath, 'link-doc-not-found', ['url property missing'], node.position) + safeFail( + config, + vfile, + filePath, + 'docs', + 'link-doc-not-found', + ['url property missing'], + node.position, + ) return node } if (typeof node.url !== 'string') { - safeFail(config, vfile, filePath, 'link-doc-not-found', ['url not a string'], node.position) + safeFail(config, vfile, filePath, 'docs', 'link-doc-not-found', ['url not a string'], node.position) return node } - if (!node.url.startsWith('/docs/')) { + if (!node.url.startsWith(config.baseDocsLink)) { return node } @@ -1648,13 +1386,13 @@ template: wide const [url, hash] = (node.url as string).split('#') - const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + const ignore = config.ignoredLink(url) if (ignore === true) return node const doc = docsMap.get(url) if (doc === undefined) { - safeFail(config, vfile, filePath, 'link-doc-not-found', [url], node.position) + safeFail(config, vfile, filePath, 'docs', 'link-doc-not-found', [url], node.position) return node } @@ -1672,7 +1410,7 @@ template: wide const frontmatter = yaml.parse(node.value) - frontmatter.canonical = doc.sdk ? scopeHrefToSDK(doc.href, ':sdk:') : doc.href + frontmatter.canonical = doc.sdk ? scopeHrefToSDK(config)(doc.href, ':sdk:') : doc.href node.value = yaml.stringify(frontmatter).split('\n').slice(0, -1).join('\n') @@ -1684,7 +1422,7 @@ template: wide messages: [], // reset the messages, otherwise they will be duplicated }) - await writeSdkFile(targetSdk, `${doc.href.replace('/docs/', '')}.mdx`, String(vfile)) + await writeSdkFile(targetSdk, `${doc.href.replace(config.baseDocsLink, '')}.mdx`, String(vfile)) return vfile }), @@ -1706,11 +1444,11 @@ template: wide const availableSDKs = new Set() mdastVisit(doc.node, (node) => { - const sdkProp = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', true, filePath) + const sdkProp = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', true, 'docs', filePath) if (sdkProp === undefined) return - const sdks = extractSDKsFromIfComponent(node, undefined, sdkProp, filePath) + const sdks = extractSDKsFromIfComponent(node, undefined, sdkProp, 'docs', filePath) if (sdks === undefined) return @@ -1724,16 +1462,25 @@ template: wide .use(remarkMdx) .use(() => (inputTree) => { return mdastFilter(inputTree, (node) => { - const sdkProp = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', false, filePath) + const sdkProp = extractComponentPropValueFromNode( + config, + node, + undefined, + 'If', + 'sdk', + false, + 'docs', + filePath, + ) if (!sdkProp) return true - const ifSdks = extractSDKsFromIfComponent(node, undefined, sdkProp, filePath) + const ifSdks = extractSDKsFromIfComponent(node, undefined, sdkProp, 'docs', filePath) if (!ifSdks) return true return ifSdks.includes(sdk) }) }) - .use(validateUniqueHeadings(config, filePath)) + .use(validateUniqueHeadings(config, filePath, 'docs')) .process({ path: filePath, value: String(doc.vfile), @@ -1753,7 +1500,7 @@ template: wide export const invalidateFile = (store: ReturnType, config: BuildConfig) => (filePath: string) => { - store.markdownFiles.delete(removeMdxSuffix(`/docs/${path.relative(config.docsPath, filePath)}`)) + store.markdownFiles.delete(removeMdxSuffix(`${config.baseDocsLink}${path.relative(config.docsPath, filePath)}`)) store.partialsFiles.delete(path.relative(config.partialsPath, filePath)) store.typedocsFiles.delete(path.relative(config.typedocPath, filePath)) } @@ -1798,85 +1545,22 @@ const watchAndRebuild = (store: ReturnType, config: Bui watcher.subscribe(config.typedocPath, handleFileChange) } -type BuildConfigOptions = { - basePath: string - validSdks: readonly SDK[] - docsPath: string - manifestPath: string - partialsPath: string - distPath: string - typedocPath: string - ignorePaths: string[] - ignoreWarnings?: Record - manifestOptions: { - wrapDefault: boolean - collapseDefault: boolean - hideTitleDefault: boolean - } - cleanDist: boolean - flags?: { - watch?: boolean - controlled?: boolean - } -} - -type BuildConfig = ReturnType - -// Takes the basePath and resolves the relative paths to be absolute paths -export function createConfig(config: BuildConfigOptions) { - const resolve = (relativePath: string) => { - return path.isAbsolute(relativePath) ? relativePath : path.join(config.basePath, relativePath) - } - - return { - basePath: config.basePath, - validSdks: config.validSdks, - - manifestRelativePath: config.manifestPath, - manifestFilePath: resolve(config.manifestPath), - - partialsRelativePath: config.partialsPath, - partialsPath: resolve(config.partialsPath), - - docsRelativePath: config.docsPath, - docsPath: resolve(config.docsPath), - - distRelativePath: config.distPath, - distPath: resolve(config.distPath), - - typedocRelativePath: config.typedocPath, - typedocPath: resolve(config.typedocPath), - - ignorePaths: config.ignorePaths, - ignoreWarnings: config.ignoreWarnings || {}, - manifestOptions: config.manifestOptions ?? { - wrapDefault: true, - collapseDefault: false, - hideTitleDefault: false, - }, - cleanDist: config.cleanDist, - flags: { - watch: config.flags?.watch ?? false, - controlled: config.flags?.controlled ?? false, - }, - } -} - const main = async () => { const args = process.argv.slice(2) const config = createConfig({ basePath: __dirname, docsPath: '../docs', + baseDocsLink: '/docs/', manifestPath: '../docs/manifest.json', partialsPath: '../docs/_partials', distPath: '../dist', typedocPath: '../clerk-typedoc', - ignorePaths: [ + ignoreLinks: [ '/docs/core-1', - '/pricing', '/docs/reference/backend-api', '/docs/reference/frontend-api', + '/pricing', '/support', '/discord', '/contact', @@ -1884,20 +1568,22 @@ const main = async () => { '/contact/support', '/blog', '/changelog/2024-04-19', - '/docs/_partials', ], ignoreWarnings: { - '/docs/index.mdx': ['doc-not-in-manifest'], - '/docs/guides/overview.mdx': ['doc-not-in-manifest'], - '/docs/quickstarts/overview.mdx': ['doc-not-in-manifest'], - '/docs/references/overview.mdx': ['doc-not-in-manifest'], - '/docs/maintenance-mode.mdx': ['doc-not-in-manifest'], - '/docs/deployments/staging-alternatives.mdx': ['doc-not-in-manifest'], - '/docs/references/nextjs/usage-with-older-versions.mdx': ['doc-not-in-manifest'], - - // Typedoc warnings - '../clerk-typedoc/types/active-session-resource.mdx': ['link-hash-not-found'], - '../clerk-typedoc/types/pending-session-resource.mdx': ['link-hash-not-found'], + docs: { + 'index.mdx': ['doc-not-in-manifest'], + 'guides/overview.mdx': ['doc-not-in-manifest'], + 'quickstarts/overview.mdx': ['doc-not-in-manifest'], + 'references/overview.mdx': ['doc-not-in-manifest'], + 'maintenance-mode.mdx': ['doc-not-in-manifest'], + 'deployments/staging-alternatives.mdx': ['doc-not-in-manifest'], + 'references/nextjs/usage-with-older-versions.mdx': ['doc-not-in-manifest'], + }, + typedoc: { + 'types/active-session-resource.mdx': ['link-hash-not-found'], + 'types/pending-session-resource.mdx': ['link-hash-not-found'], + }, + partials: {}, }, validSdks: VALID_SDKS, manifestOptions: { @@ -1938,59 +1624,16 @@ if (require.main === module) { main() } -const SDKLink = ( - props: { href: string; sdks: SDK[]; code: true } | { href: string; sdks: SDK[]; code: false; children: unknown }, -) => { - if (props.code) { - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: props.href, - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(props.sdks), - }), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'code', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: props.code, - }), - }), - ], - }) - } - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: props.href, - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(props.sdks), - }), - }), - ], - children: props.children, - }) -} - // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component const validateAndEmbedLinks = - (config: BuildConfig, docsMap: DocsMap, filePath: string) => () => (tree: Node, vfile: VFile) => { + (config: BuildConfig, docsMap: DocsMap, filePath: string, section: WarningsSection) => + () => + (tree: Node, vfile: VFile) => { return mdastMap(tree, (node) => { if (node.type !== 'link') return node if (!('url' in node)) return node if (typeof node.url !== 'string') return node - if (!node.url.startsWith('/docs/')) return node + if (!node.url.startsWith(config.baseDocsLink)) return node if (!('children' in node)) return node // we are overwriting the url with the mdx suffix removed @@ -1998,13 +1641,13 @@ const validateAndEmbedLinks = const [url, hash] = (node.url as string).split('#') - const ignore = config.ignorePaths.some((ignoreItem) => url.startsWith(ignoreItem)) + const ignore = config.ignoredLink(url) if (ignore === true) return node const doc = docsMap.get(url) if (doc === undefined) { - safeMessage(config, vfile, filePath, 'link-doc-not-found', [url], node.position) + safeMessage(config, vfile, filePath, section, 'link-doc-not-found', [url], node.position) return node } @@ -2012,7 +1655,7 @@ const validateAndEmbedLinks = const hasHash = doc.headingsHashes.has(hash) if (hasHash === false) { - safeMessage(config, vfile, filePath, 'link-hash-not-found', [hash, url], node.position) + safeMessage(config, vfile, filePath, section, 'link-hash-not-found', [hash, url], node.position) } } @@ -2026,14 +1669,14 @@ const validateAndEmbedLinks = firstChild.type = 'text' return SDKLink({ - href: scopeHrefToSDK(url, ':sdk:'), + href: scopeHrefToSDK(config)(url, ':sdk:'), sdks: doc.sdk, code: true, }) } return SDKLink({ - href: scopeHrefToSDK(url, ':sdk:'), + href: scopeHrefToSDK(config)(url, ':sdk:'), sdks: doc.sdk, code: false, children: node.children, @@ -2060,13 +1703,22 @@ const checkPartials = () => (tree: Node, vfile: VFile) => { mdastMap(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode(config, node, vfile, 'Include', 'src', true, filePath) + const partialSrc = extractComponentPropValueFromNode( + config, + node, + vfile, + 'Include', + 'src', + true, + 'docs', + filePath, + ) if (partialSrc === undefined) return node if (partialSrc.startsWith('_partials/') === false) { if (options.reportWarnings === true) { - safeMessage(config, vfile, filePath, 'include-src-not-partials', [], node.position) + safeMessage(config, vfile, filePath, 'docs', 'include-src-not-partials', [], node.position) } return node } @@ -2075,7 +1727,15 @@ const checkPartials = if (partial === undefined) { if (options.reportWarnings === true) { - safeMessage(config, vfile, filePath, 'partial-not-found', [removeMdxSuffix(partialSrc)], node.position) + safeMessage( + config, + vfile, + filePath, + 'docs', + 'partial-not-found', + [removeMdxSuffix(partialSrc)], + node.position, + ) } return node } @@ -2098,7 +1758,16 @@ const checkTypedoc = () => (tree: Node, vfile: VFile) => { mdastMap(tree, (node) => { - const typedocSrc = extractComponentPropValueFromNode(config, node, vfile, 'Typedoc', 'src', true, filePath) + const typedocSrc = extractComponentPropValueFromNode( + config, + node, + vfile, + 'Typedoc', + 'src', + true, + 'docs', + filePath, + ) if (typedocSrc === undefined) return node @@ -2116,6 +1785,7 @@ const checkTypedoc = config, vfile, filePath, + 'docs', 'typedoc-not-found', [`${removeMdxSuffix(typedocSrc)}.mdx`], node.position, @@ -2132,31 +1802,32 @@ const checkTypedoc = }) } -const validateUniqueHeadings = (config: BuildConfig, filePath: string) => () => (tree: Node, vfile: VFile) => { - const headingsHashes = new Set() - const slugify = slugifyWithCounter() +const validateUniqueHeadings = + (config: BuildConfig, filePath: string, section: WarningsSection) => () => (tree: Node, vfile: VFile) => { + const headingsHashes = new Set() + const slugify = slugifyWithCounter() - mdastVisit( - tree, - (node) => node.type === 'heading', - (node) => { - const id = extractHeadingFromHeadingNode(node) + mdastVisit( + tree, + (node) => node.type === 'heading', + (node) => { + const id = extractHeadingFromHeadingNode(node) - if (id !== undefined) { - if (headingsHashes.has(id)) { - safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, id]) - } + if (id !== undefined) { + if (headingsHashes.has(id)) { + safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [filePath, id]) + } - headingsHashes.add(id) - } else { - const slug = slugify(toString(node).trim()) + headingsHashes.add(id) + } else { + const slug = slugify(toString(node).trim()) - if (headingsHashes.has(slug)) { - safeFail(config, vfile, filePath, 'duplicate-heading-id', [filePath, slug]) - } + if (headingsHashes.has(slug)) { + safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [filePath, slug]) + } - headingsHashes.add(slug) - } - }, - ) -} + headingsHashes.add(slug) + } + }, + ) + } diff --git a/scripts/lib/components/SDKLink.ts b/scripts/lib/components/SDKLink.ts new file mode 100644 index 0000000000..cd7915dce1 --- /dev/null +++ b/scripts/lib/components/SDKLink.ts @@ -0,0 +1,47 @@ +import type { SDK } from '../validators' +import { u as mdastBuilder } from 'unist-builder' + +export const SDKLink = ( + props: { href: string; sdks: SDK[]; code: true } | { href: string; sdks: SDK[]; code: false; children: unknown }, +) => { + if (props.code) { + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: props.href, + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(props.sdks), + }), + }), + mdastBuilder('mdxJsxAttribute', { + name: 'code', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: props.code, + }), + }), + ], + }) + } + + return mdastBuilder('mdxJsxTextElement', { + name: 'SDKLink', + attributes: [ + mdastBuilder('mdxJsxAttribute', { + name: 'href', + value: props.href, + }), + mdastBuilder('mdxJsxAttribute', { + name: 'sdks', + value: mdastBuilder('mdxJsxAttributeValueExpression', { + value: JSON.stringify(props.sdks), + }), + }), + ], + children: props.children, + }) +} diff --git a/scripts/lib/config.ts b/scripts/lib/config.ts new file mode 100644 index 0000000000..63643b66d8 --- /dev/null +++ b/scripts/lib/config.ts @@ -0,0 +1,79 @@ +import path from 'node:path' +import type { SDK } from './validators' + +type BuildConfigOptions = { + basePath: string + validSdks: readonly SDK[] + docsPath: string + baseDocsLink: string + manifestPath: string + partialsPath: string + distPath: string + typedocPath: string + ignoreLinks: string[] + ignoreWarnings?: { + docs: Record + partials: Record + typedoc: Record + } + manifestOptions: { + wrapDefault: boolean + collapseDefault: boolean + hideTitleDefault: boolean + } + cleanDist: boolean + flags?: { + watch?: boolean + controlled?: boolean + } +} + +export type BuildConfig = ReturnType + +// Takes the basePath and resolves the relative paths to be absolute paths +export function createConfig(config: BuildConfigOptions) { + const resolve = (relativePath: string) => { + return path.isAbsolute(relativePath) ? relativePath : path.join(config.basePath, relativePath) + } + + return { + basePath: config.basePath, + baseDocsLink: config.baseDocsLink, + validSdks: config.validSdks, + + manifestRelativePath: config.manifestPath, + manifestFilePath: resolve(config.manifestPath), + + partialsRelativePath: config.partialsPath, + partialsPath: resolve(config.partialsPath), + + docsRelativePath: config.docsPath, + docsPath: resolve(config.docsPath), + + distRelativePath: config.distPath, + distPath: resolve(config.distPath), + + typedocRelativePath: config.typedocPath, + typedocPath: resolve(config.typedocPath), + + ignoredLink: (url: string) => config.ignoreLinks.some((ignoreItem) => url.startsWith(ignoreItem)), + ignoreWarnings: config.ignoreWarnings ?? { + docs: {}, + partials: {}, + typedoc: {}, + }, + + manifestOptions: config.manifestOptions ?? { + wrapDefault: true, + collapseDefault: false, + hideTitleDefault: false, + }, + + cleanDist: config.cleanDist, + + flags: { + watch: config.flags?.watch ?? false, + controlled: config.flags?.controlled ?? false, + }, + } +} diff --git a/scripts/lib/error-messages.ts b/scripts/lib/error-messages.ts new file mode 100644 index 0000000000..3e9cbae1df --- /dev/null +++ b/scripts/lib/error-messages.ts @@ -0,0 +1,134 @@ +import type { VFile } from 'vfile' +import type { ValidationError } from 'zod-validation-error' +import type { BuildConfig } from './config' +import type { Position } from 'unist' +import type { SDK } from './validators' + +export const errorMessages = { + // Manifest errors + 'manifest-parse-error': (error: ValidationError | Error): string => `Failed to parse manifest: ${error}`, + + // Component errors + 'component-no-props': (componentName: string): string => `<${componentName} /> component has no props`, + 'component-attributes-not-array': (componentName: string): string => + `<${componentName} /> node attributes is not an array (this is a bug with the build script, please report)`, + 'component-missing-attribute': (componentName: string, propName: string): string => + `<${componentName} /> component has no "${propName}" attribute`, + 'component-attribute-no-value': (componentName: string, propName: string): string => + `<${componentName} /> attribute "${propName}" has no value (this is a bug with the build script, please report)`, + 'component-attribute-unsupported-type': (componentName: string, propName: string): string => + `<${componentName} /> attribute "${propName}" has an unsupported value type`, + + // SDK errors + 'invalid-sdks-in-if': (invalidSDKs: string[]): string => + `sdks "${invalidSDKs.join('", "')}" in are not valid SDKs`, + 'invalid-sdk-in-if': (sdk: string): string => `sdk "${sdk}" in is not a valid SDK`, + 'invalid-sdk-in-frontmatter': (invalidSDKs: string[], validSdks: SDK[]): string => + `Invalid SDK ${JSON.stringify(invalidSDKs)}, the valid SDKs are ${JSON.stringify(validSdks)}`, + 'if-component-sdk-not-in-frontmatter': (sdk: SDK, docSdk: SDK[]): string => + ` component is attempting to filter to sdk "${sdk}" but it is not available in the docs frontmatter ["${docSdk.join('", "')}"], if this is a mistake please remove it from the otherwise update the frontmatter to include "${sdk}"`, + 'if-component-sdk-not-in-manifest': (sdk: SDK, href: string): string => + ` component is attempting to filter to sdk "${sdk}" but it is not available in the manifest.json for ${href}, if this is a mistake please remove it from the otherwise update the manifest.json to include "${sdk}"`, + 'doc-sdk-filtered-by-parent': (title: string, docSDK: SDK[], parentSDK: SDK[]): string => + `Doc "${title}" is attempting to use ${JSON.stringify(docSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, + 'group-sdk-filtered-by-parent': (title: string, groupSDK: SDK[], parentSDK: SDK[]): string => + `Group "${title}" is attempting to use ${JSON.stringify(groupSDK)} But its being filtered down to ${JSON.stringify(parentSDK)} in the manifest.json`, + + // Document structure errors + 'doc-not-in-manifest': (): string => + 'This doc is not in the manifest.json, but will still be publicly accessible and other docs can link to it', + 'invalid-href-encoding': (href: string): string => + `Href "${href}" contains characters that will be encoded by the browser, please remove them`, + 'frontmatter-missing-title': (): string => 'Frontmatter must have a "title" property', + 'frontmatter-missing-description': (): string => 'Frontmatter should have a "description" property', + 'frontmatter-parse-failed': (href: string): string => `Frontmatter parsing failed for ${href}`, + 'doc-not-found': (title: string, href: string): string => + `Doc "${title}" in manifest.json not found in the docs folder at ${href}.mdx`, + 'doc-parse-failed': (href: string): string => `Doc "${href}" failed to parse`, + 'sdk-path-conflict': (href: string, path: string): string => + `Doc "${href}" is attempting to write out a doc to ${path} but the first part of the path is a valid SDK, this causes a file path conflict.`, + 'duplicate-heading-id': (href: string, id: string): string => + `Doc "${href}" contains a duplicate heading id "${id}", please ensure all heading ids are unique`, + + // Include component errors + 'include-src-not-partials': (): string => ` prop "src" must start with "_partials/"`, + 'partial-not-found': (src: string): string => `Partial /docs/${src}.mdx not found`, + 'partials-inside-partials': (): string => + 'Partials inside of partials is not yet supported (this is a bug with the build script, please report)', + + // Link validation errors + 'link-doc-not-found': (url: string): string => `Doc ${url} not found`, + 'link-hash-not-found': (hash: string, url: string): string => `Hash "${hash}" not found in ${url}`, + + // File reading errors + 'file-read-error': (filePath: string): string => `file ${filePath} doesn't exist`, + 'partial-read-error': (path: string): string => `Failed to read in ${path} from partials file`, + 'markdown-read-error': (href: string): string => `Attempting to read in ${href}.mdx failed`, + 'partial-parse-error': (path: string): string => `Failed to parse the content of ${path}`, + + // Typedoc errors + 'typedoc-folder-not-found': (path: string): string => + `Typedoc folder ${path} not found, run "npm run typedoc:download"`, + 'typedoc-read-error': (filePath: string): string => `Failed to read in ${filePath} from typedoc file`, + 'typedoc-parse-error': (filePath: string): string => `Failed to parse ${filePath} from typedoc file`, + 'typedoc-not-found': (filePath: string): string => `Typedoc ${filePath} not found`, +} as const + +type WarningCode = keyof typeof errorMessages +export type WarningsSection = keyof BuildConfig['ignoreWarnings'] + +// Helper function to check if a warning should be ignored +export const shouldIgnoreWarning = ( + config: BuildConfig, + filePath: string, + section: WarningsSection, + warningCode: WarningCode, +): boolean => { + const replacements = { + docs: config.baseDocsLink, + partials: config.partialsRelativePath + '/', + typedoc: config.typedocRelativePath + '/', + } + + const relativeFilePath = filePath.replace(replacements[section], '') + + const ignoreList = config.ignoreWarnings[section][relativeFilePath] + + if (!ignoreList) { + return false + } + + return ignoreList.includes(warningCode) +} + +export const safeMessage = >( + config: BuildConfig, + vfile: VFile, + filePath: string, + section: WarningsSection, + warningCode: TCode, + args: TArgs, + position?: Position, +) => { + if (!shouldIgnoreWarning(config, filePath, section, warningCode)) { + // @ts-expect-error - TypeScript has trouble with spreading args into the function + const message = errorMessages[warningCode](...args) + vfile.message(message, position) + } +} + +export const safeFail = >( + config: BuildConfig, + vfile: VFile, + filePath: string, + section: WarningsSection, + warningCode: TCode, + args: TArgs, + position?: Position, +) => { + if (!shouldIgnoreWarning(config, filePath, section, warningCode)) { + // @ts-expect-error - TypeScript has trouble with spreading args into the function + const message = errorMessages[warningCode](...args) + vfile.fail(message, position) + } +} diff --git a/scripts/lib/manifest.ts b/scripts/lib/manifest.ts new file mode 100644 index 0000000000..8b815ba6a2 --- /dev/null +++ b/scripts/lib/manifest.ts @@ -0,0 +1,94 @@ +import { z } from 'zod' +import type { BuildConfig } from './config' +import { icon, sdk, tag, type Icon, type SDK, type Tag } from './validators' +import { errorMessages } from './error-messages' +import fs from 'node:fs/promises' +import { fromError } from 'zod-validation-error' + +type ManifestItem = { + title: string + href: string + tag?: Tag + wrap?: boolean + icon?: Icon + target?: '_blank' + sdk?: SDK[] +} + +export type ManifestGroup = { + title: string + items: Manifest + collapse?: boolean + tag?: Tag + wrap?: boolean + icon?: Icon + hideTitle?: boolean + sdk?: SDK[] +} + +type Manifest = (ManifestItem | ManifestGroup)[][] + +// Create manifest schema based on config +const createManifestSchema = (config: BuildConfig) => { + const manifestItem: z.ZodType = z + .object({ + title: z.string(), + href: z.string(), + tag: tag.optional(), + wrap: z.boolean().default(config.manifestOptions.wrapDefault), + icon: icon.optional(), + target: z.enum(['_blank']).optional(), + sdk: z.array(sdk).optional(), + }) + .strict() + + const manifestGroup: z.ZodType = z + .object({ + title: z.string(), + items: z.lazy(() => manifestSchema), + collapse: z.boolean().default(config.manifestOptions.collapseDefault), + tag: tag.optional(), + wrap: z.boolean().default(config.manifestOptions.wrapDefault), + icon: icon.optional(), + hideTitle: z.boolean().default(config.manifestOptions.hideTitleDefault), + sdk: z.array(sdk).optional(), + }) + .strict() + + const manifestSchema: z.ZodType = z.array(z.array(z.union([manifestItem, manifestGroup]))) + + return { + manifestItem, + manifestGroup, + manifestSchema, + } +} + +const parseJSON = (json: string) => { + try { + const output = JSON.parse(json) + + return [null, output as unknown] as const + } catch (error) { + return [new Error(`Failed to parse JSON`, { cause: error }), null] as const + } +} + +export const readManifest = (config: BuildConfig) => async (): Promise => { + const { manifestSchema } = createManifestSchema(config) + const unsafe_manifest = await fs.readFile(config.manifestFilePath, { encoding: 'utf-8' }) + + const [error, json] = parseJSON(unsafe_manifest) + + if (error) { + throw new Error(errorMessages['manifest-parse-error'](error)) + } + + const manifest = await z.object({ navigation: manifestSchema }).safeParseAsync(json) + + if (manifest.success === true) { + return manifest.data.navigation + } + + throw new Error(errorMessages['manifest-parse-error'](fromError(manifest.error))) +} diff --git a/scripts/lib/validators.ts b/scripts/lib/validators.ts new file mode 100644 index 0000000000..55552253df --- /dev/null +++ b/scripts/lib/validators.ts @@ -0,0 +1,104 @@ +import { z } from 'zod' +import type { BuildConfig } from './config' + +export const VALID_SDKS = [ + 'nextjs', + 'react', + 'js-frontend', + 'chrome-extension', + 'expo', + 'ios', + 'nodejs', + 'expressjs', + 'fastify', + 'react-router', + 'remix', + 'tanstack-react-start', + 'go', + 'astro', + 'nuxt', + 'vue', + 'ruby', + 'python', + 'js-backend', + 'sdk-development', + 'community-sdk', +] as const + +export type SDK = (typeof VALID_SDKS)[number] + +export const sdk = z.enum(VALID_SDKS) + +export const icon = z.enum([ + 'apple', + 'application-2', + 'arrow-up-circle', + 'astro', + 'angular', + 'block', + 'bolt', + 'book', + 'box', + 'c-sharp', + 'chart', + 'checkmark-circle', + 'chrome', + 'clerk', + 'code-bracket', + 'cog-6-teeth', + 'door', + 'elysia', + 'expressjs', + 'globe', + 'go', + 'home', + 'hono', + 'javascript', + 'koa', + 'link', + 'linkedin', + 'lock', + 'nextjs', + 'nodejs', + 'plug', + 'plus-circle', + 'python', + 'react', + 'redwood', + 'remix', + 'react-router', + 'rocket', + 'route', + 'ruby', + 'rust', + 'speedometer', + 'stacked-rectangle', + 'solid', + 'svelte', + 'tanstack', + 'user-circle', + 'user-dotted-circle', + 'vue', + 'x', + 'expo', + 'nuxt', + 'fastify', +]) + +export type Icon = z.infer + +export const tag = z.enum(['(Beta)', '(Community)']) + +export type Tag = z.infer + +export const isValidSdk = + (config: BuildConfig) => + (sdk: string): sdk is SDK => { + return config.validSdks.includes(sdk as SDK) + } + +export const isValidSdks = + (config: BuildConfig) => + (sdks: string[]): sdks is SDK[] => { + return sdks.every(isValidSdk(config)) + } From 05dd875c69acc200a262bbcd9b6a81306af8d712 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 24 Apr 2025 14:11:46 -0700 Subject: [PATCH 105/114] split out the functionality in to modules --- scripts/build-docs.test.ts | 3 +- scripts/build-docs.ts | 1181 ++--------------- scripts/lib/components/SDKLink.ts | 2 +- scripts/lib/config.ts | 2 +- scripts/lib/dev.ts | 44 + scripts/lib/error-messages.ts | 2 +- scripts/lib/io.ts | 49 + scripts/lib/manifest.ts | 136 +- scripts/lib/markdown.ts | 176 +++ scripts/lib/partials.ts | 116 ++ scripts/lib/{validators.ts => schemas.ts} | 0 scripts/lib/store.ts | 25 + scripts/lib/typedoc.ts | 125 ++ scripts/lib/utils/documentHasIfComponents.ts | 17 + .../extractComponentPropValueFromNode.ts | 98 ++ .../utils/extractHeadingFromHeadingNode.ts | 31 + scripts/lib/utils/extractSDKsFromIfProp.ts | 32 + scripts/lib/utils/findComponent.ts | 14 + scripts/lib/utils/removeMdxSuffix.ts | 17 + scripts/lib/utils/scopeHrefToSDK.ts | 18 + scripts/lib/validators/checkPartials.ts | 68 + scripts/lib/validators/checkTypedoc.ts | 62 + .../lib/validators/validateAndEmbedLinks.ts | 72 + .../lib/validators/validateUniqueHeadings.ts | 38 + 24 files changed, 1223 insertions(+), 1105 deletions(-) create mode 100644 scripts/lib/dev.ts create mode 100644 scripts/lib/io.ts create mode 100644 scripts/lib/markdown.ts create mode 100644 scripts/lib/partials.ts rename scripts/lib/{validators.ts => schemas.ts} (100%) create mode 100644 scripts/lib/store.ts create mode 100644 scripts/lib/typedoc.ts create mode 100644 scripts/lib/utils/documentHasIfComponents.ts create mode 100644 scripts/lib/utils/extractComponentPropValueFromNode.ts create mode 100644 scripts/lib/utils/extractHeadingFromHeadingNode.ts create mode 100644 scripts/lib/utils/extractSDKsFromIfProp.ts create mode 100644 scripts/lib/utils/findComponent.ts create mode 100644 scripts/lib/utils/removeMdxSuffix.ts create mode 100644 scripts/lib/utils/scopeHrefToSDK.ts create mode 100644 scripts/lib/validators/checkPartials.ts create mode 100644 scripts/lib/validators/checkTypedoc.ts create mode 100644 scripts/lib/validators/validateAndEmbedLinks.ts create mode 100644 scripts/lib/validators/validateUniqueHeadings.ts diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 7c9af0da9a..b6c96ff48a 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -4,7 +4,8 @@ import os from 'node:os' import { glob } from 'glob' import { describe, expect, onTestFinished, test } from 'vitest' -import { build, createBlankStore, invalidateFile } from './build-docs' +import { build } from './build-docs' +import { createBlankStore, invalidateFile } from './lib/store' import { createConfig } from './lib/config' const tempConfig = { diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index fe7a10d3f8..794f9e05c9 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -25,13 +25,8 @@ // - Removes .mdx from the end of docs markdown links // - Adds canonical links in frontmatter for SDK-specific docs -import watcher from '@parcel/watcher' -import { slugifyWithCounter } from '@sindresorhus/slugify' -import { toString } from 'mdast-util-to-string' -import { existsSync } from 'node:fs' import fs from 'node:fs/promises' import path from 'node:path' -import readdirp from 'readdirp' import { remark } from 'remark' import remarkFrontmatter from 'remark-frontmatter' import remarkMdx from 'remark-mdx' @@ -44,781 +39,105 @@ import reporter from 'vfile-reporter' import yaml from 'yaml' import { SDKLink } from './lib/components/SDKLink' import { createConfig, type BuildConfig } from './lib/config' -import { errorMessages, safeFail, safeMessage, shouldIgnoreWarning, type WarningsSection } from './lib/error-messages' -import { ManifestGroup, readManifest } from './lib/manifest' -import { isValidSdk, isValidSdks, VALID_SDKS, type SDK } from './lib/validators' - -const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { - const filePath = path.join(config.docsPath, docPath) - - try { - const fileContent = await fs.readFile(filePath, { encoding: 'utf-8' }) - return [null, fileContent] as const - } catch (error) { - return [new Error(errorMessages['file-read-error'](filePath), { cause: error }), null] as const - } -} - -const readDocsFolder = (config: BuildConfig) => async () => { - return readdirp.promise(config.docsPath, { - type: 'files', - fileFilter: (entry) => - // Partials are inside the docs folder, so we need to exclude them - `${config.docsRelativePath}/${entry.path}`.startsWith(config.partialsRelativePath) === false && - entry.path.endsWith('.mdx'), - }) -} - -const readPartialsFolder = (config: BuildConfig) => async () => { - return readdirp.promise(config.partialsPath, { - type: 'files', - fileFilter: '*.mdx', - }) -} - -const readPartial = (config: BuildConfig) => async (filePath: string) => { - const readFile = readMarkdownFile(config) - - const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, filePath) - - const [error, content] = await readFile(fullPath) +import { watchAndRebuild } from './lib/dev' +import { errorMessages, safeFail, safeMessage, shouldIgnoreWarning } from './lib/error-messages' +import { ensureDirectory, readDocsFolder, writeDistFile, writeSDKFile } from './lib/io' +import { flattenTree, ManifestGroup, readManifest, traverseTree, traverseTreeItemsFirst } from './lib/manifest' +import { parseInMarkdownFile } from './lib/markdown' +import { readPartialsFolder, readPartialsMarkdown } from './lib/partials' +import { isValidSdk, VALID_SDKS, type SDK } from './lib/schemas' +import { createBlankStore, DocsMap, Store } from './lib/store' +import { readTypedocsFolder, readTypedocsMarkdown } from './lib/typedoc' +import { documentHasIfComponents } from './lib/utils/documentHasIfComponents' +import { extractComponentPropValueFromNode } from './lib/utils/extractComponentPropValueFromNode' +import { extractSDKsFromIfProp } from './lib/utils/extractSDKsFromIfProp' +import { removeMdxSuffix } from './lib/utils/removeMdxSuffix' +import { scopeHrefToSDK } from './lib/utils/scopeHrefToSDK' +import { checkPartials } from './lib/validators/checkPartials' +import { checkTypedoc } from './lib/validators/checkTypedoc' +import { validateAndEmbedLinks } from './lib/validators/validateAndEmbedLinks' +import { validateUniqueHeadings } from './lib/validators/validateUniqueHeadings' - if (error) { - throw new Error(errorMessages['partial-read-error'](fullPath), { cause: error }) - } - - let partialNode: Node | null = null - - try { - const partialContentVFile = await remark() - .use(remarkFrontmatter) - .use(remarkMdx) - .use(() => (tree) => { - partialNode = tree - }) - .use(() => (tree, vfile) => { - mdastVisit( - tree, - (node) => - (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && - 'name' in node && - node.name === 'Include', - (node) => { - safeFail(config, vfile, fullPath, 'partials', 'partials-inside-partials', [], node.position) - }, - ) - }) - // Process links in partials and remove the .mdx suffix - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink)) return node - if (!('children' in node)) return node - - // We are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - return node - }) - }) - .process({ - path: `docs/_partials/${filePath}`, - value: content, - }) - - const partialContentReport = reporter([partialContentVFile], { quiet: true }) - - if (partialContentReport !== '') { - console.error(partialContentReport) - process.exit(1) - } - - if (partialNode === null) { - throw new Error(errorMessages['partial-parse-error'](filePath)) - } - - return { - path: filePath, - content, - vfile: partialContentVFile, - node: partialNode as Node, - } - } catch (error) { - console.error(`✗ Error parsing partial: ${filePath}`) - throw error - } +// Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts +if (require.main === module) { + main() } -const readPartialsMarkdown = - (config: BuildConfig, store: ReturnType) => async (paths: string[]) => { - const read = readPartial(config) - - return Promise.all( - paths.map(async (markdownPath) => { - const cachedValue = store.partialsFiles.get(markdownPath) - - if (cachedValue !== undefined) { - return cachedValue - } - - const partial = await read(markdownPath) - - store.partialsFiles.set(markdownPath, partial) - - return partial - }), - ) - } +async function main() { + const args = process.argv.slice(2) -const readTypedocsFolder = (config: BuildConfig) => async () => { - return readdirp.promise(config.typedocPath, { - type: 'files', - fileFilter: '*.mdx', + const config = createConfig({ + basePath: __dirname, + docsPath: '../docs', + baseDocsLink: '/docs/', + manifestPath: '../docs/manifest.json', + partialsPath: '../docs/_partials', + distPath: '../dist', + typedocPath: '../clerk-typedoc', + ignoreLinks: [ + '/docs/core-1', + '/docs/reference/backend-api', + '/docs/reference/frontend-api', + '/pricing', + '/support', + '/discord', + '/contact', + '/contact/sales', + '/contact/support', + '/blog', + '/changelog/2024-04-19', + ], + ignoreWarnings: { + docs: { + 'index.mdx': ['doc-not-in-manifest'], + 'guides/overview.mdx': ['doc-not-in-manifest'], + 'quickstarts/overview.mdx': ['doc-not-in-manifest'], + 'references/overview.mdx': ['doc-not-in-manifest'], + 'maintenance-mode.mdx': ['doc-not-in-manifest'], + 'deployments/staging-alternatives.mdx': ['doc-not-in-manifest'], + 'references/nextjs/usage-with-older-versions.mdx': ['doc-not-in-manifest'], + }, + typedoc: { + 'types/active-session-resource.mdx': ['link-hash-not-found'], + 'types/pending-session-resource.mdx': ['link-hash-not-found'], + }, + partials: {}, + }, + validSdks: VALID_SDKS, + manifestOptions: { + wrapDefault: true, + collapseDefault: false, + hideTitleDefault: false, + }, + cleanDist: false, + flags: { + watch: args.includes('--watch'), + controlled: args.includes('--controlled'), + }, }) -} - -const readTypedoc = (config: BuildConfig) => async (filePath: string) => { - const readFile = readMarkdownFile(config) - - const typedocPath = path.join(config.typedocRelativePath, filePath) - - const [error, content] = await readFile(typedocPath) - - if (error) { - throw new Error(errorMessages['typedoc-read-error'](typedocPath), { cause: error }) - } - - try { - let node: Node | null = null - - const vfile = await remark() - .use(remarkMdx) - .use(() => (tree) => { - node = tree - }) - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink)) return node - if (!('children' in node)) return node - - // We are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - return node - }) - }) - .process({ - path: typedocPath, - value: content, - }) - - if (node === null) { - throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) - } - - return { - path: `${removeMdxSuffix(filePath)}.mdx`, - content, - vfile, - node: node as Node, - } - } catch (error) { - let node: Node | null = null - - const vfile = await remark() - .use(() => (tree) => { - node = tree - }) - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink)) return node - if (!('children' in node)) return node - - // We are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - return node - }) - }) - .process({ - path: typedocPath, - value: content, - }) - - if (node === null) { - throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) - } - - return { - path: `${removeMdxSuffix(filePath)}.mdx`, - content, - vfile, - node: node as Node, - } - } -} - -const readTypedocsMarkdown = - (config: BuildConfig, store: ReturnType) => async (paths: string[]) => { - const read = readTypedoc(config) - - return Promise.all( - paths.map(async (filePath) => { - const cachedValue = store.typedocsFiles.get(filePath) - - if (cachedValue !== undefined) { - return cachedValue - } - - const typedoc = await read(filePath) - - store.typedocsFiles.set(filePath, typedoc) - return typedoc - }), - ) - } - -const ensureDirectory = - (config: BuildConfig) => - async (dirPath: string): Promise => { - try { - await fs.access(dirPath) - } catch { - await fs.mkdir(dirPath, { recursive: true }) - } - } - -const writeDistFile = (config: BuildConfig) => async (filePath: string, contents: string) => { - const ensureDir = ensureDirectory(config) - const fullPath = path.join(config.distPath, filePath) - await ensureDir(path.dirname(fullPath)) - await fs.writeFile(fullPath, contents, { encoding: 'utf-8' }) -} - -const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, contents: string) => { - const writeFile = writeDistFile(config) - await writeFile(path.join(sdk, filePath), contents) -} - -const removeMdxSuffix = (filePath: string) => { - if (filePath.includes('#')) { - const [url, hash] = filePath.split('#') - - if (url.endsWith('.mdx')) { - return url.slice(0, -4) + `#${hash}` - } - - return url + `#${hash}` - } - - if (filePath.endsWith('.mdx')) { - return filePath.slice(0, -4) - } - - return filePath -} - -type BlankTree }> = Array> - -const traverseTree = async < - Tree extends { items: BlankTree }, - InItem extends Extract, - InGroup extends Extract }>, - OutItem extends { href: string }, - OutGroup extends { items: BlankTree }, - OutTree extends BlankTree, ->( - tree: Tree, - itemCallback: (item: InItem, tree: Tree) => Promise = async (item) => item, - groupCallback: (group: InGroup, tree: Tree) => Promise = async (group) => group, - errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, -): Promise => { - const result = await Promise.all( - tree.items.map(async (group) => { - return await Promise.all( - group.map(async (item) => { - try { - if ('href' in item) { - return await itemCallback(item, tree) - } - - if ('items' in item && Array.isArray(item.items)) { - const newGroup = await groupCallback(item, tree) - - if (newGroup === null) return null - - // @ts-expect-error - OutGroup should always contain "items" property, so this is safe - const newItems = (await traverseTree(newGroup, itemCallback, groupCallback, errorCallback)).map((group) => - group.filter((item): item is NonNullable => item !== null), - ) - - return { - ...newGroup, - items: newItems, - } - } - - return item as OutItem - } catch (error) { - if (error instanceof Error && errorCallback !== undefined) { - errorCallback(item, error) - } else { - throw error - } - } - }), - ) - }), - ) - - return result.map((group) => - group.filter((item): item is NonNullable => item !== null), - ) as unknown as OutTree -} - -const traverseTreeItemsFirst = async < - Tree extends { items: BlankTree }, - InItem extends Extract, - InGroup extends Extract }>, - OutItem extends { href: string }, - OutGroup extends { items: BlankTree }, - OutTree extends BlankTree, ->( - tree: Tree, - itemCallback: (item: InItem, tree: Tree) => Promise = async (item) => item, - groupCallback: (group: InGroup, tree: Tree) => Promise = async (group) => group, - errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, -): Promise => { - const result = await Promise.all( - tree.items.map(async (group) => { - return await Promise.all( - group.map(async (item) => { - try { - if ('href' in item) { - return await itemCallback(item, tree) - } - - if ('items' in item && Array.isArray(item.items)) { - const newItems = (await traverseTreeItemsFirst(item, itemCallback, groupCallback, errorCallback)).map( - (group) => group.filter((item): item is NonNullable => item !== null), - ) - - const newGroup = await groupCallback({ ...item, items: newItems }, tree) - - return newGroup - } - - return item as OutItem - } catch (error) { - if (error instanceof Error && errorCallback !== undefined) { - errorCallback(item, error) - } else { - throw error - } - } - }), - ) - }), - ) - - return result.map((group) => - group.filter((item): item is NonNullable => item !== null), - ) as unknown as OutTree -} - -function flattenTree< - Tree extends BlankTree, - InItem extends Extract, - InGroup extends Extract }>, ->(tree: Tree): InItem[] { - const result: InItem[] = [] - - for (const group of tree) { - for (const itemOrGroup of group) { - if ('href' in itemOrGroup) { - // It's an item - result.push(itemOrGroup) - } else if ('items' in itemOrGroup && Array.isArray(itemOrGroup.items)) { - // It's a group with its own sub-tree, flatten it - result.push(...flattenTree(itemOrGroup.items)) - } - } - } - - return result -} - -const scopeHrefToSDK = (config: BuildConfig) => (href: string, targetSDK: SDK | ':sdk:') => { - // This is external so can't change it - if (href.startsWith('/docs') === false) return href - - const hrefSegments = href.split('/') - - // This is a little hacky so we might change it - // if the url already contains the sdk, we don't need to change it - if (hrefSegments.includes(targetSDK)) { - return href - } - - // Add the sdk to the url - return `${config.baseDocsLink}${targetSDK}/${hrefSegments.slice(2).join('/')}` -} - -const findComponent = (node: Node, componentName: string) => { - // Check if it's an MDX component - if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') { - return undefined - } - - // Check if it's the correct component - if (!('name' in node)) return undefined - if (node.name !== componentName) return undefined - - return node -} - -const extractComponentPropValueFromNode = ( - config: BuildConfig, - node: Node, - vfile: VFile | undefined, - componentName: string, - propName: string, - required = true, - section: WarningsSection, - filePath: string, -): string | undefined => { - const component = findComponent(node, componentName) - - if (component === undefined) return undefined - - // Check for attributes - if (!('attributes' in component)) { - if (vfile) { - safeMessage(config, vfile, filePath, section, 'component-no-props', [componentName], component.position) - } - return undefined - } - - if (!Array.isArray(component.attributes)) { - if (vfile) { - safeMessage( - config, - vfile, - filePath, - section, - 'component-attributes-not-array', - [componentName], - component.position, - ) - } - return undefined - } - - // Find the requested prop - const propAttribute = component.attributes.find((attribute) => attribute.name === propName) - - if (propAttribute === undefined) { - if (required === true && vfile) { - safeMessage( - config, - vfile, - filePath, - section, - 'component-missing-attribute', - [componentName, propName], - component.position, - ) - } - return undefined - } + const store = createBlankStore() - const value = propAttribute.value - - if (value === undefined) { - if (required === true && vfile) { - safeMessage( - config, - vfile, - filePath, - section, - 'component-attribute-no-value', - [componentName, propName], - component.position, - ) - } - return undefined - } + const output = await build(store, config) - // Handle both string values and object values (like JSX expressions) - if (typeof value === 'string') { - return value - } else if (typeof value === 'object' && 'value' in value) { - return value.value + if (config.flags.controlled) { + console.info('---initial-build-complete---') } - if (vfile) { - safeMessage( - config, - vfile, - filePath, - section, - 'component-attribute-unsupported-type', - [componentName, propName], - component.position, - ) + if (output !== '') { + console.info(output) } - return undefined -} -const extractSDKsFromIfProp = - (config: BuildConfig) => - (node: Node, vfile: VFile | undefined, sdkProp: string, section: WarningsSection, filePath: string) => { - const isValidItem = isValidSdk(config) - const isValidItems = isValidSdks(config) + if (config.flags.watch) { + console.info(`Watching for changes...`) - if (sdkProp.includes('", "') || sdkProp.includes("', '") || sdkProp.includes('["') || sdkProp.includes('"]')) { - const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) as string[] - if (isValidItems(sdks)) { - return sdks - } else { - const invalidSDKs = sdks.filter((sdk) => !isValidItem(sdk)) - if (vfile) { - safeMessage(config, vfile, filePath, section, 'invalid-sdks-in-if', [invalidSDKs], node.position) - } - } - } else { - if (isValidItem(sdkProp)) { - return [sdkProp] - } else { - if (vfile) { - safeMessage(config, vfile, filePath, section, 'invalid-sdk-in-if', [sdkProp], node.position) - } - } - } + watchAndRebuild(store, { ...config, cleanDist: true }, build) + } else if (output !== '') { + process.exit(1) } - -const extractHeadingFromHeadingNode = (node: Node) => { - // eg # test {{ id: 'my-heading' }} - // This is for remapping the hash to the custom id - const id = - ('children' in node && - Array.isArray(node.children) && - (node?.children - ?.find( - (child: unknown) => - typeof child === 'object' && child !== null && 'type' in child && child?.type === 'mdxTextExpression', - ) - ?.data?.estree?.body?.find( - (child: unknown) => - typeof child === 'object' && child !== null && 'type' in child && child?.type === 'ExpressionStatement', - ) - ?.expression?.properties?.find( - (prop: unknown) => - typeof prop === 'object' && - prop !== null && - 'key' in prop && - typeof prop.key === 'object' && - prop.key !== null && - 'name' in prop.key && - prop.key.name === 'id', - )?.value?.value as string | undefined)) || - undefined - - return id } -const documentHasIfComponents = (tree: Node) => { - let found = false - - mdastVisit(tree, (node) => { - const ifSrc = findComponent(node, 'If') - - if (ifSrc !== undefined) { - found = true - } - }) - - return found -} - -const parseInMarkdownFile = - (config: BuildConfig) => - async ( - href: string, - partials: { path: string; content: string; node: Node }[], - typedocs: { path: string; content: string; node: Node }[], - inManifest: boolean, - section: WarningsSection, - ) => { - const readFile = readMarkdownFile(config) - const validateSDKs = isValidSdks(config) - const [error, fileContent] = await readFile(`${href}.mdx`.replace(config.baseDocsLink, '')) - - if (error !== null) { - throw new Error(errorMessages['markdown-read-error'](href), { - cause: error, - }) - } - - type Frontmatter = { - title: string - description?: string - sdk?: SDK[] - } - - let frontmatter: Frontmatter | undefined = undefined - - const slugify = slugifyWithCounter() - const headingsHashes = new Set() - const filePath = `${href}.mdx` - let node: Node | undefined = undefined - - const vfile = await remark() - .use(remarkFrontmatter) - .use(remarkMdx) - .use(() => (tree, vfile) => { - node = tree - - if (inManifest === false) { - safeMessage(config, vfile, filePath, section, 'doc-not-in-manifest', []) - } - - if (href !== encodeURI(href)) { - safeFail(config, vfile, filePath, section, 'invalid-href-encoding', [href]) - } - }) - // validate and extract out the frontmatter - .use(() => (tree, vfile) => { - mdastVisit( - tree, - (node) => node.type === 'yaml' && 'value' in node, - (node) => { - if (!('value' in node)) return - if (typeof node.value !== 'string') return - - const frontmatterYaml: Record<'title' | 'description' | 'sdk', string | undefined> = yaml.parse(node.value) - - const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') - - if (frontmatterSDKs !== undefined && validateSDKs(frontmatterSDKs) === false) { - const invalidSDKs = frontmatterSDKs.filter((sdk) => isValidSdk(config)(sdk) === false) - safeFail( - config, - vfile, - filePath, - section, - 'invalid-sdk-in-frontmatter', - [invalidSDKs, config.validSdks as SDK[]], - node.position, - ) - return - } - - if (frontmatterYaml.title === undefined) { - safeFail(config, vfile, filePath, section, 'frontmatter-missing-title', [], node.position) - return - } - - if (frontmatterYaml.description === undefined) { - safeMessage(config, vfile, filePath, section, 'frontmatter-missing-description', [], node.position) - } - - frontmatter = { - title: frontmatterYaml.title, - description: frontmatterYaml.description, - sdk: frontmatterSDKs, - } - }, - ) - - if (frontmatter === undefined) { - safeFail(config, vfile, filePath, section, 'frontmatter-parse-failed', [href]) - return - } - }) - .use(checkPartials(config, partials, filePath, { reportWarnings: true, embed: false })) - .use(checkTypedoc(config, typedocs, filePath, { reportWarnings: true, embed: false })) - .process({ - path: `${href.substring(1)}.mdx`, - value: fileContent, - }) - - // This needs to be done separately as some further validation expects the partials to not be embedded - // but we need to embed it to get all the headings to check - await remark() - .use(remarkFrontmatter) - .use(remarkMdx) - .use(checkPartials(config, partials, filePath, { reportWarnings: true, embed: true })) - .use(checkTypedoc(config, typedocs, filePath, { reportWarnings: true, embed: true })) - // extract out the headings to check hashes in links - .use(() => (tree, vfile) => { - const documentContainsIfComponent = documentHasIfComponents(tree) - - mdastVisit( - tree, - (node) => node.type === 'heading', - (node) => { - const id = extractHeadingFromHeadingNode(node) - - if (id !== undefined) { - if (documentContainsIfComponent === false && headingsHashes.has(id)) { - safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [href, id]) - } - - headingsHashes.add(id) - } else { - const slug = slugify(toString(node).trim()) - - if (documentContainsIfComponent === false && headingsHashes.has(slug)) { - safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [href, slug]) - } - - headingsHashes.add(slug) - } - }, - ) - }) - .process({ - path: `${href.substring(1)}.mdx`, - value: fileContent, - }) - - if (node === undefined) { - throw new Error(errorMessages['doc-parse-failed'](href)) - } - - if (frontmatter === undefined) { - throw new Error(errorMessages['frontmatter-parse-failed'](href)) - } - - return { - href, - sdk: (frontmatter as Frontmatter).sdk, - vfile, - headingsHashes, - frontmatter: frontmatter as Frontmatter, - node: node as Node, - } - } - -export const createBlankStore = () => ({ - markdownFiles: new Map>>>(), - partialsFiles: new Map>>>(), - typedocsFiles: new Map>>>(), -}) - -type DocsMap = Map>>> - -export const build = async (store: ReturnType, config: BuildConfig) => { +export async function build(store: Store, config: BuildConfig) { // Apply currying to create functions pre-configured with config const ensureDir = ensureDirectory(config) const getManifest = readManifest(config) @@ -1497,337 +816,3 @@ template: wide return reporter([...coreVFiles, ...partialsVFiles, ...typedocVFiles, ...flatSdkSpecificVFiles], { quiet: true }) } - -export const invalidateFile = - (store: ReturnType, config: BuildConfig) => (filePath: string) => { - store.markdownFiles.delete(removeMdxSuffix(`${config.baseDocsLink}${path.relative(config.docsPath, filePath)}`)) - store.partialsFiles.delete(path.relative(config.partialsPath, filePath)) - store.typedocsFiles.delete(path.relative(config.typedocPath, filePath)) - } - -const watchAndRebuild = (store: ReturnType, config: BuildConfig) => { - const invalidate = invalidateFile(store, config) - - const handleFileChange: watcher.SubscribeCallback = async (error, events) => { - if (error !== null) { - console.error(error) - return - } - - events.forEach((event) => { - invalidate(event.path) - }) - - try { - const now = performance.now() - - const output = await build(store, config) - - if (config.flags.controlled) { - console.info('---rebuild-complete---') - } - - const after = performance.now() - - console.info(`Rebuilt docs in ${after - now} milliseconds`) - - if (output !== '') { - console.info(output) - } - } catch (error) { - console.error(error) - - return - } - } - - watcher.subscribe(config.docsPath, handleFileChange) - watcher.subscribe(config.typedocPath, handleFileChange) -} - -const main = async () => { - const args = process.argv.slice(2) - - const config = createConfig({ - basePath: __dirname, - docsPath: '../docs', - baseDocsLink: '/docs/', - manifestPath: '../docs/manifest.json', - partialsPath: '../docs/_partials', - distPath: '../dist', - typedocPath: '../clerk-typedoc', - ignoreLinks: [ - '/docs/core-1', - '/docs/reference/backend-api', - '/docs/reference/frontend-api', - '/pricing', - '/support', - '/discord', - '/contact', - '/contact/sales', - '/contact/support', - '/blog', - '/changelog/2024-04-19', - ], - ignoreWarnings: { - docs: { - 'index.mdx': ['doc-not-in-manifest'], - 'guides/overview.mdx': ['doc-not-in-manifest'], - 'quickstarts/overview.mdx': ['doc-not-in-manifest'], - 'references/overview.mdx': ['doc-not-in-manifest'], - 'maintenance-mode.mdx': ['doc-not-in-manifest'], - 'deployments/staging-alternatives.mdx': ['doc-not-in-manifest'], - 'references/nextjs/usage-with-older-versions.mdx': ['doc-not-in-manifest'], - }, - typedoc: { - 'types/active-session-resource.mdx': ['link-hash-not-found'], - 'types/pending-session-resource.mdx': ['link-hash-not-found'], - }, - partials: {}, - }, - validSdks: VALID_SDKS, - manifestOptions: { - wrapDefault: true, - collapseDefault: false, - hideTitleDefault: false, - }, - cleanDist: false, - flags: { - watch: args.includes('--watch'), - controlled: args.includes('--controlled'), - }, - }) - - const store = createBlankStore() - - const output = await build(store, config) - - if (config.flags.controlled) { - console.info('---initial-build-complete---') - } - - if (output !== '') { - console.info(output) - } - - if (config.flags.watch) { - console.info(`Watching for changes...`) - - watchAndRebuild(store, { ...config, cleanDist: true }) - } else if (output !== '') { - process.exit(1) - } -} - -// Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts -if (require.main === module) { - main() -} - -// Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component -const validateAndEmbedLinks = - (config: BuildConfig, docsMap: DocsMap, filePath: string, section: WarningsSection) => - () => - (tree: Node, vfile: VFile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink)) return node - if (!('children' in node)) return node - - // we are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - const [url, hash] = (node.url as string).split('#') - - const ignore = config.ignoredLink(url) - if (ignore === true) return node - - const doc = docsMap.get(url) - - if (doc === undefined) { - safeMessage(config, vfile, filePath, section, 'link-doc-not-found', [url], node.position) - return node - } - - if (hash !== undefined) { - const hasHash = doc.headingsHashes.has(hash) - - if (hasHash === false) { - safeMessage(config, vfile, filePath, section, 'link-hash-not-found', [hash, url], node.position) - } - } - - if (doc.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience - - const firstChild = node.children?.[0] - const childIsCodeBlock = firstChild?.type === 'inlineCode' - - if (childIsCodeBlock) { - firstChild.type = 'text' - - return SDKLink({ - href: scopeHrefToSDK(config)(url, ':sdk:'), - sdks: doc.sdk, - code: true, - }) - } - - return SDKLink({ - href: scopeHrefToSDK(config)(url, ':sdk:'), - sdks: doc.sdk, - code: false, - children: node.children, - }) - } - - return node - }) - } - -const checkPartials = - ( - config: BuildConfig, - partials: { - node: Node - path: string - }[], - filePath: string, - options: { - reportWarnings: boolean - embed: boolean - }, - ) => - () => - (tree: Node, vfile: VFile) => { - mdastMap(tree, (node) => { - const partialSrc = extractComponentPropValueFromNode( - config, - node, - vfile, - 'Include', - 'src', - true, - 'docs', - filePath, - ) - - if (partialSrc === undefined) return node - - if (partialSrc.startsWith('_partials/') === false) { - if (options.reportWarnings === true) { - safeMessage(config, vfile, filePath, 'docs', 'include-src-not-partials', [], node.position) - } - return node - } - - const partial = partials.find((partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`) - - if (partial === undefined) { - if (options.reportWarnings === true) { - safeMessage( - config, - vfile, - filePath, - 'docs', - 'partial-not-found', - [removeMdxSuffix(partialSrc)], - node.position, - ) - } - return node - } - - if (options.embed === true) { - return Object.assign(node, partial.node) - } - - return node - }) - } - -const checkTypedoc = - ( - config: BuildConfig, - typedocs: { path: string; node: Node }[], - filePath: string, - options: { reportWarnings: boolean; embed: boolean }, - ) => - () => - (tree: Node, vfile: VFile) => { - mdastMap(tree, (node) => { - const typedocSrc = extractComponentPropValueFromNode( - config, - node, - vfile, - 'Typedoc', - 'src', - true, - 'docs', - filePath, - ) - - if (typedocSrc === undefined) return node - - const typedocFolderExists = existsSync(config.typedocPath) - - if (typedocFolderExists === false && options.reportWarnings === true) { - throw new Error(errorMessages['typedoc-folder-not-found'](config.typedocPath)) - } - - const typedoc = typedocs.find(({ path }) => path === `${removeMdxSuffix(typedocSrc)}.mdx`) - - if (typedoc === undefined) { - if (options.reportWarnings === true) { - safeMessage( - config, - vfile, - filePath, - 'docs', - 'typedoc-not-found', - [`${removeMdxSuffix(typedocSrc)}.mdx`], - node.position, - ) - } - return node - } - - if (options.embed === true) { - return Object.assign(node, typedoc.node) - } - - return node - }) - } - -const validateUniqueHeadings = - (config: BuildConfig, filePath: string, section: WarningsSection) => () => (tree: Node, vfile: VFile) => { - const headingsHashes = new Set() - const slugify = slugifyWithCounter() - - mdastVisit( - tree, - (node) => node.type === 'heading', - (node) => { - const id = extractHeadingFromHeadingNode(node) - - if (id !== undefined) { - if (headingsHashes.has(id)) { - safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [filePath, id]) - } - - headingsHashes.add(id) - } else { - const slug = slugify(toString(node).trim()) - - if (headingsHashes.has(slug)) { - safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [filePath, slug]) - } - - headingsHashes.add(slug) - } - }, - ) - } diff --git a/scripts/lib/components/SDKLink.ts b/scripts/lib/components/SDKLink.ts index cd7915dce1..699ea0298c 100644 --- a/scripts/lib/components/SDKLink.ts +++ b/scripts/lib/components/SDKLink.ts @@ -1,4 +1,4 @@ -import type { SDK } from '../validators' +import type { SDK } from '../schemas' import { u as mdastBuilder } from 'unist-builder' export const SDKLink = ( diff --git a/scripts/lib/config.ts b/scripts/lib/config.ts index 63643b66d8..c32f987275 100644 --- a/scripts/lib/config.ts +++ b/scripts/lib/config.ts @@ -1,5 +1,5 @@ import path from 'node:path' -import type { SDK } from './validators' +import type { SDK } from './schemas' type BuildConfigOptions = { basePath: string diff --git a/scripts/lib/dev.ts b/scripts/lib/dev.ts new file mode 100644 index 0000000000..acc73e0d5f --- /dev/null +++ b/scripts/lib/dev.ts @@ -0,0 +1,44 @@ +import watcher from '@parcel/watcher' +import type { BuildConfig } from './config' +import { invalidateFile, type Store } from './store' +import type { build } from '../build-docs' + +export const watchAndRebuild = (store: Store, config: BuildConfig, buildFunc: typeof build) => { + const invalidate = invalidateFile(store, config) + + const handleFileChange: watcher.SubscribeCallback = async (error, events) => { + if (error !== null) { + console.error(error) + return + } + + events.forEach((event) => { + invalidate(event.path) + }) + + try { + const now = performance.now() + + const output = await buildFunc(store, config) + + if (config.flags.controlled) { + console.info('---rebuild-complete---') + } + + const after = performance.now() + + console.info(`Rebuilt docs in ${after - now} milliseconds`) + + if (output !== '') { + console.info(output) + } + } catch (error) { + console.error(error) + + return + } + } + + watcher.subscribe(config.docsPath, handleFileChange) + watcher.subscribe(config.typedocPath, handleFileChange) +} diff --git a/scripts/lib/error-messages.ts b/scripts/lib/error-messages.ts index 3e9cbae1df..05821cfb5d 100644 --- a/scripts/lib/error-messages.ts +++ b/scripts/lib/error-messages.ts @@ -2,7 +2,7 @@ import type { VFile } from 'vfile' import type { ValidationError } from 'zod-validation-error' import type { BuildConfig } from './config' import type { Position } from 'unist' -import type { SDK } from './validators' +import type { SDK } from './schemas' export const errorMessages = { // Manifest errors diff --git a/scripts/lib/io.ts b/scripts/lib/io.ts new file mode 100644 index 0000000000..febea67ac5 --- /dev/null +++ b/scripts/lib/io.ts @@ -0,0 +1,49 @@ +import { errorMessages } from './error-messages' +import fs from 'node:fs/promises' +import path from 'node:path' +import type { BuildConfig } from './config' +import readdirp from 'readdirp' +import type { SDK } from './schemas' + +export const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { + const filePath = path.join(config.docsPath, docPath) + + try { + const fileContent = await fs.readFile(filePath, { encoding: 'utf-8' }) + return [null, fileContent] as const + } catch (error) { + return [new Error(errorMessages['file-read-error'](filePath), { cause: error }), null] as const + } +} + +export const readDocsFolder = (config: BuildConfig) => async () => { + return readdirp.promise(config.docsPath, { + type: 'files', + fileFilter: (entry) => + // Partials are inside the docs folder, so we need to exclude them + `${config.docsRelativePath}/${entry.path}`.startsWith(config.partialsRelativePath) === false && + entry.path.endsWith('.mdx'), + }) +} + +export const ensureDirectory = + (config: BuildConfig) => + async (dirPath: string): Promise => { + try { + await fs.access(dirPath) + } catch { + await fs.mkdir(dirPath, { recursive: true }) + } + } + +export const writeDistFile = (config: BuildConfig) => async (filePath: string, contents: string) => { + const ensureDir = ensureDirectory(config) + const fullPath = path.join(config.distPath, filePath) + await ensureDir(path.dirname(fullPath)) + await fs.writeFile(fullPath, contents, { encoding: 'utf-8' }) +} + +export const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, contents: string) => { + const writeFile = writeDistFile(config) + await writeFile(path.join(sdk, filePath), contents) +} diff --git a/scripts/lib/manifest.ts b/scripts/lib/manifest.ts index 8b815ba6a2..575681a043 100644 --- a/scripts/lib/manifest.ts +++ b/scripts/lib/manifest.ts @@ -1,9 +1,9 @@ +import fs from 'node:fs/promises' import { z } from 'zod' +import { fromError } from 'zod-validation-error' import type { BuildConfig } from './config' -import { icon, sdk, tag, type Icon, type SDK, type Tag } from './validators' import { errorMessages } from './error-messages' -import fs from 'node:fs/promises' -import { fromError } from 'zod-validation-error' +import { icon, sdk, tag, type Icon, type SDK, type Tag } from './schemas' type ManifestItem = { title: string @@ -92,3 +92,133 @@ export const readManifest = (config: BuildConfig) => async (): Promise throw new Error(errorMessages['manifest-parse-error'](fromError(manifest.error))) } + +export type BlankTree }> = Array> + +export const traverseTree = async < + Tree extends { items: BlankTree }, + InItem extends Extract, + InGroup extends Extract }>, + OutItem extends { href: string }, + OutGroup extends { items: BlankTree }, + OutTree extends BlankTree, +>( + tree: Tree, + itemCallback: (item: InItem, tree: Tree) => Promise = async (item) => item, + groupCallback: (group: InGroup, tree: Tree) => Promise = async (group) => group, + errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, +): Promise => { + const result = await Promise.all( + tree.items.map(async (group) => { + return await Promise.all( + group.map(async (item) => { + try { + if ('href' in item) { + return await itemCallback(item, tree) + } + + if ('items' in item && Array.isArray(item.items)) { + const newGroup = await groupCallback(item, tree) + + if (newGroup === null) return null + + // @ts-expect-error - OutGroup should always contain "items" property, so this is safe + const newItems = (await traverseTree(newGroup, itemCallback, groupCallback, errorCallback)).map((group) => + group.filter((item): item is NonNullable => item !== null), + ) + + return { + ...newGroup, + items: newItems, + } + } + + return item as OutItem + } catch (error) { + if (error instanceof Error && errorCallback !== undefined) { + errorCallback(item, error) + } else { + throw error + } + } + }), + ) + }), + ) + + return result.map((group) => + group.filter((item): item is NonNullable => item !== null), + ) as unknown as OutTree +} + +export const traverseTreeItemsFirst = async < + Tree extends { items: BlankTree }, + InItem extends Extract, + InGroup extends Extract }>, + OutItem extends { href: string }, + OutGroup extends { items: BlankTree }, + OutTree extends BlankTree, +>( + tree: Tree, + itemCallback: (item: InItem, tree: Tree) => Promise = async (item) => item, + groupCallback: (group: InGroup, tree: Tree) => Promise = async (group) => group, + errorCallback?: (item: InItem | InGroup, error: Error) => void | Promise, +): Promise => { + const result = await Promise.all( + tree.items.map(async (group) => { + return await Promise.all( + group.map(async (item) => { + try { + if ('href' in item) { + return await itemCallback(item, tree) + } + + if ('items' in item && Array.isArray(item.items)) { + const newItems = (await traverseTreeItemsFirst(item, itemCallback, groupCallback, errorCallback)).map( + (group) => group.filter((item): item is NonNullable => item !== null), + ) + + const newGroup = await groupCallback({ ...item, items: newItems }, tree) + + return newGroup + } + + return item as OutItem + } catch (error) { + if (error instanceof Error && errorCallback !== undefined) { + errorCallback(item, error) + } else { + throw error + } + } + }), + ) + }), + ) + + return result.map((group) => + group.filter((item): item is NonNullable => item !== null), + ) as unknown as OutTree +} + +export function flattenTree< + Tree extends BlankTree, + InItem extends Extract, + InGroup extends Extract }>, +>(tree: Tree): InItem[] { + const result: InItem[] = [] + + for (const group of tree) { + for (const itemOrGroup of group) { + if ('href' in itemOrGroup) { + // It's an item + result.push(itemOrGroup) + } else if ('items' in itemOrGroup && Array.isArray(itemOrGroup.items)) { + // It's a group with its own sub-tree, flatten it + result.push(...flattenTree(itemOrGroup.items)) + } + } + } + + return result +} diff --git a/scripts/lib/markdown.ts b/scripts/lib/markdown.ts new file mode 100644 index 0000000000..2c86fca6e0 --- /dev/null +++ b/scripts/lib/markdown.ts @@ -0,0 +1,176 @@ +import { slugifyWithCounter } from '@sindresorhus/slugify' +import { toString } from 'mdast-util-to-string' +import { remark } from 'remark' +import remarkFrontmatter from 'remark-frontmatter' +import remarkMdx from 'remark-mdx' +import { Node } from 'unist' +import { visit as mdastVisit } from 'unist-util-visit' +import yaml from 'yaml' +import { type BuildConfig } from './config' +import { errorMessages, safeFail, safeMessage, type WarningsSection } from './error-messages' +import { readMarkdownFile } from './io' +import { isValidSdk, isValidSdks, type SDK } from './schemas' +import { documentHasIfComponents } from './utils/documentHasIfComponents' +import { extractHeadingFromHeadingNode } from './utils/extractHeadingFromHeadingNode' +import { checkPartials } from './validators/checkPartials' +import { checkTypedoc } from './validators/checkTypedoc' + +export const parseInMarkdownFile = + (config: BuildConfig) => + async ( + href: string, + partials: { path: string; content: string; node: Node }[], + typedocs: { path: string; content: string; node: Node }[], + inManifest: boolean, + section: WarningsSection, + ) => { + const readFile = readMarkdownFile(config) + const validateSDKs = isValidSdks(config) + const [error, fileContent] = await readFile(`${href}.mdx`.replace(config.baseDocsLink, '')) + + if (error !== null) { + throw new Error(errorMessages['markdown-read-error'](href), { + cause: error, + }) + } + + type Frontmatter = { + title: string + description?: string + sdk?: SDK[] + } + + let frontmatter: Frontmatter | undefined = undefined + + const slugify = slugifyWithCounter() + const headingsHashes = new Set() + const filePath = `${href}.mdx` + let node: Node | undefined = undefined + + const vfile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) + .use(() => (tree, vfile) => { + node = tree + + if (inManifest === false) { + safeMessage(config, vfile, filePath, section, 'doc-not-in-manifest', []) + } + + if (href !== encodeURI(href)) { + safeFail(config, vfile, filePath, section, 'invalid-href-encoding', [href]) + } + }) + // validate and extract out the frontmatter + .use(() => (tree, vfile) => { + mdastVisit( + tree, + (node) => node.type === 'yaml' && 'value' in node, + (node) => { + if (!('value' in node)) return + if (typeof node.value !== 'string') return + + const frontmatterYaml: Record<'title' | 'description' | 'sdk', string | undefined> = yaml.parse(node.value) + + const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') + + if (frontmatterSDKs !== undefined && validateSDKs(frontmatterSDKs) === false) { + const invalidSDKs = frontmatterSDKs.filter((sdk) => isValidSdk(config)(sdk) === false) + safeFail( + config, + vfile, + filePath, + section, + 'invalid-sdk-in-frontmatter', + [invalidSDKs, config.validSdks as SDK[]], + node.position, + ) + return + } + + if (frontmatterYaml.title === undefined) { + safeFail(config, vfile, filePath, section, 'frontmatter-missing-title', [], node.position) + return + } + + if (frontmatterYaml.description === undefined) { + safeMessage(config, vfile, filePath, section, 'frontmatter-missing-description', [], node.position) + } + + frontmatter = { + title: frontmatterYaml.title, + description: frontmatterYaml.description, + sdk: frontmatterSDKs, + } + }, + ) + + if (frontmatter === undefined) { + safeFail(config, vfile, filePath, section, 'frontmatter-parse-failed', [href]) + return + } + }) + .use(checkPartials(config, partials, filePath, { reportWarnings: true, embed: false })) + .use(checkTypedoc(config, typedocs, filePath, { reportWarnings: true, embed: false })) + .process({ + path: `${href.substring(1)}.mdx`, + value: fileContent, + }) + + // This needs to be done separately as some further validation expects the partials to not be embedded + // but we need to embed it to get all the headings to check + await remark() + .use(remarkFrontmatter) + .use(remarkMdx) + .use(checkPartials(config, partials, filePath, { reportWarnings: true, embed: true })) + .use(checkTypedoc(config, typedocs, filePath, { reportWarnings: true, embed: true })) + // extract out the headings to check hashes in links + .use(() => (tree, vfile) => { + const documentContainsIfComponent = documentHasIfComponents(tree) + + mdastVisit( + tree, + (node) => node.type === 'heading', + (node) => { + const id = extractHeadingFromHeadingNode(node) + + if (id !== undefined) { + if (documentContainsIfComponent === false && headingsHashes.has(id)) { + safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [href, id]) + } + + headingsHashes.add(id) + } else { + const slug = slugify(toString(node).trim()) + + if (documentContainsIfComponent === false && headingsHashes.has(slug)) { + safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [href, slug]) + } + + headingsHashes.add(slug) + } + }, + ) + }) + .process({ + path: `${href.substring(1)}.mdx`, + value: fileContent, + }) + + if (node === undefined) { + throw new Error(errorMessages['doc-parse-failed'](href)) + } + + if (frontmatter === undefined) { + throw new Error(errorMessages['frontmatter-parse-failed'](href)) + } + + return { + href, + sdk: (frontmatter as Frontmatter).sdk, + vfile, + headingsHashes, + frontmatter: frontmatter as Frontmatter, + node: node as Node, + } + } diff --git a/scripts/lib/partials.ts b/scripts/lib/partials.ts new file mode 100644 index 0000000000..af95555d99 --- /dev/null +++ b/scripts/lib/partials.ts @@ -0,0 +1,116 @@ +import path from 'node:path' +import readdirp from 'readdirp' +import { remark } from 'remark' +import remarkFrontmatter from 'remark-frontmatter' +import remarkMdx from 'remark-mdx' +import type { Node } from 'unist' +import { map as mdastMap } from 'unist-util-map' +import { visit as mdastVisit } from 'unist-util-visit' +import reporter from 'vfile-reporter' +import type { BuildConfig } from './config' +import { errorMessages, safeFail } from './error-messages' +import { readMarkdownFile } from './io' +import { removeMdxSuffix } from './utils/removeMdxSuffix' +import type { Store } from './store' + +export const readPartialsFolder = (config: BuildConfig) => async () => { + return readdirp.promise(config.partialsPath, { + type: 'files', + fileFilter: '*.mdx', + }) +} + +export const readPartial = (config: BuildConfig) => async (filePath: string) => { + const readFile = readMarkdownFile(config) + + const fullPath = path.join(config.docsRelativePath, config.partialsRelativePath, filePath) + + const [error, content] = await readFile(fullPath) + + if (error) { + throw new Error(errorMessages['partial-read-error'](fullPath), { cause: error }) + } + + let partialNode: Node | null = null + + try { + const partialContentVFile = await remark() + .use(remarkFrontmatter) + .use(remarkMdx) + .use(() => (tree) => { + partialNode = tree + }) + .use(() => (tree, vfile) => { + mdastVisit( + tree, + (node) => + (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && + 'name' in node && + node.name === 'Include', + (node) => { + safeFail(config, vfile, fullPath, 'partials', 'partials-inside-partials', [], node.position) + }, + ) + }) + // Process links in partials and remove the .mdx suffix + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith(config.baseDocsLink)) return node + if (!('children' in node)) return node + + // We are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + return node + }) + }) + .process({ + path: `docs/_partials/${filePath}`, + value: content, + }) + + const partialContentReport = reporter([partialContentVFile], { quiet: true }) + + if (partialContentReport !== '') { + console.error(partialContentReport) + process.exit(1) + } + + if (partialNode === null) { + throw new Error(errorMessages['partial-parse-error'](filePath)) + } + + return { + path: filePath, + content, + vfile: partialContentVFile, + node: partialNode as Node, + } + } catch (error) { + console.error(`✗ Error parsing partial: ${filePath}`) + throw error + } +} + +export const readPartialsMarkdown = (config: BuildConfig, store: Store) => async (paths: string[]) => { + const read = readPartial(config) + + return Promise.all( + paths.map(async (markdownPath) => { + const cachedValue = store.partialsFiles.get(markdownPath) + + if (cachedValue !== undefined) { + return cachedValue + } + + const partial = await read(markdownPath) + + store.partialsFiles.set(markdownPath, partial) + + return partial + }), + ) +} diff --git a/scripts/lib/validators.ts b/scripts/lib/schemas.ts similarity index 100% rename from scripts/lib/validators.ts rename to scripts/lib/schemas.ts diff --git a/scripts/lib/store.ts b/scripts/lib/store.ts new file mode 100644 index 0000000000..559281e3c8 --- /dev/null +++ b/scripts/lib/store.ts @@ -0,0 +1,25 @@ +import path from 'node:path' +import type { BuildConfig } from './config' +import { removeMdxSuffix } from './utils/removeMdxSuffix' +import type { readPartial } from './partials' +import type { readTypedoc } from './typedoc' +import type { parseInMarkdownFile } from './markdown' + +export type DocsMap = Map>>> +export type PartialsMap = Map>>> +export type TypedocsMap = Map>>> + +export const createBlankStore = () => ({ + markdownFiles: new Map() as DocsMap, + partialsFiles: new Map() as PartialsMap, + typedocsFiles: new Map() as TypedocsMap, +}) + +export type Store = ReturnType + +export const invalidateFile = + (store: ReturnType, config: BuildConfig) => (filePath: string) => { + store.markdownFiles.delete(removeMdxSuffix(`${config.baseDocsLink}${path.relative(config.docsPath, filePath)}`)) + store.partialsFiles.delete(path.relative(config.partialsPath, filePath)) + store.typedocsFiles.delete(path.relative(config.typedocPath, filePath)) + } diff --git a/scripts/lib/typedoc.ts b/scripts/lib/typedoc.ts new file mode 100644 index 0000000000..8fcd9d6d1a --- /dev/null +++ b/scripts/lib/typedoc.ts @@ -0,0 +1,125 @@ +import path from 'node:path' +import readdirp from 'readdirp' +import { remark } from 'remark' +import remarkMdx from 'remark-mdx' +import type { Node } from 'unist' +import { map as mdastMap } from 'unist-util-map' +import type { BuildConfig } from './config' +import { errorMessages } from './error-messages' +import { readMarkdownFile } from './io' +import { removeMdxSuffix } from './utils/removeMdxSuffix' +import type { Store } from './store' + +export const readTypedocsFolder = (config: BuildConfig) => async () => { + return readdirp.promise(config.typedocPath, { + type: 'files', + fileFilter: '*.mdx', + }) +} + +export const readTypedoc = (config: BuildConfig) => async (filePath: string) => { + const readFile = readMarkdownFile(config) + + const typedocPath = path.join(config.typedocRelativePath, filePath) + + const [error, content] = await readFile(typedocPath) + + if (error) { + throw new Error(errorMessages['typedoc-read-error'](typedocPath), { cause: error }) + } + + try { + let node: Node | null = null + + const vfile = await remark() + .use(remarkMdx) + .use(() => (tree) => { + node = tree + }) + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith(config.baseDocsLink)) return node + if (!('children' in node)) return node + + // We are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + return node + }) + }) + .process({ + path: typedocPath, + value: content, + }) + + if (node === null) { + throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) + } + + return { + path: `${removeMdxSuffix(filePath)}.mdx`, + content, + vfile, + node: node as Node, + } + } catch (error) { + let node: Node | null = null + + const vfile = await remark() + .use(() => (tree) => { + node = tree + }) + .use(() => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith(config.baseDocsLink)) return node + if (!('children' in node)) return node + + // We are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + return node + }) + }) + .process({ + path: typedocPath, + value: content, + }) + + if (node === null) { + throw new Error(errorMessages['typedoc-parse-error'](typedocPath)) + } + + return { + path: `${removeMdxSuffix(filePath)}.mdx`, + content, + vfile, + node: node as Node, + } + } +} + +export const readTypedocsMarkdown = (config: BuildConfig, store: Store) => async (paths: string[]) => { + const read = readTypedoc(config) + + return Promise.all( + paths.map(async (filePath) => { + const cachedValue = store.typedocsFiles.get(filePath) + + if (cachedValue !== undefined) { + return cachedValue + } + + const typedoc = await read(filePath) + + store.typedocsFiles.set(filePath, typedoc) + + return typedoc + }), + ) +} diff --git a/scripts/lib/utils/documentHasIfComponents.ts b/scripts/lib/utils/documentHasIfComponents.ts new file mode 100644 index 0000000000..ee9da6c9c6 --- /dev/null +++ b/scripts/lib/utils/documentHasIfComponents.ts @@ -0,0 +1,17 @@ +import { Node } from 'unist' +import { visit as mdastVisit } from 'unist-util-visit' +import { findComponent } from './findComponent' + +export const documentHasIfComponents = (tree: Node) => { + let found = false + + mdastVisit(tree, (node) => { + const ifSrc = findComponent(node, 'If') + + if (ifSrc !== undefined) { + found = true + } + }) + + return found +} diff --git a/scripts/lib/utils/extractComponentPropValueFromNode.ts b/scripts/lib/utils/extractComponentPropValueFromNode.ts new file mode 100644 index 0000000000..dcace2eee0 --- /dev/null +++ b/scripts/lib/utils/extractComponentPropValueFromNode.ts @@ -0,0 +1,98 @@ +import type { VFile } from 'vfile' +import type { BuildConfig } from '../config' +import type { Node } from 'unist' +import { safeMessage, type WarningsSection } from '../error-messages' +import { findComponent } from './findComponent' + +export const extractComponentPropValueFromNode = ( + config: BuildConfig, + node: Node, + vfile: VFile | undefined, + componentName: string, + propName: string, + required = true, + section: WarningsSection, + filePath: string, +): string | undefined => { + const component = findComponent(node, componentName) + + if (component === undefined) return undefined + + // Check for attributes + if (!('attributes' in component)) { + if (vfile) { + safeMessage(config, vfile, filePath, section, 'component-no-props', [componentName], component.position) + } + return undefined + } + + if (!Array.isArray(component.attributes)) { + if (vfile) { + safeMessage( + config, + vfile, + filePath, + section, + 'component-attributes-not-array', + [componentName], + component.position, + ) + } + return undefined + } + + // Find the requested prop + const propAttribute = component.attributes.find((attribute) => attribute.name === propName) + + if (propAttribute === undefined) { + if (required === true && vfile) { + safeMessage( + config, + vfile, + filePath, + section, + 'component-missing-attribute', + [componentName, propName], + component.position, + ) + } + return undefined + } + + const value = propAttribute.value + + if (value === undefined) { + if (required === true && vfile) { + safeMessage( + config, + vfile, + filePath, + section, + 'component-attribute-no-value', + [componentName, propName], + component.position, + ) + } + return undefined + } + + // Handle both string values and object values (like JSX expressions) + if (typeof value === 'string') { + return value + } else if (typeof value === 'object' && 'value' in value) { + return value.value + } + + if (vfile) { + safeMessage( + config, + vfile, + filePath, + section, + 'component-attribute-unsupported-type', + [componentName, propName], + component.position, + ) + } + return undefined +} diff --git a/scripts/lib/utils/extractHeadingFromHeadingNode.ts b/scripts/lib/utils/extractHeadingFromHeadingNode.ts new file mode 100644 index 0000000000..9559eeb328 --- /dev/null +++ b/scripts/lib/utils/extractHeadingFromHeadingNode.ts @@ -0,0 +1,31 @@ +import type { Node } from 'unist' + +export const extractHeadingFromHeadingNode = (node: Node) => { + // eg # test {{ id: 'my-heading' }} + // This is for remapping the hash to the custom id + const id = + ('children' in node && + Array.isArray(node.children) && + (node?.children + ?.find( + (child: unknown) => + typeof child === 'object' && child !== null && 'type' in child && child?.type === 'mdxTextExpression', + ) + ?.data?.estree?.body?.find( + (child: unknown) => + typeof child === 'object' && child !== null && 'type' in child && child?.type === 'ExpressionStatement', + ) + ?.expression?.properties?.find( + (prop: unknown) => + typeof prop === 'object' && + prop !== null && + 'key' in prop && + typeof prop.key === 'object' && + prop.key !== null && + 'name' in prop.key && + prop.key.name === 'id', + )?.value?.value as string | undefined)) || + undefined + + return id +} diff --git a/scripts/lib/utils/extractSDKsFromIfProp.ts b/scripts/lib/utils/extractSDKsFromIfProp.ts new file mode 100644 index 0000000000..d7b8cb9a3d --- /dev/null +++ b/scripts/lib/utils/extractSDKsFromIfProp.ts @@ -0,0 +1,32 @@ +import { VFile } from 'vfile' +import { BuildConfig } from '../config' +import type { Node } from 'unist' +import { safeMessage, type WarningsSection } from '../error-messages' +import { isValidSdk, isValidSdks } from '../schemas' + +export const extractSDKsFromIfProp = + (config: BuildConfig) => + (node: Node, vfile: VFile | undefined, sdkProp: string, section: WarningsSection, filePath: string) => { + const isValidItem = isValidSdk(config) + const isValidItems = isValidSdks(config) + + if (sdkProp.includes('", "') || sdkProp.includes("', '") || sdkProp.includes('["') || sdkProp.includes('"]')) { + const sdks = JSON.parse(sdkProp.replaceAll("'", '"')) as string[] + if (isValidItems(sdks)) { + return sdks + } else { + const invalidSDKs = sdks.filter((sdk) => !isValidItem(sdk)) + if (vfile) { + safeMessage(config, vfile, filePath, section, 'invalid-sdks-in-if', [invalidSDKs], node.position) + } + } + } else { + if (isValidItem(sdkProp)) { + return [sdkProp] + } else { + if (vfile) { + safeMessage(config, vfile, filePath, section, 'invalid-sdk-in-if', [sdkProp], node.position) + } + } + } + } diff --git a/scripts/lib/utils/findComponent.ts b/scripts/lib/utils/findComponent.ts new file mode 100644 index 0000000000..3851400f34 --- /dev/null +++ b/scripts/lib/utils/findComponent.ts @@ -0,0 +1,14 @@ +import type { Node } from 'unist' + +export const findComponent = (node: Node, componentName: string) => { + // Check if it's an MDX component + if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') { + return undefined + } + + // Check if it's the correct component + if (!('name' in node)) return undefined + if (node.name !== componentName) return undefined + + return node +} diff --git a/scripts/lib/utils/removeMdxSuffix.ts b/scripts/lib/utils/removeMdxSuffix.ts new file mode 100644 index 0000000000..12386db082 --- /dev/null +++ b/scripts/lib/utils/removeMdxSuffix.ts @@ -0,0 +1,17 @@ +export const removeMdxSuffix = (filePath: string) => { + if (filePath.includes('#')) { + const [url, hash] = filePath.split('#') + + if (url.endsWith('.mdx')) { + return url.slice(0, -4) + `#${hash}` + } + + return url + `#${hash}` + } + + if (filePath.endsWith('.mdx')) { + return filePath.slice(0, -4) + } + + return filePath +} diff --git a/scripts/lib/utils/scopeHrefToSDK.ts b/scripts/lib/utils/scopeHrefToSDK.ts new file mode 100644 index 0000000000..349e4366d4 --- /dev/null +++ b/scripts/lib/utils/scopeHrefToSDK.ts @@ -0,0 +1,18 @@ +import type { BuildConfig } from '../config' +import type { SDK } from '../schemas' + +export const scopeHrefToSDK = (config: BuildConfig) => (href: string, targetSDK: SDK | ':sdk:') => { + // This is external so can't change it + if (href.startsWith('/docs') === false) return href + + const hrefSegments = href.split('/') + + // This is a little hacky so we might change it + // if the url already contains the sdk, we don't need to change it + if (hrefSegments.includes(targetSDK)) { + return href + } + + // Add the sdk to the url + return `${config.baseDocsLink}${targetSDK}/${hrefSegments.slice(2).join('/')}` +} diff --git a/scripts/lib/validators/checkPartials.ts b/scripts/lib/validators/checkPartials.ts new file mode 100644 index 0000000000..fcde6d80e0 --- /dev/null +++ b/scripts/lib/validators/checkPartials.ts @@ -0,0 +1,68 @@ +import type { BuildConfig } from '../config' +import type { Node } from 'unist' +import type { VFile } from 'vfile' +import { map as mdastMap } from 'unist-util-map' +import { extractComponentPropValueFromNode } from '../utils/extractComponentPropValueFromNode' +import { safeMessage } from '../error-messages' +import { removeMdxSuffix } from '../utils/removeMdxSuffix' + +export const checkPartials = + ( + config: BuildConfig, + partials: { + node: Node + path: string + }[], + filePath: string, + options: { + reportWarnings: boolean + embed: boolean + }, + ) => + () => + (tree: Node, vfile: VFile) => { + mdastMap(tree, (node) => { + const partialSrc = extractComponentPropValueFromNode( + config, + node, + vfile, + 'Include', + 'src', + true, + 'docs', + filePath, + ) + + if (partialSrc === undefined) return node + + if (partialSrc.startsWith('_partials/') === false) { + if (options.reportWarnings === true) { + safeMessage(config, vfile, filePath, 'docs', 'include-src-not-partials', [], node.position) + } + return node + } + + const partial = partials.find((partial) => `_partials/${partial.path}` === `${removeMdxSuffix(partialSrc)}.mdx`) + + if (partial === undefined) { + if (options.reportWarnings === true) { + safeMessage( + config, + vfile, + filePath, + 'docs', + 'partial-not-found', + [removeMdxSuffix(partialSrc)], + node.position, + ) + } + return node + } + + if (options.embed === true) { + return Object.assign(node, partial.node) + } + + return node + }) + } diff --git a/scripts/lib/validators/checkTypedoc.ts b/scripts/lib/validators/checkTypedoc.ts new file mode 100644 index 0000000000..3e1a6c97b0 --- /dev/null +++ b/scripts/lib/validators/checkTypedoc.ts @@ -0,0 +1,62 @@ +import type { BuildConfig } from '../config' +import type { Node } from 'unist' +import type { VFile } from 'vfile' +import { map as mdastMap } from 'unist-util-map' +import { extractComponentPropValueFromNode } from '../utils/extractComponentPropValueFromNode' +import { errorMessages, safeMessage } from '../error-messages' +import { removeMdxSuffix } from '../utils/removeMdxSuffix' +import { existsSync } from 'node:fs' + +export const checkTypedoc = + ( + config: BuildConfig, + typedocs: { path: string; node: Node }[], + filePath: string, + options: { reportWarnings: boolean; embed: boolean }, + ) => + () => + (tree: Node, vfile: VFile) => { + mdastMap(tree, (node) => { + const typedocSrc = extractComponentPropValueFromNode( + config, + node, + vfile, + 'Typedoc', + 'src', + true, + 'docs', + filePath, + ) + + if (typedocSrc === undefined) return node + + const typedocFolderExists = existsSync(config.typedocPath) + + if (typedocFolderExists === false && options.reportWarnings === true) { + throw new Error(errorMessages['typedoc-folder-not-found'](config.typedocPath)) + } + + const typedoc = typedocs.find(({ path }) => path === `${removeMdxSuffix(typedocSrc)}.mdx`) + + if (typedoc === undefined) { + if (options.reportWarnings === true) { + safeMessage( + config, + vfile, + filePath, + 'docs', + 'typedoc-not-found', + [`${removeMdxSuffix(typedocSrc)}.mdx`], + node.position, + ) + } + return node + } + + if (options.embed === true) { + return Object.assign(node, typedoc.node) + } + + return node + }) + } diff --git a/scripts/lib/validators/validateAndEmbedLinks.ts b/scripts/lib/validators/validateAndEmbedLinks.ts new file mode 100644 index 0000000000..e523d28593 --- /dev/null +++ b/scripts/lib/validators/validateAndEmbedLinks.ts @@ -0,0 +1,72 @@ +import { Node } from 'unist' +import { map as mdastMap } from 'unist-util-map' +import type { VFile } from 'vfile' +import { SDKLink } from '../components/SDKLink' +import { type BuildConfig } from '../config' +import { safeMessage, type WarningsSection } from '../error-messages' +import { DocsMap } from '../store' +import { removeMdxSuffix } from '../utils/removeMdxSuffix' +import { scopeHrefToSDK } from '../utils/scopeHrefToSDK' + +// Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component +export const validateAndEmbedLinks = + (config: BuildConfig, docsMap: DocsMap, filePath: string, section: WarningsSection) => + () => + (tree: Node, vfile: VFile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'link') return node + if (!('url' in node)) return node + if (typeof node.url !== 'string') return node + if (!node.url.startsWith(config.baseDocsLink)) return node + if (!('children' in node)) return node + + // we are overwriting the url with the mdx suffix removed + node.url = removeMdxSuffix(node.url) + + const [url, hash] = (node.url as string).split('#') + + const ignore = config.ignoredLink(url) + if (ignore === true) return node + + const doc = docsMap.get(url) + + if (doc === undefined) { + safeMessage(config, vfile, filePath, section, 'link-doc-not-found', [url], node.position) + return node + } + + if (hash !== undefined) { + const hasHash = doc.headingsHashes.has(hash) + + if (hasHash === false) { + safeMessage(config, vfile, filePath, section, 'link-hash-not-found', [hash, url], node.position) + } + } + + if (doc.sdk !== undefined) { + // we are going to swap it for the sdk link component to give the users a great experience + + const firstChild = node.children?.[0] + const childIsCodeBlock = firstChild?.type === 'inlineCode' + + if (childIsCodeBlock) { + firstChild.type = 'text' + + return SDKLink({ + href: scopeHrefToSDK(config)(url, ':sdk:'), + sdks: doc.sdk, + code: true, + }) + } + + return SDKLink({ + href: scopeHrefToSDK(config)(url, ':sdk:'), + sdks: doc.sdk, + code: false, + children: node.children, + }) + } + + return node + }) + } diff --git a/scripts/lib/validators/validateUniqueHeadings.ts b/scripts/lib/validators/validateUniqueHeadings.ts new file mode 100644 index 0000000000..4f1fee1f91 --- /dev/null +++ b/scripts/lib/validators/validateUniqueHeadings.ts @@ -0,0 +1,38 @@ +import { slugifyWithCounter } from '@sindresorhus/slugify' +import { toString } from 'mdast-util-to-string' +import { Node } from 'unist' +import { visit as mdastVisit } from 'unist-util-visit' +import type { VFile } from 'vfile' +import { type BuildConfig } from '../config' +import { safeFail, type WarningsSection } from '../error-messages' +import { extractHeadingFromHeadingNode } from '../utils/extractHeadingFromHeadingNode' + +export const validateUniqueHeadings = + (config: BuildConfig, filePath: string, section: WarningsSection) => () => (tree: Node, vfile: VFile) => { + const headingsHashes = new Set() + const slugify = slugifyWithCounter() + + mdastVisit( + tree, + (node) => node.type === 'heading', + (node) => { + const id = extractHeadingFromHeadingNode(node) + + if (id !== undefined) { + if (headingsHashes.has(id)) { + safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [filePath, id]) + } + + headingsHashes.add(id) + } else { + const slug = slugify(toString(node).trim()) + + if (headingsHashes.has(slug)) { + safeFail(config, vfile, filePath, section, 'duplicate-heading-id', [filePath, slug]) + } + + headingsHashes.add(slug) + } + }, + ) + } From 006c480d1f15554298ce63732e5eeb38df11055a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 24 Apr 2025 15:33:22 -0700 Subject: [PATCH 106/114] Add comments for each file to explain its purpose --- scripts/lib/components/SDKLink.ts | 3 + scripts/lib/config.ts | 3 + scripts/lib/dev.ts | 3 + scripts/lib/error-messages.ts | 3 + scripts/lib/io.ts | 16 +++++ scripts/lib/manifest.ts | 58 +++++++++---------- scripts/lib/markdown.ts | 11 ++++ scripts/lib/partials.ts | 5 ++ scripts/lib/schemas.ts | 2 + scripts/lib/store.ts | 5 ++ scripts/lib/typedoc.ts | 6 ++ scripts/lib/utils/documentHasIfComponents.ts | 2 + .../extractComponentPropValueFromNode.ts | 4 ++ .../utils/extractHeadingFromHeadingNode.ts | 3 + scripts/lib/utils/extractSDKsFromIfProp.ts | 4 ++ scripts/lib/utils/findComponent.ts | 2 + scripts/lib/utils/removeMdxSuffix.ts | 2 + scripts/lib/utils/scopeHrefToSDK.ts | 2 + scripts/lib/validators/checkPartials.ts | 6 ++ scripts/lib/validators/checkTypedoc.ts | 7 +++ .../lib/validators/validateAndEmbedLinks.ts | 7 ++- .../lib/validators/validateUniqueHeadings.ts | 2 + 22 files changed, 126 insertions(+), 30 deletions(-) diff --git a/scripts/lib/components/SDKLink.ts b/scripts/lib/components/SDKLink.ts index 699ea0298c..cdbb883217 100644 --- a/scripts/lib/components/SDKLink.ts +++ b/scripts/lib/components/SDKLink.ts @@ -1,3 +1,6 @@ +// a fake component that takes the possible props and creates a mdx node +// SDKLink is used for the links that get replaced as they point to a sdk scoped page + import type { SDK } from '../schemas' import { u as mdastBuilder } from 'unist-builder' diff --git a/scripts/lib/config.ts b/scripts/lib/config.ts index c32f987275..5f8e10d625 100644 --- a/scripts/lib/config.ts +++ b/scripts/lib/config.ts @@ -1,3 +1,6 @@ +// For the test suite to work effectively we need to be able to +// configure the builds, this file defines the config object + import path from 'node:path' import type { SDK } from './schemas' diff --git a/scripts/lib/dev.ts b/scripts/lib/dev.ts index acc73e0d5f..f767547a60 100644 --- a/scripts/lib/dev.ts +++ b/scripts/lib/dev.ts @@ -1,3 +1,6 @@ +// for development mode, this function watches the markdown, +// invalidates the cache and kicks off a rebuild of the docs + import watcher from '@parcel/watcher' import type { BuildConfig } from './config' import { invalidateFile, type Store } from './store' diff --git a/scripts/lib/error-messages.ts b/scripts/lib/error-messages.ts index 05821cfb5d..46637450af 100644 --- a/scripts/lib/error-messages.ts +++ b/scripts/lib/error-messages.ts @@ -1,3 +1,6 @@ +// defining most of the error messages that may be thrown by the build script +// with some helper functions that check if the warning should be ignored + import type { VFile } from 'vfile' import type { ValidationError } from 'zod-validation-error' import type { BuildConfig } from './config' diff --git a/scripts/lib/io.ts b/scripts/lib/io.ts index febea67ac5..c99504b210 100644 --- a/scripts/lib/io.ts +++ b/scripts/lib/io.ts @@ -5,6 +5,7 @@ import type { BuildConfig } from './config' import readdirp from 'readdirp' import type { SDK } from './schemas' +// Read in a markdown file from the docs folder export const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { const filePath = path.join(config.docsPath, docPath) @@ -16,6 +17,7 @@ export const readMarkdownFile = (config: BuildConfig) => async (docPath: string) } } +// list all the docs in the docs folder export const readDocsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.docsPath, { type: 'files', @@ -26,6 +28,7 @@ export const readDocsFolder = (config: BuildConfig) => async () => { }) } +// checks if a folder exists, if not it will be created export const ensureDirectory = (config: BuildConfig) => async (dirPath: string): Promise => { @@ -36,6 +39,7 @@ export const ensureDirectory = } } +// write a file to the dist (output) folder export const writeDistFile = (config: BuildConfig) => async (filePath: string, contents: string) => { const ensureDir = ensureDirectory(config) const fullPath = path.join(config.distPath, filePath) @@ -43,7 +47,19 @@ export const writeDistFile = (config: BuildConfig) => async (filePath: string, c await fs.writeFile(fullPath, contents, { encoding: 'utf-8' }) } +// write a file to the dist (output) folder, inside the specified sdk folder export const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, contents: string) => { const writeFile = writeDistFile(config) await writeFile(path.join(sdk, filePath), contents) } + +// not exactly io, but used to parse the json using a result patten +export const parseJSON = (json: string) => { + try { + const output = JSON.parse(json) + + return [null, output as unknown] as const + } catch (error) { + return [new Error(`Failed to parse JSON`, { cause: error }), null] as const + } +} diff --git a/scripts/lib/manifest.ts b/scripts/lib/manifest.ts index 575681a043..fa2d3f27f3 100644 --- a/scripts/lib/manifest.ts +++ b/scripts/lib/manifest.ts @@ -1,11 +1,38 @@ +// parsing and traversing the manifest.json file +// main thing here is that the tree uses double arrays + import fs from 'node:fs/promises' import { z } from 'zod' import { fromError } from 'zod-validation-error' import type { BuildConfig } from './config' import { errorMessages } from './error-messages' +import { parseJSON } from './io' import { icon, sdk, tag, type Icon, type SDK, type Tag } from './schemas' -type ManifestItem = { +// read in the manifest + +export const readManifest = (config: BuildConfig) => async (): Promise => { + const { manifestSchema } = createManifestSchema(config) + const unsafe_manifest = await fs.readFile(config.manifestFilePath, { encoding: 'utf-8' }) + + const [error, json] = parseJSON(unsafe_manifest) + + if (error) { + throw new Error(errorMessages['manifest-parse-error'](error)) + } + + const manifest = await z.object({ navigation: manifestSchema }).safeParseAsync(json) + + if (manifest.success === true) { + return manifest.data.navigation + } + + throw new Error(errorMessages['manifest-parse-error'](fromError(manifest.error))) +} + +// verify the manifest is valid + +export type ManifestItem = { title: string href: string tag?: Tag @@ -64,34 +91,7 @@ const createManifestSchema = (config: BuildConfig) => { } } -const parseJSON = (json: string) => { - try { - const output = JSON.parse(json) - - return [null, output as unknown] as const - } catch (error) { - return [new Error(`Failed to parse JSON`, { cause: error }), null] as const - } -} - -export const readManifest = (config: BuildConfig) => async (): Promise => { - const { manifestSchema } = createManifestSchema(config) - const unsafe_manifest = await fs.readFile(config.manifestFilePath, { encoding: 'utf-8' }) - - const [error, json] = parseJSON(unsafe_manifest) - - if (error) { - throw new Error(errorMessages['manifest-parse-error'](error)) - } - - const manifest = await z.object({ navigation: manifestSchema }).safeParseAsync(json) - - if (manifest.success === true) { - return manifest.data.navigation - } - - throw new Error(errorMessages['manifest-parse-error'](fromError(manifest.error))) -} +// helper functions for traversing the manifest tree export type BlankTree }> = Array> diff --git a/scripts/lib/markdown.ts b/scripts/lib/markdown.ts index 2c86fca6e0..0cc95a4b10 100644 --- a/scripts/lib/markdown.ts +++ b/scripts/lib/markdown.ts @@ -1,3 +1,14 @@ +// responsible for reading in the markdown files and parsing them +// This is only for parsing in the main docs files, not the partials or typedocs +// - throws a warning if the doc is not in the manifest.json +// - throws a warning if the filename contains characters that will be encoded by the browser +// - extracts the frontmatter and validates it +// - title is required, will fail +// - description is required, will warn if missing +// - sdk is optional, but if present must be a valid sdk +// - validates (but does not embed) the partials and typedocs +// - extracts the headings and validates that they are unique + import { slugifyWithCounter } from '@sindresorhus/slugify' import { toString } from 'mdast-util-to-string' import { remark } from 'remark' diff --git a/scripts/lib/partials.ts b/scripts/lib/partials.ts index af95555d99..74d1e18a9c 100644 --- a/scripts/lib/partials.ts +++ b/scripts/lib/partials.ts @@ -1,3 +1,8 @@ +// responsible for reading in and parsing the partials markdown +// for validation see validators/checkPartials.ts +// for partials we currently do not allow them to embed other partials +// this also removes the .mdx suffix from the urls in the markdown + import path from 'node:path' import readdirp from 'readdirp' import { remark } from 'remark' diff --git a/scripts/lib/schemas.ts b/scripts/lib/schemas.ts index 55552253df..6de7348ad6 100644 --- a/scripts/lib/schemas.ts +++ b/scripts/lib/schemas.ts @@ -1,3 +1,5 @@ +// zod schemas for some of the basic types + import { z } from 'zod' import type { BuildConfig } from './config' diff --git a/scripts/lib/store.ts b/scripts/lib/store.ts index 559281e3c8..e0b058256e 100644 --- a/scripts/lib/store.ts +++ b/scripts/lib/store.ts @@ -1,3 +1,8 @@ +// only really needed when in dev mode +// if `build()` is run twice, this can store the important markdown files +// so that we don't have to read them from the file system again which is slow +// use the `invalidateFile()` function to remove a file from the store + import path from 'node:path' import type { BuildConfig } from './config' import { removeMdxSuffix } from './utils/removeMdxSuffix' diff --git a/scripts/lib/typedoc.ts b/scripts/lib/typedoc.ts index 8fcd9d6d1a..9424c2cf8c 100644 --- a/scripts/lib/typedoc.ts +++ b/scripts/lib/typedoc.ts @@ -1,3 +1,9 @@ +// responsible for reading in and parsing the typedoc markdown +// for validation see validators/checkTypedoc.ts +// this also removes the .mdx suffix from the urls in the markdown +// some of the typedoc files with not parse when using `remarkMdx` +// so we catch those errors and parse them the same but without `remarkMdx` + import path from 'node:path' import readdirp from 'readdirp' import { remark } from 'remark' diff --git a/scripts/lib/utils/documentHasIfComponents.ts b/scripts/lib/utils/documentHasIfComponents.ts index ee9da6c9c6..d89be3ff38 100644 --- a/scripts/lib/utils/documentHasIfComponents.ts +++ b/scripts/lib/utils/documentHasIfComponents.ts @@ -1,3 +1,5 @@ +// checks if a document has any components + import { Node } from 'unist' import { visit as mdastVisit } from 'unist-util-visit' import { findComponent } from './findComponent' diff --git a/scripts/lib/utils/extractComponentPropValueFromNode.ts b/scripts/lib/utils/extractComponentPropValueFromNode.ts index dcace2eee0..99432a0608 100644 --- a/scripts/lib/utils/extractComponentPropValueFromNode.ts +++ b/scripts/lib/utils/extractComponentPropValueFromNode.ts @@ -1,3 +1,7 @@ +// Given a component name (eg ) and a prop name (eg sdk) +// this function will extract and return the value of the prop +// note that this won't further parse the value + import type { VFile } from 'vfile' import type { BuildConfig } from '../config' import type { Node } from 'unist' diff --git a/scripts/lib/utils/extractHeadingFromHeadingNode.ts b/scripts/lib/utils/extractHeadingFromHeadingNode.ts index 9559eeb328..9c050f5d8a 100644 --- a/scripts/lib/utils/extractHeadingFromHeadingNode.ts +++ b/scripts/lib/utils/extractHeadingFromHeadingNode.ts @@ -1,3 +1,6 @@ +// We give authors the control to override the default heading id generated by slugify +// This function extracts the custom id from the heading node if it exists + import type { Node } from 'unist' export const extractHeadingFromHeadingNode = (node: Node) => { diff --git a/scripts/lib/utils/extractSDKsFromIfProp.ts b/scripts/lib/utils/extractSDKsFromIfProp.ts index d7b8cb9a3d..41139256ef 100644 --- a/scripts/lib/utils/extractSDKsFromIfProp.ts +++ b/scripts/lib/utils/extractSDKsFromIfProp.ts @@ -1,3 +1,7 @@ +// This function takes the value pulled out from +// `extractComponentPropValueFromNode()` and parses it in to +// an array of sdk keys + import { VFile } from 'vfile' import { BuildConfig } from '../config' import type { Node } from 'unist' diff --git a/scripts/lib/utils/findComponent.ts b/scripts/lib/utils/findComponent.ts index 3851400f34..a1bd241c3b 100644 --- a/scripts/lib/utils/findComponent.ts +++ b/scripts/lib/utils/findComponent.ts @@ -1,3 +1,5 @@ +// hunts a markdown tree for a specific component + import type { Node } from 'unist' export const findComponent = (node: Node, componentName: string) => { diff --git a/scripts/lib/utils/removeMdxSuffix.ts b/scripts/lib/utils/removeMdxSuffix.ts index 12386db082..36696bc896 100644 --- a/scripts/lib/utils/removeMdxSuffix.ts +++ b/scripts/lib/utils/removeMdxSuffix.ts @@ -1,3 +1,5 @@ +// removes the .mdx suffix from a file path if it exists + export const removeMdxSuffix = (filePath: string) => { if (filePath.includes('#')) { const [url, hash] = filePath.split('#') diff --git a/scripts/lib/utils/scopeHrefToSDK.ts b/scripts/lib/utils/scopeHrefToSDK.ts index 349e4366d4..39de6fd363 100644 --- a/scripts/lib/utils/scopeHrefToSDK.ts +++ b/scripts/lib/utils/scopeHrefToSDK.ts @@ -1,3 +1,5 @@ +// if a link contains the :sdk: token, it will be replaced with the targetSDK + import type { BuildConfig } from '../config' import type { SDK } from '../schemas' diff --git a/scripts/lib/validators/checkPartials.ts b/scripts/lib/validators/checkPartials.ts index fcde6d80e0..a1f82140c0 100644 --- a/scripts/lib/validators/checkPartials.ts +++ b/scripts/lib/validators/checkPartials.ts @@ -1,3 +1,9 @@ +// This validator manages the partials in the docs +// based on the options passed through it can +// - only report warnings if something ain't right +// - only embed the partials contents in to the markdown +// - both report warnings and embed the partials contents + import type { BuildConfig } from '../config' import type { Node } from 'unist' import type { VFile } from 'vfile' diff --git a/scripts/lib/validators/checkTypedoc.ts b/scripts/lib/validators/checkTypedoc.ts index 3e1a6c97b0..86eff45fa3 100644 --- a/scripts/lib/validators/checkTypedoc.ts +++ b/scripts/lib/validators/checkTypedoc.ts @@ -1,3 +1,10 @@ +// This validator manages the typedoc in the docs +// based on the options passed through it can +// - only report warnings if something ain't right +// - only embed the typedoc contents in to the markdown +// - both report warnings and embed the typedoc contents +// This validator will also ensure that the typedoc folder exists + import type { BuildConfig } from '../config' import type { Node } from 'unist' import type { VFile } from 'vfile' diff --git a/scripts/lib/validators/validateAndEmbedLinks.ts b/scripts/lib/validators/validateAndEmbedLinks.ts index e523d28593..213f50e18d 100644 --- a/scripts/lib/validators/validateAndEmbedLinks.ts +++ b/scripts/lib/validators/validateAndEmbedLinks.ts @@ -1,3 +1,9 @@ +// Validates +// - remove the mdx suffix from the url +// - check if the link is a valid link +// - check if the link is a link to a sdk scoped page +// - replace the link with the sdk link component if it is a link to a sdk scoped page + import { Node } from 'unist' import { map as mdastMap } from 'unist-util-map' import type { VFile } from 'vfile' @@ -8,7 +14,6 @@ import { DocsMap } from '../store' import { removeMdxSuffix } from '../utils/removeMdxSuffix' import { scopeHrefToSDK } from '../utils/scopeHrefToSDK' -// Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component export const validateAndEmbedLinks = (config: BuildConfig, docsMap: DocsMap, filePath: string, section: WarningsSection) => () => diff --git a/scripts/lib/validators/validateUniqueHeadings.ts b/scripts/lib/validators/validateUniqueHeadings.ts index 4f1fee1f91..463648540e 100644 --- a/scripts/lib/validators/validateUniqueHeadings.ts +++ b/scripts/lib/validators/validateUniqueHeadings.ts @@ -1,3 +1,5 @@ +// goes through the markdown tree and ensures that all the heading ids are unique + import { slugifyWithCounter } from '@sindresorhus/slugify' import { toString } from 'mdast-util-to-string' import { Node } from 'unist' From 5e7170d3059adf53a25cc214c56e35300eac1ede Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 24 Apr 2025 20:10:56 -0700 Subject: [PATCH 107/114] clean up caching functionality --- scripts/build-docs.ts | 68 ++++++++++++++++------------------------- scripts/lib/partials.ts | 19 ++---------- scripts/lib/store.ts | 55 +++++++++++++++++++++++++++------ scripts/lib/typedoc.ts | 19 ++---------- 4 files changed, 79 insertions(+), 82 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 794f9e05c9..ff65e4e2c9 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -46,7 +46,7 @@ import { flattenTree, ManifestGroup, readManifest, traverseTree, traverseTreeIte import { parseInMarkdownFile } from './lib/markdown' import { readPartialsFolder, readPartialsMarkdown } from './lib/partials' import { isValidSdk, VALID_SDKS, type SDK } from './lib/schemas' -import { createBlankStore, DocsMap, Store } from './lib/store' +import { createBlankStore, DocsMap, getMarkdownCache, Store } from './lib/store' import { readTypedocsFolder, readTypedocsMarkdown } from './lib/typedoc' import { documentHasIfComponents } from './lib/utils/documentHasIfComponents' import { extractComponentPropValueFromNode } from './lib/utils/extractComponentPropValueFromNode' @@ -149,6 +149,7 @@ export async function build(store: Store, config: BuildConfig) { const parseMarkdownFile = parseInMarkdownFile(config) const writeFile = writeDistFile(config) const writeSdkFile = writeSDKFile(config) + const markdownCache = getMarkdownCache(store) await ensureDir(config.distPath) @@ -158,11 +159,11 @@ export async function build(store: Store, config: BuildConfig) { const docsFiles = await getDocsFolder() console.info('✓ Read Docs Folder') - const cachedPartialsSize = store.partialsFiles.size + const cachedPartialsSize = store.partials.size const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) console.info(`✓ Loaded in ${partials.length} partials (${cachedPartialsSize} cached)`) - const cachedTypedocsSize = store.typedocsFiles.size + const cachedTypedocsSize = store.typedocs.size const typedocs = await getTypedocsMarkdown((await getTypedocsFolder()).map((item) => item.path)) console.info(`✓ Read ${typedocs.length} Typedocs (${cachedTypedocsSize} cached)`) @@ -183,28 +184,18 @@ export async function build(store: Store, config: BuildConfig) { }) console.info('✓ Parsed in Manifest') - const cachedDocsSize = store.markdownFiles.size + const cachedDocsSize = store.markdown.size // Read in all the docs const docsArray = await Promise.all( docsFiles.map(async (file) => { const href = removeMdxSuffix(`${config.baseDocsLink}${file.path}`) - const inManifest = docsInManifest.has(href) - let markdownFile: Awaited> - - const cachedMarkdownFile = store.markdownFiles.get(href) - - if (cachedMarkdownFile) { - markdownFile = structuredClone(cachedMarkdownFile) - } else { - markdownFile = await parseMarkdownFile(href, partials, typedocs, inManifest, 'docs') - - store.markdownFiles.set(href, structuredClone(markdownFile)) - } + const markdownFile = await markdownCache(href, () => + parseMarkdownFile(href, partials, typedocs, inManifest, 'docs'), + ) docsMap.set(href, markdownFile) - return markdownFile }), ) @@ -355,30 +346,26 @@ export async function build(store: Store, config: BuildConfig) { JSON.stringify({ navigation: await traverseTree( { items: sdkScopedManifest }, - async (item) => { - return { - title: item.title, - href: docsMap.get(item.href)?.sdk !== undefined ? scopeHrefToSDK(config)(item.href, ':sdk:') : item.href, - tag: item.tag, - wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, - icon: item.icon, - target: item.target, - sdk: item.sdk, - } - }, + async (item) => ({ + title: item.title, + href: docsMap.get(item.href)?.sdk !== undefined ? scopeHrefToSDK(config)(item.href, ':sdk:') : item.href, + tag: item.tag, + wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, + icon: item.icon, + target: item.target, + sdk: item.sdk, + }), // @ts-expect-error - This traverseTree function might just be the death of me - async (group) => { - return { - title: group.title, - collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, - tag: group.tag, - wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, - icon: group.icon, - hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, - sdk: group.sdk, - items: group.items, - } - }, + async (group) => ({ + title: group.title, + collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, + tag: group.tag, + wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, + icon: group.icon, + hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, + sdk: group.sdk, + items: group.items, + }), ), }), ) @@ -616,7 +603,6 @@ export async function build(store: Store, config: BuildConfig) { await writeFile( distFilePath, - // It's possible we will want to / need to put some frontmatter here `--- template: wide --- diff --git a/scripts/lib/partials.ts b/scripts/lib/partials.ts index 74d1e18a9c..fe6feebab4 100644 --- a/scripts/lib/partials.ts +++ b/scripts/lib/partials.ts @@ -16,7 +16,7 @@ import type { BuildConfig } from './config' import { errorMessages, safeFail } from './error-messages' import { readMarkdownFile } from './io' import { removeMdxSuffix } from './utils/removeMdxSuffix' -import type { Store } from './store' +import { getPartialsCache, type Store } from './store' export const readPartialsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.partialsPath, { @@ -102,20 +102,7 @@ export const readPartial = (config: BuildConfig) => async (filePath: string) => export const readPartialsMarkdown = (config: BuildConfig, store: Store) => async (paths: string[]) => { const read = readPartial(config) + const partialsCache = getPartialsCache(store) - return Promise.all( - paths.map(async (markdownPath) => { - const cachedValue = store.partialsFiles.get(markdownPath) - - if (cachedValue !== undefined) { - return cachedValue - } - - const partial = await read(markdownPath) - - store.partialsFiles.set(markdownPath, partial) - - return partial - }), - ) + return Promise.all(paths.map(async (markdownPath) => partialsCache(markdownPath, () => read(markdownPath)))) } diff --git a/scripts/lib/store.ts b/scripts/lib/store.ts index e0b058256e..ca44f23cd2 100644 --- a/scripts/lib/store.ts +++ b/scripts/lib/store.ts @@ -10,21 +10,58 @@ import type { readPartial } from './partials' import type { readTypedoc } from './typedoc' import type { parseInMarkdownFile } from './markdown' -export type DocsMap = Map>>> -export type PartialsMap = Map>>> -export type TypedocsMap = Map>>> +type MarkdownFile = Awaited>> +type PartialsFile = Awaited>> +type TypedocsFile = Awaited>> + +export type DocsMap = Map +export type PartialsMap = Map +export type TypedocsMap = Map export const createBlankStore = () => ({ - markdownFiles: new Map() as DocsMap, - partialsFiles: new Map() as PartialsMap, - typedocsFiles: new Map() as TypedocsMap, + markdown: new Map() as DocsMap, + partials: new Map() as PartialsMap, + typedocs: new Map() as TypedocsMap, }) export type Store = ReturnType export const invalidateFile = (store: ReturnType, config: BuildConfig) => (filePath: string) => { - store.markdownFiles.delete(removeMdxSuffix(`${config.baseDocsLink}${path.relative(config.docsPath, filePath)}`)) - store.partialsFiles.delete(path.relative(config.partialsPath, filePath)) - store.typedocsFiles.delete(path.relative(config.typedocPath, filePath)) + store.markdown.delete(removeMdxSuffix(`${config.baseDocsLink}${path.relative(config.docsPath, filePath)}`)) + store.partials.delete(path.relative(config.partialsPath, filePath)) + store.typedocs.delete(path.relative(config.typedocPath, filePath)) + } + +export const getMarkdownCache = (store: Store) => { + return async (key: string, cacheMiss: (key: string) => Promise) => { + const cached = store.markdown.get(key) + if (cached) return structuredClone(cached) + + const result = await cacheMiss(key) + store.markdown.set(key, structuredClone(result)) + return result + } +} + +export const getPartialsCache = (store: Store) => { + return async (key: string, cacheMiss: (key: string) => Promise) => { + const cached = store.partials.get(key) + if (cached) return structuredClone(cached) + + const result = await cacheMiss(key) + store.partials.set(key, structuredClone(result)) + return result + } +} + +export const getTypedocsCache = (store: Store) => { + return async (key: string, cacheMiss: (key: string) => Promise) => { + const cached = store.typedocs.get(key) + if (cached) return structuredClone(cached) + + const result = await cacheMiss(key) + store.typedocs.set(key, structuredClone(result)) + return result } +} diff --git a/scripts/lib/typedoc.ts b/scripts/lib/typedoc.ts index 9424c2cf8c..8cbdc2cd64 100644 --- a/scripts/lib/typedoc.ts +++ b/scripts/lib/typedoc.ts @@ -14,7 +14,7 @@ import type { BuildConfig } from './config' import { errorMessages } from './error-messages' import { readMarkdownFile } from './io' import { removeMdxSuffix } from './utils/removeMdxSuffix' -import type { Store } from './store' +import { getTypedocsCache, type Store } from './store' export const readTypedocsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.typedocPath, { @@ -112,20 +112,7 @@ export const readTypedoc = (config: BuildConfig) => async (filePath: string) => export const readTypedocsMarkdown = (config: BuildConfig, store: Store) => async (paths: string[]) => { const read = readTypedoc(config) + const typedocsCache = getTypedocsCache(store) - return Promise.all( - paths.map(async (filePath) => { - const cachedValue = store.typedocsFiles.get(filePath) - - if (cachedValue !== undefined) { - return cachedValue - } - - const typedoc = await read(filePath) - - store.typedocsFiles.set(filePath, typedoc) - - return typedoc - }), - ) + return Promise.all(paths.map(async (filePath) => typedocsCache(filePath, () => read(filePath)))) } From 74ba3c72282c0fe33fc81c2850dfc01392de276c Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 24 Apr 2025 20:15:29 -0700 Subject: [PATCH 108/114] clean up script messages --- scripts/build-docs.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index ff65e4e2c9..fbe3c14331 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -618,7 +618,7 @@ template: wide }), ) - console.info(`✓ Validated and wrote out all docs`) + console.info(`✓ Validated and wrote out all core docs`) const sdkSpecificVFiles = await Promise.all( config.validSdks.map(async (targetSdk) => { @@ -733,7 +733,11 @@ template: wide }), ) - console.info(`✓ Wrote out ${vFiles.filter(Boolean).length} ${targetSdk} specific docs`) + const numberOfSdkSpecificDocs = vFiles.filter(Boolean).length + + if (numberOfSdkSpecificDocs > 0) { + console.info(`✓ Wrote out ${numberOfSdkSpecificDocs} ${targetSdk} specific docs`) + } return { targetSdk, vFiles } }), From 961b20e11359cd035089d7e106070cdb42475998 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 24 Apr 2025 20:42:59 -0700 Subject: [PATCH 109/114] refactor out more of the remark plugins to be separate --- scripts/build-docs.ts | 226 +----------------- .../validators/filterOtherSDKsContentOut.ts | 31 +++ scripts/lib/validators/insertFrontmatter.ts | 18 ++ .../lib/validators/validateAndEmbedLinks.ts | 23 +- .../lib/validators/validateIfComponents.ts | 69 ++++++ 5 files changed, 141 insertions(+), 226 deletions(-) create mode 100644 scripts/lib/validators/filterOtherSDKsContentOut.ts create mode 100644 scripts/lib/validators/insertFrontmatter.ts create mode 100644 scripts/lib/validators/validateIfComponents.ts diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index fbe3c14331..207639b189 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -32,15 +32,11 @@ import remarkFrontmatter from 'remark-frontmatter' import remarkMdx from 'remark-mdx' import { Node } from 'unist' import { filter as mdastFilter } from 'unist-util-filter' -import { map as mdastMap } from 'unist-util-map' import { visit as mdastVisit } from 'unist-util-visit' -import type { VFile } from 'vfile' import reporter from 'vfile-reporter' -import yaml from 'yaml' -import { SDKLink } from './lib/components/SDKLink' import { createConfig, type BuildConfig } from './lib/config' import { watchAndRebuild } from './lib/dev' -import { errorMessages, safeFail, safeMessage, shouldIgnoreWarning } from './lib/error-messages' +import { errorMessages, shouldIgnoreWarning } from './lib/error-messages' import { ensureDirectory, readDocsFolder, writeDistFile, writeSDKFile } from './lib/io' import { flattenTree, ManifestGroup, readManifest, traverseTree, traverseTreeItemsFirst } from './lib/manifest' import { parseInMarkdownFile } from './lib/markdown' @@ -55,7 +51,10 @@ import { removeMdxSuffix } from './lib/utils/removeMdxSuffix' import { scopeHrefToSDK } from './lib/utils/scopeHrefToSDK' import { checkPartials } from './lib/validators/checkPartials' import { checkTypedoc } from './lib/validators/checkTypedoc' +import { filterOtherSDKsContentOut } from './lib/validators/filterOtherSDKsContentOut' +import { insertFrontmatter } from './lib/validators/insertFrontmatter' import { validateAndEmbedLinks } from './lib/validators/validateAndEmbedLinks' +import { validateIfComponents } from './lib/validators/validateIfComponents' import { validateUniqueHeadings } from './lib/validators/validateUniqueHeadings' // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts @@ -465,127 +464,8 @@ export async function build(store: Store, config: BuildConfig) { const vfile = await remark() .use(remarkFrontmatter) .use(remarkMdx) - // Validate links between docs are valid and replace the links to sdk scoped pages with the sdk link component - .use(() => (tree: Node, vfile: VFile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink) && !node.url.startsWith('#')) return node - if (!('children' in node)) return node - - // we are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - let [url, hash] = (node.url as string).split('#') - - if (url === '') { - // If the link is just a hash, then we need to link to the same doc - url = doc.href - } - - const ignore = config.ignoredLink(url) - if (ignore === true) return node - - const linkedDoc = docsMap.get(url) - - if (linkedDoc === undefined) { - safeMessage(config, vfile, filePath, 'docs', 'link-doc-not-found', [url], node.position) - return node - } - - if (hash !== undefined) { - const hasHash = linkedDoc.headingsHashes.has(hash) - - if (hasHash === false) { - safeMessage(config, vfile, filePath, 'docs', 'link-hash-not-found', [hash, url], node.position) - } - } - - if (linkedDoc.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience - - const firstChild = node.children?.[0] - const childIsCodeBlock = firstChild?.type === 'inlineCode' - - if (childIsCodeBlock) { - firstChild.type = 'text' - - return SDKLink({ - href: scopeHrefToSDK(config)(url, ':sdk:'), - sdks: linkedDoc.sdk, - code: true, - }) - } - - return SDKLink({ - href: scopeHrefToSDK(config)(url, ':sdk:'), - sdks: linkedDoc.sdk, - code: false, - children: node.children, - }) - } - - return node - }) - }) - // Validate the components - .use(() => (tree, vfile) => { - mdastVisit(tree, (node) => { - const sdk = extractComponentPropValueFromNode(config, node, vfile, 'If', 'sdk', false, 'docs', filePath) - - if (sdk === undefined) return - - const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk, 'docs', filePath) - - if (sdksFilter === undefined) return - - const manifestItems = flatSDKScopedManifest.filter((item) => item.href === doc.href) - - const availableSDKs = manifestItems.flatMap((item) => item.sdk).filter(Boolean) - - // The doc doesn't exist in the manifest so we are skipping it - if (manifestItems.length === 0) return - - sdksFilter.forEach((sdk) => { - ;(() => { - if (doc.sdk === undefined) return - - const available = doc.sdk.includes(sdk) - - if (available === false) { - safeFail( - config, - vfile, - filePath, - 'docs', - 'if-component-sdk-not-in-frontmatter', - [sdk, doc.sdk], - node.position, - ) - } - })() - ;(() => { - // The doc is generic so we are skipping it - if (availableSDKs.length === 0) return - - const available = availableSDKs.includes(sdk) - - if (available === false) { - safeFail( - config, - vfile, - filePath, - 'docs', - 'if-component-sdk-not-in-manifest', - [sdk, doc.href], - node.position, - ) - } - })() - }) - }) - }) + .use(validateAndEmbedLinks(config, docsMap, filePath, 'docs', doc)) + .use(validateIfComponents(config, filePath, doc, flatSDKScopedManifest)) .use(checkPartials(config, validatedPartials, filePath, { reportWarnings: false, embed: true })) .use(checkTypedoc(config, validatedTypedocs, filePath, { reportWarnings: false, embed: true })) .process(doc.vfile) @@ -631,97 +511,9 @@ template: wide const vfile = await remark() .use(remarkFrontmatter) .use(remarkMdx) - // filter out content that is only available to other sdk's - .use(() => (tree, vfile) => { - return mdastFilter(tree, (node) => { - // We aren't passing the vfile here as the as the warning - // should have already been reported above when we initially - // parsed the file - const sdk = extractComponentPropValueFromNode( - config, - node, - undefined, - 'If', - 'sdk', - true, - 'docs', - filePath, - ) - - if (sdk === undefined) return true - - const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk, 'docs', filePath) - - if (sdksFilter === undefined) return true - - if (sdksFilter.includes(targetSdk)) { - return true - } - - return false - }) - }) + .use(filterOtherSDKsContentOut(config, filePath, targetSdk)) .use(validateUniqueHeadings(config, filePath, 'docs')) - // scope urls so they point to the current sdk - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) { - safeFail( - config, - vfile, - filePath, - 'docs', - 'link-doc-not-found', - ['url property missing'], - node.position, - ) - return node - } - if (typeof node.url !== 'string') { - safeFail(config, vfile, filePath, 'docs', 'link-doc-not-found', ['url not a string'], node.position) - return node - } - if (!node.url.startsWith(config.baseDocsLink)) { - return node - } - - // we are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - const [url, hash] = (node.url as string).split('#') - - const ignore = config.ignoredLink(url) - if (ignore === true) return node - - const doc = docsMap.get(url) - - if (doc === undefined) { - safeFail(config, vfile, filePath, 'docs', 'link-doc-not-found', [url], node.position) - return node - } - - // we might need to do something here with doc - - return node - }) - }) - // Insert the canonical link into the doc frontmatter - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'yaml') return node - if (!('value' in node)) return node - if (typeof node.value !== 'string') return node - - const frontmatter = yaml.parse(node.value) - - frontmatter.canonical = doc.sdk ? scopeHrefToSDK(config)(doc.href, ':sdk:') : doc.href - - node.value = yaml.stringify(frontmatter).split('\n').slice(0, -1).join('\n') - - return node - }) - }) + .use(insertFrontmatter({ canonical: doc.sdk ? scopeHrefToSDK(config)(doc.href, ':sdk:') : doc.href })) .process({ ...doc.vfile, messages: [], // reset the messages, otherwise they will be duplicated @@ -764,7 +556,6 @@ template: wide sdks.forEach((sdk) => availableSDKs.add(sdk)) }) - // For each SDK, check heading uniqueness after filtering for (const sdk of availableSDKs) { await remark() .use(remarkFrontmatter) @@ -784,6 +575,7 @@ template: wide if (!sdkProp) return true const ifSdks = extractSDKsFromIfComponent(node, undefined, sdkProp, 'docs', filePath) + if (!ifSdks) return true return ifSdks.includes(sdk) diff --git a/scripts/lib/validators/filterOtherSDKsContentOut.ts b/scripts/lib/validators/filterOtherSDKsContentOut.ts new file mode 100644 index 0000000000..c4a436b366 --- /dev/null +++ b/scripts/lib/validators/filterOtherSDKsContentOut.ts @@ -0,0 +1,31 @@ +import { filter as mdastFilter } from 'unist-util-filter' +import { type BuildConfig } from '../config' +import { type SDK } from '../schemas' +import { extractComponentPropValueFromNode } from '../utils/extractComponentPropValueFromNode' +import { extractSDKsFromIfProp } from '../utils/extractSDKsFromIfProp' +import type { Node } from 'unist' +import type { VFile } from 'vfile' + +// filter out content that is only available to other sdk's + +export const filterOtherSDKsContentOut = + (config: BuildConfig, filePath: string, targetSdk: SDK) => () => (tree: Node, vfile: VFile) => { + return mdastFilter(tree, (node) => { + // We aren't passing the vfile here as the as the warning + // should have already been reported above when we initially + // parsed the file + const sdk = extractComponentPropValueFromNode(config, node, undefined, 'If', 'sdk', true, 'docs', filePath) + + if (sdk === undefined) return true + + const sdksFilter = extractSDKsFromIfProp(config)(node, undefined, sdk, 'docs', filePath) + + if (sdksFilter === undefined) return true + + if (sdksFilter.includes(targetSdk)) { + return true + } + + return false + }) + } diff --git a/scripts/lib/validators/insertFrontmatter.ts b/scripts/lib/validators/insertFrontmatter.ts new file mode 100644 index 0000000000..d7fd82ec43 --- /dev/null +++ b/scripts/lib/validators/insertFrontmatter.ts @@ -0,0 +1,18 @@ +import { map as mdastMap } from 'unist-util-map' +import yaml from 'yaml' + +export const insertFrontmatter = (newFrontmatter: Record) => () => (tree, vfile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'yaml') return node + if (!('value' in node)) return node + if (typeof node.value !== 'string') return node + + const frontmatter = yaml.parse(node.value) + + const transformedFrontmatter = { ...frontmatter, ...newFrontmatter } + + node.value = yaml.stringify(transformedFrontmatter).split('\n').slice(0, -1).join('\n') + + return node + }) +} diff --git a/scripts/lib/validators/validateAndEmbedLinks.ts b/scripts/lib/validators/validateAndEmbedLinks.ts index 213f50e18d..3fa2b0b43e 100644 --- a/scripts/lib/validators/validateAndEmbedLinks.ts +++ b/scripts/lib/validators/validateAndEmbedLinks.ts @@ -15,40 +15,45 @@ import { removeMdxSuffix } from '../utils/removeMdxSuffix' import { scopeHrefToSDK } from '../utils/scopeHrefToSDK' export const validateAndEmbedLinks = - (config: BuildConfig, docsMap: DocsMap, filePath: string, section: WarningsSection) => + (config: BuildConfig, docsMap: DocsMap, filePath: string, section: WarningsSection, doc?: { href: string }) => () => (tree: Node, vfile: VFile) => { return mdastMap(tree, (node) => { if (node.type !== 'link') return node if (!('url' in node)) return node if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink)) return node + if (!node.url.startsWith(config.baseDocsLink) && (!node.url.startsWith('#') || doc === undefined)) return node if (!('children' in node)) return node // we are overwriting the url with the mdx suffix removed node.url = removeMdxSuffix(node.url) - const [url, hash] = (node.url as string).split('#') + let [url, hash] = (node.url as string).split('#') + + if (url === '' && doc !== undefined) { + // If the link is just a hash, then we need to link to the same doc + url = doc.href + } const ignore = config.ignoredLink(url) if (ignore === true) return node - const doc = docsMap.get(url) + const linkedDoc = docsMap.get(url) - if (doc === undefined) { + if (linkedDoc === undefined) { safeMessage(config, vfile, filePath, section, 'link-doc-not-found', [url], node.position) return node } if (hash !== undefined) { - const hasHash = doc.headingsHashes.has(hash) + const hasHash = linkedDoc.headingsHashes.has(hash) if (hasHash === false) { safeMessage(config, vfile, filePath, section, 'link-hash-not-found', [hash, url], node.position) } } - if (doc.sdk !== undefined) { + if (linkedDoc.sdk !== undefined) { // we are going to swap it for the sdk link component to give the users a great experience const firstChild = node.children?.[0] @@ -59,14 +64,14 @@ export const validateAndEmbedLinks = return SDKLink({ href: scopeHrefToSDK(config)(url, ':sdk:'), - sdks: doc.sdk, + sdks: linkedDoc.sdk, code: true, }) } return SDKLink({ href: scopeHrefToSDK(config)(url, ':sdk:'), - sdks: doc.sdk, + sdks: linkedDoc.sdk, code: false, children: node.children, }) diff --git a/scripts/lib/validators/validateIfComponents.ts b/scripts/lib/validators/validateIfComponents.ts new file mode 100644 index 0000000000..511f575462 --- /dev/null +++ b/scripts/lib/validators/validateIfComponents.ts @@ -0,0 +1,69 @@ +import { Node } from 'unist' +import { visit as mdastVisit } from 'unist-util-visit' +import type { VFile } from 'vfile' +import { type BuildConfig } from '../config' +import { safeFail } from '../error-messages' +import { ManifestItem } from '../manifest' +import { type SDK } from '../schemas' +import { extractComponentPropValueFromNode } from '../utils/extractComponentPropValueFromNode' +import { extractSDKsFromIfProp } from '../utils/extractSDKsFromIfProp' + +export const validateIfComponents = + (config: BuildConfig, filePath: string, doc: { href: string; sdk?: SDK[] }, flatSDKScopedManifest: ManifestItem[]) => + () => + (tree: Node, vfile: VFile) => { + mdastVisit(tree, (node) => { + const sdk = extractComponentPropValueFromNode(config, node, vfile, 'If', 'sdk', false, 'docs', filePath) + + if (sdk === undefined) return + + const sdksFilter = extractSDKsFromIfProp(config)(node, vfile, sdk, 'docs', filePath) + + if (sdksFilter === undefined) return + + const manifestItems = flatSDKScopedManifest.filter((item) => item.href === doc.href) + + const availableSDKs = manifestItems.flatMap((item) => item.sdk).filter(Boolean) + + // The doc doesn't exist in the manifest so we are skipping it + if (manifestItems.length === 0) return + + sdksFilter.forEach((sdk) => { + ;(() => { + if (doc.sdk === undefined) return + + const available = doc.sdk.includes(sdk) + + if (available === false) { + safeFail( + config, + vfile, + filePath, + 'docs', + 'if-component-sdk-not-in-frontmatter', + [sdk, doc.sdk], + node.position, + ) + } + })() + ;(() => { + // The doc is generic so we are skipping it + if (availableSDKs.length === 0) return + + const available = availableSDKs.includes(sdk) + + if (available === false) { + safeFail( + config, + vfile, + filePath, + 'docs', + 'if-component-sdk-not-in-manifest', + [sdk, doc.href], + node.position, + ) + } + })() + }) + }) + } From 7561b71b75654a2a38412926c78cb8810c81ed15 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 24 Apr 2025 20:43:47 -0700 Subject: [PATCH 110/114] rename validators to plugins to better reflect there purpose --- scripts/build-docs.ts | 17 ++++++++++------- scripts/lib/markdown.ts | 4 ++-- .../{validators => plugins}/checkPartials.ts | 0 .../lib/{validators => plugins}/checkTypedoc.ts | 0 .../filterOtherSDKsContentOut.ts | 0 .../insertFrontmatter.ts | 0 .../validateAndEmbedLinks.ts | 0 .../validateIfComponents.ts | 0 .../validateUniqueHeadings.ts | 0 9 files changed, 12 insertions(+), 9 deletions(-) rename scripts/lib/{validators => plugins}/checkPartials.ts (100%) rename scripts/lib/{validators => plugins}/checkTypedoc.ts (100%) rename scripts/lib/{validators => plugins}/filterOtherSDKsContentOut.ts (100%) rename scripts/lib/{validators => plugins}/insertFrontmatter.ts (100%) rename scripts/lib/{validators => plugins}/validateAndEmbedLinks.ts (100%) rename scripts/lib/{validators => plugins}/validateIfComponents.ts (100%) rename scripts/lib/{validators => plugins}/validateUniqueHeadings.ts (100%) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 207639b189..dc018493dd 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -34,6 +34,7 @@ import { Node } from 'unist' import { filter as mdastFilter } from 'unist-util-filter' import { visit as mdastVisit } from 'unist-util-visit' import reporter from 'vfile-reporter' + import { createConfig, type BuildConfig } from './lib/config' import { watchAndRebuild } from './lib/dev' import { errorMessages, shouldIgnoreWarning } from './lib/error-messages' @@ -44,18 +45,20 @@ import { readPartialsFolder, readPartialsMarkdown } from './lib/partials' import { isValidSdk, VALID_SDKS, type SDK } from './lib/schemas' import { createBlankStore, DocsMap, getMarkdownCache, Store } from './lib/store' import { readTypedocsFolder, readTypedocsMarkdown } from './lib/typedoc' + import { documentHasIfComponents } from './lib/utils/documentHasIfComponents' import { extractComponentPropValueFromNode } from './lib/utils/extractComponentPropValueFromNode' import { extractSDKsFromIfProp } from './lib/utils/extractSDKsFromIfProp' import { removeMdxSuffix } from './lib/utils/removeMdxSuffix' import { scopeHrefToSDK } from './lib/utils/scopeHrefToSDK' -import { checkPartials } from './lib/validators/checkPartials' -import { checkTypedoc } from './lib/validators/checkTypedoc' -import { filterOtherSDKsContentOut } from './lib/validators/filterOtherSDKsContentOut' -import { insertFrontmatter } from './lib/validators/insertFrontmatter' -import { validateAndEmbedLinks } from './lib/validators/validateAndEmbedLinks' -import { validateIfComponents } from './lib/validators/validateIfComponents' -import { validateUniqueHeadings } from './lib/validators/validateUniqueHeadings' + +import { checkPartials } from './lib/plugins/checkPartials' +import { checkTypedoc } from './lib/plugins/checkTypedoc' +import { filterOtherSDKsContentOut } from './lib/plugins/filterOtherSDKsContentOut' +import { insertFrontmatter } from './lib/plugins/insertFrontmatter' +import { validateAndEmbedLinks } from './lib/plugins/validateAndEmbedLinks' +import { validateIfComponents } from './lib/plugins/validateIfComponents' +import { validateUniqueHeadings } from './lib/plugins/validateUniqueHeadings' // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts if (require.main === module) { diff --git a/scripts/lib/markdown.ts b/scripts/lib/markdown.ts index 0cc95a4b10..57eb6ab7b3 100644 --- a/scripts/lib/markdown.ts +++ b/scripts/lib/markdown.ts @@ -23,8 +23,8 @@ import { readMarkdownFile } from './io' import { isValidSdk, isValidSdks, type SDK } from './schemas' import { documentHasIfComponents } from './utils/documentHasIfComponents' import { extractHeadingFromHeadingNode } from './utils/extractHeadingFromHeadingNode' -import { checkPartials } from './validators/checkPartials' -import { checkTypedoc } from './validators/checkTypedoc' +import { checkPartials } from './plugins/checkPartials' +import { checkTypedoc } from './plugins/checkTypedoc' export const parseInMarkdownFile = (config: BuildConfig) => diff --git a/scripts/lib/validators/checkPartials.ts b/scripts/lib/plugins/checkPartials.ts similarity index 100% rename from scripts/lib/validators/checkPartials.ts rename to scripts/lib/plugins/checkPartials.ts diff --git a/scripts/lib/validators/checkTypedoc.ts b/scripts/lib/plugins/checkTypedoc.ts similarity index 100% rename from scripts/lib/validators/checkTypedoc.ts rename to scripts/lib/plugins/checkTypedoc.ts diff --git a/scripts/lib/validators/filterOtherSDKsContentOut.ts b/scripts/lib/plugins/filterOtherSDKsContentOut.ts similarity index 100% rename from scripts/lib/validators/filterOtherSDKsContentOut.ts rename to scripts/lib/plugins/filterOtherSDKsContentOut.ts diff --git a/scripts/lib/validators/insertFrontmatter.ts b/scripts/lib/plugins/insertFrontmatter.ts similarity index 100% rename from scripts/lib/validators/insertFrontmatter.ts rename to scripts/lib/plugins/insertFrontmatter.ts diff --git a/scripts/lib/validators/validateAndEmbedLinks.ts b/scripts/lib/plugins/validateAndEmbedLinks.ts similarity index 100% rename from scripts/lib/validators/validateAndEmbedLinks.ts rename to scripts/lib/plugins/validateAndEmbedLinks.ts diff --git a/scripts/lib/validators/validateIfComponents.ts b/scripts/lib/plugins/validateIfComponents.ts similarity index 100% rename from scripts/lib/validators/validateIfComponents.ts rename to scripts/lib/plugins/validateIfComponents.ts diff --git a/scripts/lib/validators/validateUniqueHeadings.ts b/scripts/lib/plugins/validateUniqueHeadings.ts similarity index 100% rename from scripts/lib/validators/validateUniqueHeadings.ts rename to scripts/lib/plugins/validateUniqueHeadings.ts From 0a8ae54f9d346893cb7aaef79656d5952cc7232a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 25 Apr 2025 10:43:03 -0700 Subject: [PATCH 111/114] pull out frontmatter extraction to its own plugin --- scripts/lib/markdown.ts | 68 +++----------------- scripts/lib/plugins/extractFrontmatter.ts | 77 +++++++++++++++++++++++ 2 files changed, 85 insertions(+), 60 deletions(-) create mode 100644 scripts/lib/plugins/extractFrontmatter.ts diff --git a/scripts/lib/markdown.ts b/scripts/lib/markdown.ts index 57eb6ab7b3..69a0ad4a74 100644 --- a/scripts/lib/markdown.ts +++ b/scripts/lib/markdown.ts @@ -16,15 +16,14 @@ import remarkFrontmatter from 'remark-frontmatter' import remarkMdx from 'remark-mdx' import { Node } from 'unist' import { visit as mdastVisit } from 'unist-util-visit' -import yaml from 'yaml' import { type BuildConfig } from './config' import { errorMessages, safeFail, safeMessage, type WarningsSection } from './error-messages' import { readMarkdownFile } from './io' -import { isValidSdk, isValidSdks, type SDK } from './schemas' -import { documentHasIfComponents } from './utils/documentHasIfComponents' -import { extractHeadingFromHeadingNode } from './utils/extractHeadingFromHeadingNode' import { checkPartials } from './plugins/checkPartials' import { checkTypedoc } from './plugins/checkTypedoc' +import { extractFrontmatter, type Frontmatter } from './plugins/extractFrontmatter' +import { documentHasIfComponents } from './utils/documentHasIfComponents' +import { extractHeadingFromHeadingNode } from './utils/extractHeadingFromHeadingNode' export const parseInMarkdownFile = (config: BuildConfig) => @@ -36,7 +35,6 @@ export const parseInMarkdownFile = section: WarningsSection, ) => { const readFile = readMarkdownFile(config) - const validateSDKs = isValidSdks(config) const [error, fileContent] = await readFile(`${href}.mdx`.replace(config.baseDocsLink, '')) if (error !== null) { @@ -45,12 +43,6 @@ export const parseInMarkdownFile = }) } - type Frontmatter = { - title: string - description?: string - sdk?: SDK[] - } - let frontmatter: Frontmatter | undefined = undefined const slugify = slugifyWithCounter() @@ -72,55 +64,11 @@ export const parseInMarkdownFile = safeFail(config, vfile, filePath, section, 'invalid-href-encoding', [href]) } }) - // validate and extract out the frontmatter - .use(() => (tree, vfile) => { - mdastVisit( - tree, - (node) => node.type === 'yaml' && 'value' in node, - (node) => { - if (!('value' in node)) return - if (typeof node.value !== 'string') return - - const frontmatterYaml: Record<'title' | 'description' | 'sdk', string | undefined> = yaml.parse(node.value) - - const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') - - if (frontmatterSDKs !== undefined && validateSDKs(frontmatterSDKs) === false) { - const invalidSDKs = frontmatterSDKs.filter((sdk) => isValidSdk(config)(sdk) === false) - safeFail( - config, - vfile, - filePath, - section, - 'invalid-sdk-in-frontmatter', - [invalidSDKs, config.validSdks as SDK[]], - node.position, - ) - return - } - - if (frontmatterYaml.title === undefined) { - safeFail(config, vfile, filePath, section, 'frontmatter-missing-title', [], node.position) - return - } - - if (frontmatterYaml.description === undefined) { - safeMessage(config, vfile, filePath, section, 'frontmatter-missing-description', [], node.position) - } - - frontmatter = { - title: frontmatterYaml.title, - description: frontmatterYaml.description, - sdk: frontmatterSDKs, - } - }, - ) - - if (frontmatter === undefined) { - safeFail(config, vfile, filePath, section, 'frontmatter-parse-failed', [href]) - return - } - }) + .use( + extractFrontmatter(config, href, filePath, section, (fm) => { + frontmatter = fm + }), + ) .use(checkPartials(config, partials, filePath, { reportWarnings: true, embed: false })) .use(checkTypedoc(config, typedocs, filePath, { reportWarnings: true, embed: false })) .process({ diff --git a/scripts/lib/plugins/extractFrontmatter.ts b/scripts/lib/plugins/extractFrontmatter.ts new file mode 100644 index 0000000000..bdbda330a6 --- /dev/null +++ b/scripts/lib/plugins/extractFrontmatter.ts @@ -0,0 +1,77 @@ +import type { Node } from 'unist' +import { visit as mdastVisit } from 'unist-util-visit' +import type { VFile } from 'vfile' +import yaml from 'yaml' +import { type BuildConfig } from '../config' +import { safeFail, safeMessage, WarningsSection } from '../error-messages' +import { isValidSdk, isValidSdks, type SDK } from '../schemas' + +export type Frontmatter = { + title: string + description?: string + sdk?: SDK[] +} + +export const extractFrontmatter = + ( + config: BuildConfig, + href: string, + filePath: string, + section: WarningsSection, + callback: (frontmatter: Frontmatter) => void, + ) => + () => + (tree: Node, vfile: VFile) => { + const validateSDKs = isValidSdks(config) + + let frontmatter: Frontmatter | undefined = undefined + + mdastVisit( + tree, + (node) => node.type === 'yaml' && 'value' in node, + (node) => { + if (!('value' in node)) return + if (typeof node.value !== 'string') return + + const frontmatterYaml: Record<'title' | 'description' | 'sdk', string | undefined> = yaml.parse(node.value) + + const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') + + if (frontmatterSDKs !== undefined && validateSDKs(frontmatterSDKs) === false) { + const invalidSDKs = frontmatterSDKs.filter((sdk) => isValidSdk(config)(sdk) === false) + safeFail( + config, + vfile, + filePath, + section, + 'invalid-sdk-in-frontmatter', + [invalidSDKs, config.validSdks as SDK[]], + node.position, + ) + return + } + + if (frontmatterYaml.title === undefined) { + safeFail(config, vfile, filePath, section, 'frontmatter-missing-title', [], node.position) + return + } + + if (frontmatterYaml.description === undefined) { + safeMessage(config, vfile, filePath, section, 'frontmatter-missing-description', [], node.position) + } + + frontmatter = { + title: frontmatterYaml.title, + description: frontmatterYaml.description, + sdk: frontmatterSDKs, + } + }, + ) + + if (frontmatter === undefined) { + safeFail(config, vfile, filePath, section, 'frontmatter-parse-failed', [href]) + return + } + + callback(frontmatter) + } From 6a1cf8df02499aa49875dfeaf265f55a48419486 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 25 Apr 2025 12:30:31 -0700 Subject: [PATCH 112/114] Remove all generation aspects of the script --- scripts/build-docs.test.ts | 2093 ++--------------- scripts/build-docs.ts | 146 +- scripts/lib/components/SDKLink.ts | 50 - scripts/lib/config.ts | 16 - scripts/lib/dev.ts | 47 - scripts/lib/io.ts | 30 +- scripts/lib/partials.ts | 23 +- scripts/lib/plugins/insertFrontmatter.ts | 18 - ...idateAndEmbedLinks.ts => validateLinks.ts} | 48 +- scripts/lib/store.ts | 58 +- scripts/lib/typedoc.ts | 35 +- 11 files changed, 189 insertions(+), 2375 deletions(-) delete mode 100644 scripts/lib/components/SDKLink.ts delete mode 100644 scripts/lib/dev.ts delete mode 100644 scripts/lib/plugins/insertFrontmatter.ts rename scripts/lib/plugins/{validateAndEmbedLinks.ts => validateLinks.ts} (55%) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index b6c96ff48a..590ffaf9e2 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -5,7 +5,6 @@ import { glob } from 'glob' import { describe, expect, onTestFinished, test } from 'vitest' import { build } from './build-docs' -import { createBlankStore, invalidateFile } from './lib/store' import { createConfig } from './lib/config' const tempConfig = { @@ -162,7 +161,6 @@ Testing with a simple page.`, ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -171,1311 +169,235 @@ Testing with a simple page.`, ) expect(output).toBe('') - - expect(await fileExists(pathJoin('./dist/simple-test.mdx'))).toBe(true) - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe(`--- -title: Simple Test -description: This is a simple test page ---- - -# Simple Test Page - -Testing with a simple page.`) - - expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) - expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }) - }) - - test('Warning on missing description in frontmatter', async () => { - // Create temp environment with minimal files array - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test ---- - -# Simple Test Page - -Testing with a simple page.`, - }, - ]) - - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['nextjs', 'react'], - }), - ) - - expect(output).toContain('warning Frontmatter should have a "description" property') - }) - - test('should ignore non-MDX files in the docs folder', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'MDX Doc', href: '/docs/mdx-doc' }]], - }), - }, - { - path: './docs/mdx-doc.mdx', - content: `--- -title: MDX Doc ---- - -# MDX Document`, - }, - { - path: './docs/non-mdx-file.txt', - content: `This is a text file, not an MDX file.`, - }, - { - path: './docs/image.png', - content: `fake image content`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // Verify only MDX files were processed - expect(await fileExists(pathJoin('./dist/mdx-doc.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/non-mdx-file.txt'))).toBe(false) - expect(await fileExists(pathJoin('./dist/image.png'))).toBe(false) - }) -}) - -describe('Manifest Validation', () => { - test('should fail build with completely malformed manifest JSON', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: '{invalid json structure', - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test ---- - -# Simple Test`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - await expect(promise).rejects.toThrow('Failed to parse manifest:') - }) - - test('should apply manifest options (wrapDefault, collapseDefault, hideTitleDefault) correctly', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'Group One', - items: [[{ title: 'Item One', href: '/docs/item-one' }]], - wrap: true, - collapse: true, - hideTitle: false, - }, - { - title: 'Group Two', - items: [[{ title: 'Item Two', href: '/docs/item-two' }]], - wrap: true, - collapse: false, - hideTitle: true, - }, - { - title: 'Group Three', - items: [[{ title: 'Item Three', href: '/docs/item-three' }]], - wrap: false, - collapse: true, - hideTitle: false, - }, - { - title: 'Group Four', - items: [[{ title: 'Item Four', href: '/docs/item-four' }]], - wrap: false, - collapse: false, - hideTitle: true, - }, - ], - ], - }), - }, - { path: './docs/item-one.mdx', content: `---\ntitle: Item One\n---\nItem One` }, - { path: './docs/item-two.mdx', content: `---\ntitle: Item Two\n---\nItem Two` }, - { path: './docs/item-three.mdx', content: `---\ntitle: Item Three\n---\nItem Three` }, - { path: './docs/item-four.mdx', content: `---\ntitle: Item Four\n---\nItem Four` }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['nextjs'], - manifestOptions: { - wrapDefault: false, - collapseDefault: false, - hideTitleDefault: false, - }, - }), - ) - - const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) - const groups = manifest.navigation[0] - - expect(groups[0].wrap).toBe(true) - expect(groups[0].collapse).toBe(true) - expect(groups[0].hideTitle).toBe(undefined) - - expect(groups[1].wrap).toBe(true) - expect(groups[1].collapse).toBe(undefined) - expect(groups[1].hideTitle).toBe(true) - - expect(groups[2].wrap).toBe(undefined) - expect(groups[2].collapse).toBe(true) - expect(groups[2].hideTitle).toBe(undefined) - - expect(groups[3].wrap).toBe(undefined) - expect(groups[3].collapse).toBe(undefined) - expect(groups[3].hideTitle).toBe(true) - }) - - test('should properly pass down SDK filtering from parent groups to child items', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'SDK Group', - sdk: ['nextjs', 'react'], - items: [ - [ - { - title: 'Sub Group', - items: [ - [ - { title: 'SDK Item', href: '/docs/sdk-item' }, - { title: 'Nested Group', items: [[{ title: 'Nested Item', href: '/docs/nested-item' }]] }, - ], - ], - }, - ], - ], - }, - { - title: 'Generic Group', - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], - }, - ], - ], - }, - { - title: 'Vue Group', - sdk: ['vue'], - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Vue Item', href: '/docs/vue-item' }]], - }, - ], - ], - }, - ], - ], - }), - }, - { - path: './docs/sdk-item.mdx', - content: `---\ntitle: SDK Item\n---\nSDK specific content`, - }, - { - path: './docs/nested-item.mdx', - content: `---\ntitle: Nested Item\n---\nNested SDK specific content`, - }, - { - path: './docs/generic-item.mdx', - content: `---\ntitle: Generic Item\n---\nGeneric content`, - }, - { - path: './docs/vue-item.mdx', - content: `---\ntitle: Vue Item\n---\nVue specific content`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['nextjs', 'react', 'vue'], - }), - ) - - // Check manifest - const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) - - expect(manifest).toEqual({ - navigation: [ - [ - { - title: 'SDK Group', - sdk: ['nextjs', 'react'], - items: [ - [ - { - title: 'Sub Group', - sdk: ['nextjs', 'react'], - items: [ - [ - { title: 'SDK Item', sdk: ['nextjs', 'react'], href: '/docs/sdk-item' }, - { - title: 'Nested Group', - sdk: ['nextjs', 'react'], - items: [[{ title: 'Nested Item', sdk: ['nextjs', 'react'], href: '/docs/nested-item' }]], - }, - ], - ], - }, - ], - ], - }, - { - title: 'Generic Group', - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], - }, - ], - ], - }, - { - title: 'Vue Group', - sdk: ['vue'], - items: [ - [ - { - title: 'Sub Group', - sdk: ['vue'], - items: [[{ title: 'Vue Item', sdk: ['vue'], href: '/docs/vue-item' }]], - }, - ], - ], - }, - ], - ], - }) - }) - - test('should properly inherit SDK filtering from child items up to parent groups', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'SDK Group', - items: [ - [ - { - title: 'Sub Group', - items: [ - [ - { title: 'SDK Item', href: '/docs/sdk-item' }, - { title: 'Nested Group', items: [[{ title: 'Nested Item', href: '/docs/nested-item' }]] }, - ], - ], - }, - ], - ], - }, - { - title: 'Generic Group', - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], - }, - ], - ], - }, - { - title: 'Vue Group', - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Vue Item', href: '/docs/vue-item' }]], - }, - ], - ], - }, - ], - ], - }), - }, - { - path: './docs/sdk-item.mdx', - content: `---\ntitle: SDK Item\nsdk: react\n---\nSDK specific content`, - }, - { - path: './docs/nested-item.mdx', - content: `---\ntitle: Nested Item\nsdk: nextjs\n---\nNested SDK specific content`, - }, - { - path: './docs/generic-item.mdx', - content: `---\ntitle: Generic Item\n---\nGeneric content`, - }, - { - path: './docs/vue-item.mdx', - content: `---\ntitle: Vue Item\nsdk: vue\n---\nVue specific content`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['nextjs', 'react', 'vue'], - }), - ) - - // Check manifest - const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) - - expect(manifest).toEqual({ - navigation: [ - [ - { - title: 'SDK Group', - sdk: ['react', 'nextjs'], - items: [ - [ - { - title: 'Sub Group', - sdk: ['react', 'nextjs'], - items: [ - [ - { title: 'SDK Item', sdk: ['react'], href: '/docs/:sdk:/sdk-item' }, - { - title: 'Nested Group', - sdk: ['nextjs'], - items: [[{ title: 'Nested Item', sdk: ['nextjs'], href: '/docs/:sdk:/nested-item' }]], - }, - ], - ], - }, - ], - ], - }, - { - title: 'Generic Group', - items: [ - [ - { - title: 'Sub Group', - items: [[{ title: 'Generic Item', href: '/docs/generic-item' }]], - }, - ], - ], - }, - { - title: 'Vue Group', - sdk: ['vue'], - items: [ - [ - { - title: 'Sub Group', - sdk: ['vue'], - items: [[{ title: 'Vue Item', sdk: ['vue'], href: '/docs/:sdk:/vue-item' }]], - }, - ], - ], - }, - ], - ], - }) - }) - - test('Check link and hash in partial is valid', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Page 1', href: '/docs/page-1' }, - { title: 'Page 2', href: '/docs/page-2' }, - ], - ], - }), - }, - { - path: './docs/page-1.mdx', - content: `--- -title: Page 1 ---- - -`, - }, - { - path: './docs/_partials/links.mdx', - content: `[Page 2](/docs/page-2#my-heading) -[Page 2](/docs/page-3)`, - }, - { - path: './docs/page-2.mdx', - content: `--- -title: Page 2 ---- - -test`, - }, - ]) - - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - expect(output).toContain(`warning Hash "my-heading" not found in /docs/page-2`) - expect(output).toContain(`warning Doc /docs/page-3 not found`) - }) - - test('should process target="_blank" links in manifest correctly', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Normal Link', href: '/docs/normal-link' }, - { title: 'External Link', href: 'https://example.com', target: '_blank' }, - ], - ], - }), - }, - { - path: './docs/normal-link.mdx', - content: `--- -title: Normal Link ---- - -# Normal Link - -This is a normal document.`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // Check that the manifest contains the target="_blank" attribute - const manifest = JSON.parse(await readFile(pathJoin('./dist/manifest.json'))) - expect(manifest).toEqual({ - navigation: [ - [ - { title: 'Normal Link', href: '/docs/normal-link' }, - { title: 'External Link', href: 'https://example.com', target: '_blank' }, - ], - ], - }) - }) -}) - -describe('SDK Processing', () => { - test('Two Docs, each grouped by a different SDK', async () => { - // Create temp environment with minimal files array - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'React', - sdk: ['react'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/react' }]], - }, - { - title: 'Vue', - sdk: ['vue'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue' }]], - }, - ], - ], - }), - }, - { - path: './docs/quickstart/react.mdx', - content: `--- -title: Quickstart ---- - -# React Quickstart`, - }, - { - path: './docs/quickstart/vue.mdx', - content: `--- -title: Quickstart ---- - -# Vue Quickstart`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'vue'], - }), - ) - - expect(await fileExists(pathJoin('./dist/manifest.json'))).toBe(true) - expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ - navigation: [ - [ - { - title: 'React', - sdk: ['react'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/react', sdk: ['react'] }]], - }, - { - title: 'Vue', - sdk: ['vue'], - items: [[{ title: 'Quickstart', href: '/docs/quickstart/vue', sdk: ['vue'] }]], - }, - ], - ], - }) - - const distFiles = await treeDir(pathJoin('./dist')) - - expect(distFiles.length).toBe(3) - expect(distFiles).toContain('manifest.json') - expect(distFiles).toContain('quickstart/vue.mdx') - expect(distFiles).toContain('quickstart/react.mdx') - }) - - test('sdk in frontmatter filters the docs', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react ---- - -# Simple Test Page - -Testing with a simple page.`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ - navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react'] }]], - }) - - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toBe(`--- -title: Simple Test -sdk: react -canonical: /docs/:sdk:/simple-test ---- - -# Simple Test Page - -Testing with a simple page.`) - - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toBe( - `---\ntemplate: wide\n---\n`, - ) - - const distFiles = await treeDir(pathJoin('./dist')) - - expect(distFiles.length).toBe(3) - expect(distFiles).toContain('simple-test.mdx') - expect(distFiles).toContain('manifest.json') - expect(distFiles).toContain('react/simple-test.mdx') - }) - - test('3 sdks in frontmatter generates 3 variants', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, vue, astro ---- - -# Simple Test Page - -Testing with a simple page.`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'vue', 'astro'], - }), - ) - - expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ - navigation: [[{ title: 'Simple Test', href: '/docs/:sdk:/simple-test', sdk: ['react', 'vue', 'astro'] }]], - }) - - const distFiles = await treeDir(pathJoin('./dist')) - - expect(distFiles.length).toBe(5) - expect(distFiles).toContain('simple-test.mdx') - expect(distFiles).toContain('manifest.json') - expect(distFiles).toContain('react/simple-test.mdx') - expect(distFiles).toContain('vue/simple-test.mdx') - expect(distFiles).toContain('astro/simple-test.mdx') - }) - - test(' content filtered out when sdk is in frontmatter', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, expo ---- - -# Simple Test Page - - - React Content - - -Testing with a simple page.`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'expo'], - }), - ) - - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('React Content') - - expect(await readFile(pathJoin('./dist/expo/simple-test.mdx'))).not.toContain('React Content') - }) - - test('Invalid SDK in frontmatter fails the build', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, expo, coffeescript ---- - -# Simple Test Page - -Testing with a simple page.`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'expo'], - }), - ) - - await expect(promise).rejects.toThrow(`Invalid SDK ["coffeescript"], the valid SDKs are ["react","expo"]`) - }) - - test('Invalid SDK in fails the build', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test -sdk: react, expo ---- - -# Simple Test Page - - - astro Content - - -Testing with a simple page.`, - }, - ]) - - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'expo'], - }), - ) - - expect(output).toContain(`warning sdk \"astro\" in is not a valid SDK`) - }) - - test('should fail when child SDK is not in parent SDK list', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'Authentication', - sdk: ['react'], - items: [ - [ - { - title: 'Login', - href: '/docs/auth/login', - sdk: ['react', 'python'], // python not in parent - }, - ], - ], - }, - ], - ], - }), - }, - { - path: './docs/auth/login.mdx', - content: `--- -title: Login -sdk: react, python ---- - -# Login Page - -Authentication login documentation.`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'python', 'nextjs'], - }), - ) - - await expect(promise).rejects.toThrow( - 'Doc "Login" is attempting to use ["react","python"] But its being filtered down to ["react"] in the manifest.json', - ) }) - test('should generate appropriate landing pages for SDK-specific docs', async () => { + test('Warning on missing description in frontmatter', async () => { + // Create temp environment with minimal files array const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'SDK Document', href: '/docs/sdk-document' }]], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/sdk-document.mdx', + path: './docs/simple-test.mdx', content: `--- -title: SDK Document -description: This document is available for React and Next.js. -sdk: react, nextjs +title: Simple Test --- -# SDK Document +# Simple Test Page -This document is available for React and Next.js.`, +Testing with a simple page.`, }, ]) - await build( - createBlankStore(), + const output = await build( createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs'], + validSdks: ['nextjs', 'react'], }), ) - // Check that SDK-specific versions were created - expect(await fileExists(pathJoin('./dist/react/sdk-document.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/nextjs/sdk-document.mdx'))).toBe(true) - - // Check that a landing page was created at the original URL - expect(await fileExists(pathJoin('./dist/sdk-document.mdx'))).toBe(true) - - // Verify landing page content - const landingPage = await readFile(pathJoin('./dist/sdk-document.mdx')) - expect(landingPage).toBe( - `---\ntemplate: wide\n---\n`, - ) + expect(output).toContain('warning Frontmatter should have a "description" property') }) +}) - test('should handle SDK filtering with deeply nested manifest structures', async () => { - const { tempDir, pathJoin } = await createTempFiles([ +describe('Manifest Validation', () => { + test('should fail build with completely malformed manifest JSON', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { - title: 'Top Level', - items: [ - [ - { - title: 'Mid Level', - sdk: ['react', 'nextjs'], - items: [ - [ - { - title: 'Deep Level', - sdk: ['nextjs'], - items: [[{ title: 'Deeply Nested Page', href: '/docs/deeply-nested-nextjs' }]], - }, - { - title: 'Deep Level', - sdk: ['react'], - items: [[{ title: 'Deeply Nested Page', href: '/docs/deeply-nested-react' }]], - }, - ], - ], - }, - ], - ], - }, - ], - ], - }), - }, - { - path: './docs/deeply-nested-nextjs.mdx', - content: `--- -title: Deeply Nested Page -sdk: nextjs ---- - -Content for Next.js users.`, + content: '{invalid json structure', }, { - path: './docs/deeply-nested-react.mdx', + path: './docs/simple-test.mdx', content: `--- -title: Deeply Nested Page -sdk: react +title: Simple Test --- -Content for React users.`, +# Simple Test`, }, ]) - await build( - createBlankStore(), + const promise = build( createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'js-frontend'], + validSdks: ['react'], }), ) - expect(JSON.parse(await readFile(pathJoin('./dist/manifest.json')))).toEqual({ - navigation: [ - [ - { - title: 'Top Level', - sdk: ['react', 'nextjs'], - items: [ - [ - { - title: 'Mid Level', - sdk: ['react', 'nextjs'], - items: [ - [ - { - title: 'Deep Level', - sdk: ['nextjs'], - items: [ - [ - { - href: '/docs/:sdk:/deeply-nested-nextjs', - sdk: ['nextjs'], - title: 'Deeply Nested Page', - }, - ], - ], - }, - { - title: 'Deep Level', - sdk: ['react'], - items: [ - [ - { - title: 'Deeply Nested Page', - sdk: ['react'], - href: '/docs/:sdk:/deeply-nested-react', - }, - ], - ], - }, - ], - ], - }, - ], - ], - }, - ], - ], - }) - - // Page should be available in nextjs (from manifest deep nesting) - expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/nextjs/deeply-nested-react.mdx'))).toBe(false) - expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).toContain('Content for Next.js users.') - expect(await readFile(pathJoin('./dist/nextjs/deeply-nested-nextjs.mdx'))).not.toContain('Content for React users.') - - // Page should be available in react (from parent manifest item) - expect(await fileExists(pathJoin('./dist/react/deeply-nested-react.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/react/deeply-nested-nextjs.mdx'))).toBe(false) - expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).toContain('Content for React users.') - expect(await readFile(pathJoin('./dist/react/deeply-nested-react.mdx'))).not.toContain('Content for Next.js users.') - - // Page should NOT be available in js-frontend (filtered out by manifest) - expect(await fileExists(pathJoin('./dist/js-frontend/deeply-nested-nextjs.mdx'))).toBe(false) - expect(await fileExists(pathJoin('./dist/js-frontend/deeply-nested-react.mdx'))).toBe(false) + await expect(promise).rejects.toThrow('Failed to parse manifest:') }) - test('should correctly process multiple blocks with different SDKs in a single document', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('Check link and hash in partial is valid', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [ [ - { - title: 'Multiple SDK Blocks', - href: '/multiple-sdk-blocks', - }, + { title: 'Page 1', href: '/docs/page-1' }, + { title: 'Page 2', href: '/docs/page-2' }, ], ], }), }, { - path: './docs/multiple-sdk-blocks.mdx', + path: './docs/page-1.mdx', content: `--- -title: Multiple SDK Blocks -sdk: react, nextjs, js-frontend +title: Page 1 --- -# Multiple SDK Blocks - - - This content is for React users only. - - - - This content is for Next.js users only. - - - - This content is for JavaScript Frontend users only. - +`, + }, + { + path: './docs/_partials/links.mdx', + content: `[Page 2](/docs/page-2#my-heading) +[Page 2](/docs/page-3)`, + }, + { + path: './docs/page-2.mdx', + content: `--- +title: Page 2 +--- -Common content for all SDKs.`, +test`, }, ]) - await build( - createBlankStore(), + const output = await build( createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'js-frontend'], + validSdks: ['react'], }), ) - // Check React version - expect(await fileExists(pathJoin('./dist/react/multiple-sdk-blocks.mdx'))).toBe(true) - const reactContent = await readFile(pathJoin('./dist/react/multiple-sdk-blocks.mdx')) - expect(reactContent).toContain('This content is for React users only.') - expect(reactContent).not.toContain('This content is for Next.js users only.') - expect(reactContent).not.toContain('This content is for JavaScript Frontend users only.') - expect(reactContent).toContain('Common content for all SDKs.') - - // Check Next.js version - expect(await fileExists(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx'))).toBe(true) - const nextjsContent = await readFile(pathJoin('./dist/nextjs/multiple-sdk-blocks.mdx')) - expect(nextjsContent).not.toContain('This content is for React users only.') - expect(nextjsContent).toContain('This content is for Next.js users only.') - expect(nextjsContent).not.toContain('This content is for JavaScript Frontend users only.') - expect(nextjsContent).toContain('Common content for all SDKs.') - - // Check JavaScript Frontend version - expect(await fileExists(pathJoin('./dist/js-frontend/multiple-sdk-blocks.mdx'))).toBe(true) - const jsContent = await readFile(pathJoin('./dist/js-frontend/multiple-sdk-blocks.mdx')) - expect(jsContent).not.toContain('This content is for React users only.') - expect(jsContent).not.toContain('This content is for Next.js users only.') - expect(jsContent).toContain('This content is for JavaScript Frontend users only.') - expect(jsContent).toContain('Common content for all SDKs.') + expect(output).toContain(`warning Hash "my-heading" not found in /docs/page-2`) + expect(output).toContain(`warning Doc /docs/page-3 not found`) }) +}) - test('should handle nested components correctly', async () => { +describe('SDK Processing', () => { + test('Invalid SDK in frontmatter fails the build', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { - title: 'Parent Group', - sdk: ['react', 'nextjs'], - items: [[{ title: 'Nested SDK Page', href: '/docs/nested-sdk-page' }]], - }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/nested-sdk-page.mdx', + path: './docs/simple-test.mdx', content: `--- -title: Nested SDK Page -sdk: react, nextjs +title: Simple Test +sdk: react, expo, coffeescript --- -# Nested SDK Filtering - - - This content is for React users. - - - This is nested content specifically for Next.js users who are also using React. - - +# Simple Test Page -Common content for all SDKs.`, +Testing with a simple page.`, }, ]) - await build( - createBlankStore(), + const promise = build( createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs'], + validSdks: ['react', 'expo'], }), ) - // Check React output has only React content - const reactOutput = await readFile(pathJoin('./dist/react/nested-sdk-page.mdx')) - expect(reactOutput).toContain('This content is for React users.') - expect(reactOutput).not.toContain('This is nested content specifically for Next.js users') - - // Check Next.js output has both React and Next.js content - const nextjsOutput = await readFile(pathJoin('./dist/nextjs/nested-sdk-page.mdx')) - expect(nextjsOutput).toContain('This content is for React users.') - expect(nextjsOutput).toContain('This is nested content specifically for Next.js users') + await expect(promise).rejects.toThrow(`Invalid SDK ["coffeescript"], the valid SDKs are ["react","expo"]`) }) - test('should support components with array syntax for multiple SDKs', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('Invalid SDK in fails the build', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [ - [ - { - title: 'Multiple SDK Test', - href: '/docs/multiple-sdk-test', - }, - ], - ], + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], }), }, { - path: './docs/multiple-sdk-test.mdx', + path: './docs/simple-test.mdx', content: `--- -title: Multiple SDK Test -sdk: react, nextjs, js-frontend +title: Simple Test +sdk: react, expo --- -# Multiple SDK Test - - - This content is for React and Next.js users. - +# Simple Test Page - - This content is for JavaScript Frontend users. + + astro Content -Common content for all SDKs.`, +Testing with a simple page.`, }, ]) - await build( - createBlankStore(), + const output = await build( createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs', 'js-frontend'], + validSdks: ['react', 'expo'], }), ) - // Check React output has React content but not JavaScript Frontend content - const reactOutput = await readFile(pathJoin('./dist/react/multiple-sdk-test.mdx')) - expect(reactOutput).toContain('This content is for React and Next.js users.') - expect(reactOutput).not.toContain('This content is for JavaScript Frontend users.') - - // Check Next.js output has Next.js content but not JavaScript Frontend content - const nextjsOutput = await readFile(pathJoin('./dist/nextjs/multiple-sdk-test.mdx')) - expect(nextjsOutput).toContain('This content is for React and Next.js users.') - expect(nextjsOutput).not.toContain('This content is for JavaScript Frontend users.') - - // Check JavaScript Frontend output has JavaScript Frontend content but not React/Next.js content - const jsOutput = await readFile(pathJoin('./dist/js-frontend/multiple-sdk-test.mdx')) - expect(jsOutput).toContain('This content is for JavaScript Frontend users.') - expect(jsOutput).not.toContain('This content is for React and Next.js users.') + expect(output).toContain(`warning sdk \"astro\" in is not a valid SDK`) }) - test('should embed canonical link in frontmatter', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('should fail when child SDK is not in parent SDK list', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [ [ { - title: 'Overview', - href: '/docs/overview', + title: 'Authentication', + sdk: ['react'], + items: [ + [ + { + title: 'Login', + href: '/docs/auth/login', + sdk: ['react', 'python'], // python not in parent + }, + ], + ], }, ], ], }), }, { - path: './docs/overview.mdx', + path: './docs/auth/login.mdx', content: `--- -title: Overview -sdk: fastify, expressjs +title: Login +sdk: react, python --- -# Hello World`, +# Login Page + +Authentication login documentation.`, }, ]) - await build( - createBlankStore(), + const promise = build( createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['fastify', 'expressjs'], + validSdks: ['react', 'python', 'nextjs'], }), ) - expect(await readFile(pathJoin('./dist/fastify/overview.mdx'))).toContain('canonical: /docs/:sdk:/overview') - expect(await readFile(pathJoin('./dist/expressjs/overview.mdx'))).toContain('canonical: /docs/:sdk:/overview') + await expect(promise).rejects.toThrow( + 'Doc "Login" is attempting to use ["react","python"] But its being filtered down to ["react"] in the manifest.json', + ) }) }) @@ -1503,7 +425,6 @@ title: Duplicate Headings ]) const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -1543,7 +464,6 @@ sdk: react, nextjs ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -1570,151 +490,69 @@ description: Quickstart page sdk: react, nextjs --- - - # Title {{ id: 'title' }} - - - - # Title {{ id: 'title' }} -`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'nextjs'], - }), - ) - - await expect(promise).rejects.toThrow( - 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', - ) - }) - - test('should error on duplicate headings if they are in different components but with the same sdk without sdk in frontmatter', async () => { - const { tempDir } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], - }), - }, - { - path: './docs/quickstart.mdx', - content: `--- -title: Quickstart -description: Quickstart page ---- - - - # Title {{ id: 'title' }} - - - - # Title {{ id: 'title' }} -`, - }, - ]) - - const promise = build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - await expect(promise).rejects.toThrow( - 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', - ) - }) -}) - -describe('Includes and Partials', () => { - test(' Component embeds content in to guide', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/_partials/test-partial.mdx', - content: `Test Partial Content`, - }, - { - path: './docs/simple-test.mdx', - content: `--- -title: Simple Test ---- - - + + # Title {{ id: 'title' }} + -# Simple Test Page`, + + # Title {{ id: 'title' }} +`, }, ]) - await build( - createBlankStore(), + const promise = build( createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react'], + validSdks: ['react', 'nextjs'], }), ) - expect(await readFile(pathJoin('./dist/simple-test.mdx'))).toContain('Test Partial Content') + await expect(promise).rejects.toThrow( + 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', + ) }) - test(' Component embeds content in to sdk scoped guide', async () => { - const { tempDir, pathJoin } = await createTempFiles([ + test('should error on duplicate headings if they are in different components but with the same sdk without sdk in frontmatter', async () => { + const { tempDir } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + navigation: [[{ title: 'Quickstart', href: '/docs/quickstart' }]], }), }, { - path: './docs/_partials/test-partial.mdx', - content: `Test Partial Content`, - }, - { - path: './docs/simple-test.mdx', + path: './docs/quickstart.mdx', content: `--- -title: Simple Test -sdk: react, nextjs +title: Quickstart +description: Quickstart page --- - + + # Title {{ id: 'title' }} + -# Simple Test Page`, + + # Title {{ id: 'title' }} +`, }, ]) - await build( - createBlankStore(), + const promise = build( createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs'], + validSdks: ['react'], }), ) - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).toContain('Test Partial Content') - expect(await readFile(pathJoin('./dist/react/simple-test.mdx'))).not.toContain( - '', - ) - expect(await readFile(pathJoin('./dist/nextjs/simple-test.mdx'))).toContain('Test Partial Content') - expect(await readFile(pathJoin('./dist/nextjs/simple-test.mdx'))).not.toContain( - '', + await expect(promise).rejects.toThrow( + 'Doc "/docs/quickstart.mdx" contains a duplicate heading id "title", please ensure all heading ids are unique', ) }) +}) +describe('Includes and Partials', () => { test('Invalid partial src fails the build', async () => { const { tempDir } = await createTempFiles([ { @@ -1736,7 +574,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -1776,7 +613,6 @@ title: Simple Test ]) const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -1808,7 +644,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -1855,7 +690,6 @@ description: This is a test page ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -1890,7 +724,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -1930,7 +763,6 @@ title: Core Page ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -1962,7 +794,6 @@ title: Simple Test ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -2005,341 +836,73 @@ title: Simple Test ]) const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) - }) - - test('Swap out links for when a link points to an sdk generated guide', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'SDK Filtered Page', href: '/docs/sdk-filtered-page' }, - { title: 'Core Page', href: '/docs/core-page' }, - ], - ], - }), - }, - { - path: './docs/sdk-filtered-page.mdx', - content: `--- -title: SDK Filtered Page -sdk: react, nextjs ---- - -SDK filtered page`, - }, - { - path: './docs/core-page.mdx', - content: `--- -title: Core Page ---- - -# Core page - -[SDK Filtered Page](/docs/sdk-filtered-page) -`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'nextjs'], - }), - ) - - expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain( - `SDK Filtered Page`, - ) - }) - - test('Should swap out links for in partials', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'SDK Filtered Page', href: '/docs/sdk-filtered-page' }, - { title: 'Core Page', href: '/docs/core-page' }, - ], - ], - }), - }, - { - path: './docs/sdk-filtered-page.mdx', - content: `--- -title: SDK Filtered Page -sdk: react, nextjs ---- - -SDK filtered page`, - }, - { - path: './docs/_partials/links.mdx', - content: `[SDK Filtered Page](/docs/sdk-filtered-page)`, - }, - { - path: './docs/core-page.mdx', - content: `--- -title: Core Page ---- - -# Core page - - -`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'nextjs'], - }), - ) - - expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain( - `SDK Filtered Page`, - ) - }) - - test('Should swap out links for inside a component', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'SDK Filtered Page', href: '/docs/sdk-filtered-page' }, - { title: 'Core Page', href: '/docs/core-page' }, - ], - ], - }), - }, - { - path: './docs/sdk-filtered-page.mdx', - content: `--- -title: SDK Filtered Page -sdk: react, nextjs ---- - -SDK filtered page`, - }, - { - path: './docs/core-page.mdx', - content: `--- -title: Core Page ---- - -# Core page - - - - afterMultiSessionSingleSignOutUrl - - string - - go use [SDK Filtered Page](/docs/sdk-filtered-page) - -`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react', 'nextjs'], - }), - ) - - expect(await readFile(pathJoin('./dist/core-page.mdx'))).toContain( - `SDK Filtered Page`, - ) - }) - - test('should correctly handle links with anchors to specific sections of documents', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Source Document', href: '/docs/source-document' }, - { title: 'Target Document', href: '/docs/target-document' }, - ], - ], - }), - }, - { - path: './docs/source-document.mdx', - content: `--- -title: Source Document ---- - -# Source Document - -[Link to Section 1](/docs/target-document#section-1) -[Link to Section 2](/docs/target-document#section-2) -[Link to Invalid Section](/docs/target-document#invalid-section)`, - }, - { - path: './docs/target-document.mdx', - content: `--- -title: Target Document ---- - -# Target Document - -## Section 1 - -Content for section 1. - -## Section 2 - -Content for section 2.`, - }, - ]) - - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // Valid links should work without warnings - expect(output).not.toContain('warning Hash "section-1" not found') - expect(output).not.toContain('warning Hash "section-2" not found') - - // Invalid link should produce a warning - expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document') - }) - - test('if the contents of a link starts with a ` and ends with a ` it should inject the code prop', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Link with code', href: '/docs/link-with-code' }, - { title: 'Sign In', href: '/docs/components/sign-in' }, - ], - ], - }), - }, - { - path: './docs/components/sign-in.mdx', - content: `--- -title: Sign In -description: Sign In component -sdk: react, nextjs ---- - -\`\`\`js -const x = 'y' -\`\`\` -`, - }, - { - path: './docs/link-with-code.mdx', - content: `--- -title: Link with code -description: Link with code ---- -- [\`\`](/docs/components/sign-in) -`, - }, - ]) - - const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs'], + validSdks: ['react'], }), ) - expect(output).toBe('') - - expect(await readFile(pathJoin('./dist/link-with-code.mdx'))).toContain( - `\\`, - ) + expect(output).not.toContain(`warning Hash "my-heading" not found in /docs/headings`) }) - test('if the contents of a link starts with a ` and ends with a ` it should inject the code prop (when in a partial)', async () => { + test('should correctly handle links with anchors to specific sections of documents', async () => { const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ navigation: [ [ - { title: 'Link with code', href: '/docs/link-with-code' }, - { title: 'Sign In', href: '/docs/components/sign-in' }, + { title: 'Source Document', href: '/docs/source-document' }, + { title: 'Target Document', href: '/docs/target-document' }, ], ], }), }, { - path: './docs/components/sign-in.mdx', + path: './docs/source-document.mdx', content: `--- -title: Sign In -description: Sign In component -sdk: react, nextjs +title: Source Document --- -\`\`\`js -const x = 'y' -\`\`\` -`, - }, - { - path: './docs/_partials/links.mdx', - content: `[\`\`](/docs/components/sign-in)`, +# Source Document + +[Link to Section 1](/docs/target-document#section-1) +[Link to Section 2](/docs/target-document#section-2) +[Link to Invalid Section](/docs/target-document#invalid-section)`, }, { - path: './docs/link-with-code.mdx', + path: './docs/target-document.mdx', content: `--- -title: Link with code -description: Link with code +title: Target Document --- -- -`, + +# Target Document + +## Section 1 + +Content for section 1. + +## Section 2 + +Content for section 2.`, }, ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, - validSdks: ['react', 'nextjs'], + validSdks: ['react'], }), ) - expect(output).toBe('') + // Valid links should work without warnings + expect(output).not.toContain('warning Hash "section-1" not found') + expect(output).not.toContain('warning Hash "section-2" not found') - expect(await readFile(pathJoin('./dist/link-with-code.mdx'))).toContain( - `\\`, - ) + // Invalid link should produce a warning + expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document') }) test('Links with only a hash to the same page are valid', async () => { @@ -2364,8 +927,6 @@ description: This is a test page ]) const output = await build( - createBlankStore(), - createConfig({ ...baseConfig, basePath: tempDir, @@ -2396,8 +957,6 @@ description: This is a test page ]) const output = await build( - createBlankStore(), - createConfig({ ...baseConfig, basePath: tempDir, @@ -2450,7 +1009,6 @@ sdk: react ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -2482,7 +1040,6 @@ title: React Doc // This should throw an error because the file path starts with an SDK name const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -2494,173 +1051,6 @@ title: React Doc 'Doc "/docs/react/conflict" is attempting to write out a doc to react/conflict.mdx but the first part of the path is a valid SDK, this causes a file path conflict.', ) }) - - test('should remove .mdx suffix from links in standard pages', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Target Page', href: '/docs/target-page' }, - { title: 'Standard Page', href: '/docs/standard-page' }, - ], - ], - }), - }, - { - path: './docs/target-page.mdx', - content: `--- -title: Target Page ---- - -# Target Page Content`, - }, - { - path: './docs/standard-page.mdx', - content: `--- -title: Standard Page ---- - -# Standard Page - -[Link to Target with .mdx](/docs/target-page.mdx) -[Link to Target without .mdx](/docs/target-page) -[Link to Target with hash](/docs/target-page#target-page-content) -[Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // links should be processed to remove .mdx - const standardPageContent = await readFile(pathJoin('./dist/standard-page.mdx')) - expect(standardPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') - expect(standardPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') - expect(standardPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') - expect(standardPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') - expect(standardPageContent).not.toContain('/docs/target-page.mdx') - }) - - test('should remove .mdx suffix from links in pages with partials', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Target Page', href: '/docs/target-page' }, - { title: 'Partials Page', href: '/docs/partials-page' }, - ], - ], - }), - }, - { - path: './docs/target-page.mdx', - content: `--- -title: Target Page ---- - -# Target Page Content`, - }, - { - path: './docs/_partials/links.mdx', - content: `[Link to Target with .mdx](/docs/target-page.mdx) -[Link to Target without .mdx](/docs/target-page) -[Link to Target with hash](/docs/target-page#target-page-content) -[Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, - }, - { - path: './docs/partials-page.mdx', - content: `--- -title: Partials Page ---- - -`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // Partials should be processed to remove .mdx - const partialsPageContent = await readFile(pathJoin('./dist/partials-page.mdx')) - expect(partialsPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') - expect(partialsPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') - expect(partialsPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') - expect(partialsPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') - expect(partialsPageContent).not.toContain('/docs/target-page.mdx') - }) - - test('should remove .mdx suffix from links in scoped pages', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Target Page', href: '/docs/target-page' }, - { title: 'Scoped Page', href: '/docs/scoped-page' }, - ], - ], - }), - }, - { - path: './docs/target-page.mdx', - content: `--- -title: Target Page ---- - -# Target Page Content`, - }, - { - path: './docs/_partials/links.mdx', - content: `[Link to Target with .mdx](/docs/target-page.mdx) -[Link to Target without .mdx](/docs/target-page) -[Link to Target with hash](/docs/target-page#target-page-content) -[Link to Target with hash and .mdx](/docs/target-page.mdx#target-page-content)`, - }, - { - path: './docs/scoped-page.mdx', - content: `--- -title: Scoped Page -sdk: expo ---- - -`, - }, - ]) - - await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['expo'], - }), - ) - - // Scoped page should be processed to remove .mdx - const scopedPageContent = await readFile(pathJoin('./dist/expo/scoped-page.mdx')) - expect(scopedPageContent).toContain('[Link to Target with .mdx](/docs/target-page)') - expect(scopedPageContent).toContain('[Link to Target without .mdx](/docs/target-page)') - expect(scopedPageContent).toContain('[Link to Target with hash](/docs/target-page#target-page-content)') - expect(scopedPageContent).toContain('[Link to Target with hash and .mdx](/docs/target-page#target-page-content)') - expect(scopedPageContent).not.toContain('/docs/target-page.mdx') - }) }) describe('Edge Cases', () => { @@ -2685,7 +1075,6 @@ description: \`This frontmatter has an unbalanced quote // This should throw a parsing error const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -2716,7 +1105,6 @@ description: This frontmatter is missing the required title field // This should throw an error about missing title const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -2742,7 +1130,6 @@ description: This frontmatter is missing the required title field ]) const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -2780,7 +1167,6 @@ This page has an invalid SDK in frontmatter.`, // This should throw an error with specific message about invalid SDK const promise = build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -2828,7 +1214,6 @@ This document doesn't have the referenced header.`, // Should complete with warnings const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -2839,123 +1224,6 @@ This document doesn't have the referenced header.`, // Should report warning about missing hash expect(output).toContain('warning Hash "non-existent-header" not found in /docs/invalid-reference') }) - - test('should complete build workflow when errors are present in some files', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [ - [ - { title: 'Valid Document', href: '/docs/valid-document' }, - { title: 'Document with Warnings', href: '/docs/document-with-warnings' }, - ], - ], - }), - }, - { - path: './docs/valid-document.mdx', - content: `--- -title: Valid Document ---- - -# Valid Document - -This is a completely valid document.`, - }, - { - path: './docs/document-with-warnings.mdx', - content: `--- -title: Document with Warnings ---- - -# Document with Warnings - -[Broken Link](/docs/non-existent-document) - - - This content has an invalid SDK. -`, - }, - ]) - - // Should complete with warnings - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - // Check that the build completed and valid files were created - expect(await fileExists(pathJoin('./dist/valid-document.mdx'))).toBe(true) - expect(await fileExists(pathJoin('./dist/document-with-warnings.mdx'))).toBe(true) - - // Check that warnings were reported - expect(output).toContain('warning Doc /docs/non-existent-document not found') - expect(output).toContain('warning sdk "invalid-sdk" in is not a valid SDK') - }) -}) - -describe('Cache Handling', () => { - test('should update cached files when their content changes', async () => { - const { tempDir, pathJoin } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Cached Doc', href: '/docs/cached-doc' }]], - }), - }, - { - path: './docs/cached-doc.mdx', - content: `--- -title: Original Title ---- - -# Original Content`, - }, - ]) - - // Create store to maintain cache across builds - const store = createBlankStore() - const config = createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }) - const invalidate = invalidateFile(store, config) - - // First build - await build(store, config) - - // Check initial content - const initialContent = await readFile(pathJoin('./dist/cached-doc.mdx')) - expect(initialContent).toContain('Original Title') - expect(initialContent).toContain('Original Content') - - // Update file content - await fs.writeFile( - pathJoin('./docs/cached-doc.mdx'), - `--- -title: Updated Title ---- - -# Updated Content`, - 'utf-8', - ) - - invalidate(pathJoin('./docs/cached-doc.mdx')) - - // Second build with same store (should detect changes) - await build(store, config) - - // Check updated content - const updatedContent = await readFile(pathJoin('./dist/cached-doc.mdx')) - expect(updatedContent).toContain('Updated Title') - expect(updatedContent).toContain('Updated Content') - }) }) describe('Configuration Options', () => { @@ -2980,7 +1248,6 @@ description: This page has a description ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3028,7 +1295,6 @@ description: This page has a description ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3079,7 +1345,6 @@ description: This page has a description // Should complete without the ignored warnings const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3134,7 +1399,6 @@ description: This page has a description // Only ignore the link warning, but leave SDK warning const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3183,7 +1447,6 @@ description: This page has a description // Ignore component attribute warnings const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3228,7 +1491,6 @@ title: Missing Description ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3284,7 +1546,6 @@ description: The page being linked to // Ignore hash warnings const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3342,7 +1603,6 @@ description: This page has a description ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3387,7 +1647,6 @@ description: Test page with partial // Ignore link warnings in partials const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3445,7 +1704,6 @@ interface Client { ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3492,7 +1750,6 @@ interface Client { ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3536,7 +1793,7 @@ description: Generated API docs }) // Should fail due to missing typedoc folder - const promise = build(createBlankStore(), configWithMissingFolder) + const promise = build(configWithMissingFolder) await expect(promise).rejects.toThrow('Typedoc folder') }) @@ -3575,7 +1832,6 @@ interface Client { ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3650,7 +1906,6 @@ interface Client { ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3692,7 +1947,6 @@ description: Generated API docs ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3731,7 +1985,6 @@ description: Generated API docs ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3785,7 +2038,6 @@ description: Generated API docs ]) const output = await build( - createBlankStore(), createConfig({ ...baseConfig, basePath: tempDir, @@ -3795,43 +2047,4 @@ description: Generated API docs expect(output).toContain('Hash "non-existent-hash" not found in /docs/overview') }) - - test('should embed typedoc into the doc', async () => { - const { tempDir, readFile } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'API Doc', href: '/docs/api-doc' }]], - }), - }, - { - path: './typedoc/api/client.mdx', - content: `# Client API`, - }, - { - path: './docs/api-doc.mdx', - content: `--- -title: API Documentation -description: Generated API docs ---- - -# API Documentation - - -`, - }, - ]) - - const output = await build( - createBlankStore(), - createConfig({ - ...baseConfig, - basePath: tempDir, - validSdks: ['react'], - }), - ) - - expect(await readFile('./dist/api-doc.mdx')).toContain('Client API') - expect(output).toBe('') - }) }) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index dc018493dd..a6a9a43a89 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -14,18 +14,6 @@ // - File existence for both docs and partials // - Path conflicts (prevents SDK name conflicts in paths) -// Transforms -// - Embeds the partials in the markdown files -// - Updates the links in the content if they point to the sdk specific docs -// - Converts links to SDK-specific docs to use components -// - Copies over "core" docs to the dist folder -// - Generates "landing" pages for the sdk specific docs at the original url -// - Generates out the sdk specific docs to their respective folders -// - Stripping filtered out content based on SDK -// - Removes .mdx from the end of docs markdown links -// - Adds canonical links in frontmatter for SDK-specific docs - -import fs from 'node:fs/promises' import path from 'node:path' import { remark } from 'remark' import remarkFrontmatter from 'remark-frontmatter' @@ -36,28 +24,23 @@ import { visit as mdastVisit } from 'unist-util-visit' import reporter from 'vfile-reporter' import { createConfig, type BuildConfig } from './lib/config' -import { watchAndRebuild } from './lib/dev' import { errorMessages, shouldIgnoreWarning } from './lib/error-messages' -import { ensureDirectory, readDocsFolder, writeDistFile, writeSDKFile } from './lib/io' +import { readDocsFolder } from './lib/io' import { flattenTree, ManifestGroup, readManifest, traverseTree, traverseTreeItemsFirst } from './lib/manifest' import { parseInMarkdownFile } from './lib/markdown' import { readPartialsFolder, readPartialsMarkdown } from './lib/partials' import { isValidSdk, VALID_SDKS, type SDK } from './lib/schemas' -import { createBlankStore, DocsMap, getMarkdownCache, Store } from './lib/store' +import { DocsMap } from './lib/store' import { readTypedocsFolder, readTypedocsMarkdown } from './lib/typedoc' import { documentHasIfComponents } from './lib/utils/documentHasIfComponents' import { extractComponentPropValueFromNode } from './lib/utils/extractComponentPropValueFromNode' import { extractSDKsFromIfProp } from './lib/utils/extractSDKsFromIfProp' import { removeMdxSuffix } from './lib/utils/removeMdxSuffix' -import { scopeHrefToSDK } from './lib/utils/scopeHrefToSDK' -import { checkPartials } from './lib/plugins/checkPartials' -import { checkTypedoc } from './lib/plugins/checkTypedoc' import { filterOtherSDKsContentOut } from './lib/plugins/filterOtherSDKsContentOut' -import { insertFrontmatter } from './lib/plugins/insertFrontmatter' -import { validateAndEmbedLinks } from './lib/plugins/validateAndEmbedLinks' import { validateIfComponents } from './lib/plugins/validateIfComponents' +import { validateLinks } from './lib/plugins/validateLinks' import { validateUniqueHeadings } from './lib/plugins/validateUniqueHeadings' // Only invokes the main function if we run the script directly eg npm run build, bun run ./scripts/build-docs.ts @@ -66,15 +49,12 @@ if (require.main === module) { } async function main() { - const args = process.argv.slice(2) - const config = createConfig({ basePath: __dirname, docsPath: '../docs', baseDocsLink: '/docs/', manifestPath: '../docs/manifest.json', partialsPath: '../docs/_partials', - distPath: '../dist', typedocPath: '../clerk-typedoc', ignoreLinks: [ '/docs/core-1', @@ -111,49 +91,25 @@ async function main() { collapseDefault: false, hideTitleDefault: false, }, - cleanDist: false, - flags: { - watch: args.includes('--watch'), - controlled: args.includes('--controlled'), - }, }) - const store = createBlankStore() - - const output = await build(store, config) - - if (config.flags.controlled) { - console.info('---initial-build-complete---') - } + const output = await build(config) if (output !== '') { console.info(output) - } - - if (config.flags.watch) { - console.info(`Watching for changes...`) - - watchAndRebuild(store, { ...config, cleanDist: true }, build) - } else if (output !== '') { process.exit(1) } } -export async function build(store: Store, config: BuildConfig) { +export async function build(config: BuildConfig) { // Apply currying to create functions pre-configured with config - const ensureDir = ensureDirectory(config) const getManifest = readManifest(config) const getDocsFolder = readDocsFolder(config) const getPartialsFolder = readPartialsFolder(config) - const getPartialsMarkdown = readPartialsMarkdown(config, store) + const getPartialsMarkdown = readPartialsMarkdown(config) const getTypedocsFolder = readTypedocsFolder(config) - const getTypedocsMarkdown = readTypedocsMarkdown(config, store) + const getTypedocsMarkdown = readTypedocsMarkdown(config) const parseMarkdownFile = parseInMarkdownFile(config) - const writeFile = writeDistFile(config) - const writeSdkFile = writeSDKFile(config) - const markdownCache = getMarkdownCache(store) - - await ensureDir(config.distPath) const userManifest = await getManifest() console.info('✓ Read Manifest') @@ -161,13 +117,11 @@ export async function build(store: Store, config: BuildConfig) { const docsFiles = await getDocsFolder() console.info('✓ Read Docs Folder') - const cachedPartialsSize = store.partials.size const partials = await getPartialsMarkdown((await getPartialsFolder()).map((item) => item.path)) - console.info(`✓ Loaded in ${partials.length} partials (${cachedPartialsSize} cached)`) + console.info(`✓ Loaded in ${partials.length} partials`) - const cachedTypedocsSize = store.typedocs.size const typedocs = await getTypedocsMarkdown((await getTypedocsFolder()).map((item) => item.path)) - console.info(`✓ Read ${typedocs.length} Typedocs (${cachedTypedocsSize} cached)`) + console.info(`✓ Read ${typedocs.length} Typedocs`) const docsMap: DocsMap = new Map() const docsInManifest = new Set() @@ -186,22 +140,19 @@ export async function build(store: Store, config: BuildConfig) { }) console.info('✓ Parsed in Manifest') - const cachedDocsSize = store.markdown.size // Read in all the docs const docsArray = await Promise.all( docsFiles.map(async (file) => { const href = removeMdxSuffix(`${config.baseDocsLink}${file.path}`) const inManifest = docsInManifest.has(href) - const markdownFile = await markdownCache(href, () => - parseMarkdownFile(href, partials, typedocs, inManifest, 'docs'), - ) + const markdownFile = await parseMarkdownFile(href, partials, typedocs, inManifest, 'docs') docsMap.set(href, markdownFile) return markdownFile }), ) - console.info(`✓ Loaded in ${docsArray.length} docs (${cachedDocsSize} cached)`) + console.info(`✓ Loaded in ${docsArray.length} docs`) // Goes through and grabs the sdk scoping out of the manifest const sdkScopedManifestFirstPass = await traverseTree( @@ -338,40 +289,6 @@ export async function build(store: Store, config: BuildConfig) { ) console.info('✓ Applied manifest sdk scoping') - if (config.cleanDist) { - await fs.rm(config.distPath, { recursive: true }) - console.info('✓ Removed dist folder') - } - - await writeFile( - 'manifest.json', - JSON.stringify({ - navigation: await traverseTree( - { items: sdkScopedManifest }, - async (item) => ({ - title: item.title, - href: docsMap.get(item.href)?.sdk !== undefined ? scopeHrefToSDK(config)(item.href, ':sdk:') : item.href, - tag: item.tag, - wrap: item.wrap === config.manifestOptions.wrapDefault ? undefined : item.wrap, - icon: item.icon, - target: item.target, - sdk: item.sdk, - }), - // @ts-expect-error - This traverseTree function might just be the death of me - async (group) => ({ - title: group.title, - collapse: group.collapse === config.manifestOptions.collapseDefault ? undefined : group.collapse, - tag: group.tag, - wrap: group.wrap === config.manifestOptions.wrapDefault ? undefined : group.wrap, - icon: group.icon, - hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, - sdk: group.sdk, - items: group.items, - }), - ), - }), - ) - const flatSDKScopedManifest = flattenTree(sdkScopedManifest) const validatedPartials = await Promise.all( @@ -384,7 +301,7 @@ export async function build(store: Store, config: BuildConfig) { const vfile = await remark() .use(remarkFrontmatter) .use(remarkMdx) - .use(validateAndEmbedLinks(config, docsMap, partialPath, 'partials')) + .use(validateLinks(config, docsMap, partialPath, 'partials')) .use(() => (tree, vfile) => { node = tree }) @@ -416,7 +333,7 @@ export async function build(store: Store, config: BuildConfig) { const vfile = await remark() .use(remarkMdx) - .use(validateAndEmbedLinks(config, docsMap, filePath, 'typedoc')) + .use(validateLinks(config, docsMap, filePath, 'typedoc')) .use(() => (tree, vfile) => { node = tree }) @@ -436,7 +353,7 @@ export async function build(store: Store, config: BuildConfig) { let node: Node | null = null const vfile = await remark() - .use(validateAndEmbedLinks(config, docsMap, filePath, 'typedoc')) + .use(validateLinks(config, docsMap, filePath, 'typedoc')) .use(() => (tree, vfile) => { node = tree }) @@ -467,10 +384,8 @@ export async function build(store: Store, config: BuildConfig) { const vfile = await remark() .use(remarkFrontmatter) .use(remarkMdx) - .use(validateAndEmbedLinks(config, docsMap, filePath, 'docs', doc)) + .use(validateLinks(config, docsMap, filePath, 'docs', doc)) .use(validateIfComponents(config, filePath, doc, flatSDKScopedManifest)) - .use(checkPartials(config, validatedPartials, filePath, { reportWarnings: false, embed: true })) - .use(checkTypedoc(config, validatedTypedocs, filePath, { reportWarnings: false, embed: true })) .process(doc.vfile) const distFilePath = `${doc.href.replace(config.baseDocsLink, '')}.mdx` @@ -481,27 +396,11 @@ export async function build(store: Store, config: BuildConfig) { } } - if (doc.sdk !== undefined) { - // This is a sdk specific doc, so we want to put a landing page here to redirect the user to a doc customized to their sdk. - - await writeFile( - distFilePath, - `--- -template: wide ---- -`, - ) - - return vfile - } - - await writeFile(distFilePath, String(vfile)) - return vfile }), ) - console.info(`✓ Validated and wrote out all core docs`) + console.info(`✓ Validated all core docs`) const sdkSpecificVFiles = await Promise.all( config.validSdks.map(async (targetSdk) => { @@ -516,25 +415,16 @@ template: wide .use(remarkMdx) .use(filterOtherSDKsContentOut(config, filePath, targetSdk)) .use(validateUniqueHeadings(config, filePath, 'docs')) - .use(insertFrontmatter({ canonical: doc.sdk ? scopeHrefToSDK(config)(doc.href, ':sdk:') : doc.href })) .process({ ...doc.vfile, messages: [], // reset the messages, otherwise they will be duplicated }) - await writeSdkFile(targetSdk, `${doc.href.replace(config.baseDocsLink, '')}.mdx`, String(vfile)) - return vfile }), ) - const numberOfSdkSpecificDocs = vFiles.filter(Boolean).length - - if (numberOfSdkSpecificDocs > 0) { - console.info(`✓ Wrote out ${numberOfSdkSpecificDocs} ${targetSdk} specific docs`) - } - - return { targetSdk, vFiles } + return vFiles }), ) @@ -593,7 +483,7 @@ template: wide } const flatSdkSpecificVFiles = sdkSpecificVFiles - .flatMap(({ vFiles }) => vFiles) + .flatMap((vFiles) => vFiles) .filter((item): item is NonNullable => item !== null) const partialsVFiles = validatedPartials.map((partial) => partial.vfile) diff --git a/scripts/lib/components/SDKLink.ts b/scripts/lib/components/SDKLink.ts deleted file mode 100644 index cdbb883217..0000000000 --- a/scripts/lib/components/SDKLink.ts +++ /dev/null @@ -1,50 +0,0 @@ -// a fake component that takes the possible props and creates a mdx node -// SDKLink is used for the links that get replaced as they point to a sdk scoped page - -import type { SDK } from '../schemas' -import { u as mdastBuilder } from 'unist-builder' - -export const SDKLink = ( - props: { href: string; sdks: SDK[]; code: true } | { href: string; sdks: SDK[]; code: false; children: unknown }, -) => { - if (props.code) { - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: props.href, - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(props.sdks), - }), - }), - mdastBuilder('mdxJsxAttribute', { - name: 'code', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: props.code, - }), - }), - ], - }) - } - - return mdastBuilder('mdxJsxTextElement', { - name: 'SDKLink', - attributes: [ - mdastBuilder('mdxJsxAttribute', { - name: 'href', - value: props.href, - }), - mdastBuilder('mdxJsxAttribute', { - name: 'sdks', - value: mdastBuilder('mdxJsxAttributeValueExpression', { - value: JSON.stringify(props.sdks), - }), - }), - ], - children: props.children, - }) -} diff --git a/scripts/lib/config.ts b/scripts/lib/config.ts index 5f8e10d625..4cc134ceaf 100644 --- a/scripts/lib/config.ts +++ b/scripts/lib/config.ts @@ -11,7 +11,6 @@ type BuildConfigOptions = { baseDocsLink: string manifestPath: string partialsPath: string - distPath: string typedocPath: string ignoreLinks: string[] ignoreWarnings?: { @@ -24,11 +23,6 @@ type BuildConfigOptions = { collapseDefault: boolean hideTitleDefault: boolean } - cleanDist: boolean - flags?: { - watch?: boolean - controlled?: boolean - } } export type BuildConfig = ReturnType @@ -53,9 +47,6 @@ export function createConfig(config: BuildConfigOptions) { docsRelativePath: config.docsPath, docsPath: resolve(config.docsPath), - distRelativePath: config.distPath, - distPath: resolve(config.distPath), - typedocRelativePath: config.typedocPath, typedocPath: resolve(config.typedocPath), @@ -71,12 +62,5 @@ export function createConfig(config: BuildConfigOptions) { collapseDefault: false, hideTitleDefault: false, }, - - cleanDist: config.cleanDist, - - flags: { - watch: config.flags?.watch ?? false, - controlled: config.flags?.controlled ?? false, - }, } } diff --git a/scripts/lib/dev.ts b/scripts/lib/dev.ts deleted file mode 100644 index f767547a60..0000000000 --- a/scripts/lib/dev.ts +++ /dev/null @@ -1,47 +0,0 @@ -// for development mode, this function watches the markdown, -// invalidates the cache and kicks off a rebuild of the docs - -import watcher from '@parcel/watcher' -import type { BuildConfig } from './config' -import { invalidateFile, type Store } from './store' -import type { build } from '../build-docs' - -export const watchAndRebuild = (store: Store, config: BuildConfig, buildFunc: typeof build) => { - const invalidate = invalidateFile(store, config) - - const handleFileChange: watcher.SubscribeCallback = async (error, events) => { - if (error !== null) { - console.error(error) - return - } - - events.forEach((event) => { - invalidate(event.path) - }) - - try { - const now = performance.now() - - const output = await buildFunc(store, config) - - if (config.flags.controlled) { - console.info('---rebuild-complete---') - } - - const after = performance.now() - - console.info(`Rebuilt docs in ${after - now} milliseconds`) - - if (output !== '') { - console.info(output) - } - } catch (error) { - console.error(error) - - return - } - } - - watcher.subscribe(config.docsPath, handleFileChange) - watcher.subscribe(config.typedocPath, handleFileChange) -} diff --git a/scripts/lib/io.ts b/scripts/lib/io.ts index c99504b210..d64364adbd 100644 --- a/scripts/lib/io.ts +++ b/scripts/lib/io.ts @@ -1,9 +1,8 @@ -import { errorMessages } from './error-messages' import fs from 'node:fs/promises' import path from 'node:path' -import type { BuildConfig } from './config' import readdirp from 'readdirp' -import type { SDK } from './schemas' +import type { BuildConfig } from './config' +import { errorMessages } from './error-messages' // Read in a markdown file from the docs folder export const readMarkdownFile = (config: BuildConfig) => async (docPath: string) => { @@ -28,31 +27,6 @@ export const readDocsFolder = (config: BuildConfig) => async () => { }) } -// checks if a folder exists, if not it will be created -export const ensureDirectory = - (config: BuildConfig) => - async (dirPath: string): Promise => { - try { - await fs.access(dirPath) - } catch { - await fs.mkdir(dirPath, { recursive: true }) - } - } - -// write a file to the dist (output) folder -export const writeDistFile = (config: BuildConfig) => async (filePath: string, contents: string) => { - const ensureDir = ensureDirectory(config) - const fullPath = path.join(config.distPath, filePath) - await ensureDir(path.dirname(fullPath)) - await fs.writeFile(fullPath, contents, { encoding: 'utf-8' }) -} - -// write a file to the dist (output) folder, inside the specified sdk folder -export const writeSDKFile = (config: BuildConfig) => async (sdk: SDK, filePath: string, contents: string) => { - const writeFile = writeDistFile(config) - await writeFile(path.join(sdk, filePath), contents) -} - // not exactly io, but used to parse the json using a result patten export const parseJSON = (json: string) => { try { diff --git a/scripts/lib/partials.ts b/scripts/lib/partials.ts index fe6feebab4..818ed757a3 100644 --- a/scripts/lib/partials.ts +++ b/scripts/lib/partials.ts @@ -9,14 +9,11 @@ import { remark } from 'remark' import remarkFrontmatter from 'remark-frontmatter' import remarkMdx from 'remark-mdx' import type { Node } from 'unist' -import { map as mdastMap } from 'unist-util-map' import { visit as mdastVisit } from 'unist-util-visit' import reporter from 'vfile-reporter' import type { BuildConfig } from './config' import { errorMessages, safeFail } from './error-messages' import { readMarkdownFile } from './io' -import { removeMdxSuffix } from './utils/removeMdxSuffix' -import { getPartialsCache, type Store } from './store' export const readPartialsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.partialsPath, { @@ -57,21 +54,6 @@ export const readPartial = (config: BuildConfig) => async (filePath: string) => }, ) }) - // Process links in partials and remove the .mdx suffix - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink)) return node - if (!('children' in node)) return node - - // We are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - return node - }) - }) .process({ path: `docs/_partials/${filePath}`, value: content, @@ -100,9 +82,8 @@ export const readPartial = (config: BuildConfig) => async (filePath: string) => } } -export const readPartialsMarkdown = (config: BuildConfig, store: Store) => async (paths: string[]) => { +export const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => { const read = readPartial(config) - const partialsCache = getPartialsCache(store) - return Promise.all(paths.map(async (markdownPath) => partialsCache(markdownPath, () => read(markdownPath)))) + return Promise.all(paths.map(async (markdownPath) => read(markdownPath))) } diff --git a/scripts/lib/plugins/insertFrontmatter.ts b/scripts/lib/plugins/insertFrontmatter.ts deleted file mode 100644 index d7fd82ec43..0000000000 --- a/scripts/lib/plugins/insertFrontmatter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { map as mdastMap } from 'unist-util-map' -import yaml from 'yaml' - -export const insertFrontmatter = (newFrontmatter: Record) => () => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'yaml') return node - if (!('value' in node)) return node - if (typeof node.value !== 'string') return node - - const frontmatter = yaml.parse(node.value) - - const transformedFrontmatter = { ...frontmatter, ...newFrontmatter } - - node.value = yaml.stringify(transformedFrontmatter).split('\n').slice(0, -1).join('\n') - - return node - }) -} diff --git a/scripts/lib/plugins/validateAndEmbedLinks.ts b/scripts/lib/plugins/validateLinks.ts similarity index 55% rename from scripts/lib/plugins/validateAndEmbedLinks.ts rename to scripts/lib/plugins/validateLinks.ts index 3fa2b0b43e..7a4d3be109 100644 --- a/scripts/lib/plugins/validateAndEmbedLinks.ts +++ b/scripts/lib/plugins/validateLinks.ts @@ -5,25 +5,23 @@ // - replace the link with the sdk link component if it is a link to a sdk scoped page import { Node } from 'unist' -import { map as mdastMap } from 'unist-util-map' +import { visit as mdastVisit } from 'unist-util-visit' import type { VFile } from 'vfile' -import { SDKLink } from '../components/SDKLink' import { type BuildConfig } from '../config' import { safeMessage, type WarningsSection } from '../error-messages' import { DocsMap } from '../store' import { removeMdxSuffix } from '../utils/removeMdxSuffix' -import { scopeHrefToSDK } from '../utils/scopeHrefToSDK' -export const validateAndEmbedLinks = +export const validateLinks = (config: BuildConfig, docsMap: DocsMap, filePath: string, section: WarningsSection, doc?: { href: string }) => () => (tree: Node, vfile: VFile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink) && (!node.url.startsWith('#') || doc === undefined)) return node - if (!('children' in node)) return node + return mdastVisit(tree, (node) => { + if (node.type !== 'link') return + if (!('url' in node)) return + if (typeof node.url !== 'string') return + if (!node.url.startsWith(config.baseDocsLink) && (!node.url.startsWith('#') || doc === undefined)) return + if (!('children' in node)) return // we are overwriting the url with the mdx suffix removed node.url = removeMdxSuffix(node.url) @@ -36,13 +34,13 @@ export const validateAndEmbedLinks = } const ignore = config.ignoredLink(url) - if (ignore === true) return node + if (ignore === true) return const linkedDoc = docsMap.get(url) if (linkedDoc === undefined) { safeMessage(config, vfile, filePath, section, 'link-doc-not-found', [url], node.position) - return node + return } if (hash !== undefined) { @@ -53,30 +51,6 @@ export const validateAndEmbedLinks = } } - if (linkedDoc.sdk !== undefined) { - // we are going to swap it for the sdk link component to give the users a great experience - - const firstChild = node.children?.[0] - const childIsCodeBlock = firstChild?.type === 'inlineCode' - - if (childIsCodeBlock) { - firstChild.type = 'text' - - return SDKLink({ - href: scopeHrefToSDK(config)(url, ':sdk:'), - sdks: linkedDoc.sdk, - code: true, - }) - } - - return SDKLink({ - href: scopeHrefToSDK(config)(url, ':sdk:'), - sdks: linkedDoc.sdk, - code: false, - children: node.children, - }) - } - - return node + return }) } diff --git a/scripts/lib/store.ts b/scripts/lib/store.ts index ca44f23cd2..6a23e8c3de 100644 --- a/scripts/lib/store.ts +++ b/scripts/lib/store.ts @@ -1,14 +1,6 @@ -// only really needed when in dev mode -// if `build()` is run twice, this can store the important markdown files -// so that we don't have to read them from the file system again which is slow -// use the `invalidateFile()` function to remove a file from the store - -import path from 'node:path' -import type { BuildConfig } from './config' -import { removeMdxSuffix } from './utils/removeMdxSuffix' +import type { parseInMarkdownFile } from './markdown' import type { readPartial } from './partials' import type { readTypedoc } from './typedoc' -import type { parseInMarkdownFile } from './markdown' type MarkdownFile = Awaited>> type PartialsFile = Awaited>> @@ -17,51 +9,3 @@ type TypedocsFile = Awaited>> export type DocsMap = Map export type PartialsMap = Map export type TypedocsMap = Map - -export const createBlankStore = () => ({ - markdown: new Map() as DocsMap, - partials: new Map() as PartialsMap, - typedocs: new Map() as TypedocsMap, -}) - -export type Store = ReturnType - -export const invalidateFile = - (store: ReturnType, config: BuildConfig) => (filePath: string) => { - store.markdown.delete(removeMdxSuffix(`${config.baseDocsLink}${path.relative(config.docsPath, filePath)}`)) - store.partials.delete(path.relative(config.partialsPath, filePath)) - store.typedocs.delete(path.relative(config.typedocPath, filePath)) - } - -export const getMarkdownCache = (store: Store) => { - return async (key: string, cacheMiss: (key: string) => Promise) => { - const cached = store.markdown.get(key) - if (cached) return structuredClone(cached) - - const result = await cacheMiss(key) - store.markdown.set(key, structuredClone(result)) - return result - } -} - -export const getPartialsCache = (store: Store) => { - return async (key: string, cacheMiss: (key: string) => Promise) => { - const cached = store.partials.get(key) - if (cached) return structuredClone(cached) - - const result = await cacheMiss(key) - store.partials.set(key, structuredClone(result)) - return result - } -} - -export const getTypedocsCache = (store: Store) => { - return async (key: string, cacheMiss: (key: string) => Promise) => { - const cached = store.typedocs.get(key) - if (cached) return structuredClone(cached) - - const result = await cacheMiss(key) - store.typedocs.set(key, structuredClone(result)) - return result - } -} diff --git a/scripts/lib/typedoc.ts b/scripts/lib/typedoc.ts index 8cbdc2cd64..5ffdc2e214 100644 --- a/scripts/lib/typedoc.ts +++ b/scripts/lib/typedoc.ts @@ -9,12 +9,10 @@ import readdirp from 'readdirp' import { remark } from 'remark' import remarkMdx from 'remark-mdx' import type { Node } from 'unist' -import { map as mdastMap } from 'unist-util-map' import type { BuildConfig } from './config' import { errorMessages } from './error-messages' import { readMarkdownFile } from './io' import { removeMdxSuffix } from './utils/removeMdxSuffix' -import { getTypedocsCache, type Store } from './store' export const readTypedocsFolder = (config: BuildConfig) => async () => { return readdirp.promise(config.typedocPath, { @@ -42,20 +40,6 @@ export const readTypedoc = (config: BuildConfig) => async (filePath: string) => .use(() => (tree) => { node = tree }) - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink)) return node - if (!('children' in node)) return node - - // We are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - return node - }) - }) .process({ path: typedocPath, value: content, @@ -78,20 +62,6 @@ export const readTypedoc = (config: BuildConfig) => async (filePath: string) => .use(() => (tree) => { node = tree }) - .use(() => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'link') return node - if (!('url' in node)) return node - if (typeof node.url !== 'string') return node - if (!node.url.startsWith(config.baseDocsLink)) return node - if (!('children' in node)) return node - - // We are overwriting the url with the mdx suffix removed - node.url = removeMdxSuffix(node.url) - - return node - }) - }) .process({ path: typedocPath, value: content, @@ -110,9 +80,8 @@ export const readTypedoc = (config: BuildConfig) => async (filePath: string) => } } -export const readTypedocsMarkdown = (config: BuildConfig, store: Store) => async (paths: string[]) => { +export const readTypedocsMarkdown = (config: BuildConfig) => async (paths: string[]) => { const read = readTypedoc(config) - const typedocsCache = getTypedocsCache(store) - return Promise.all(paths.map(async (filePath) => typedocsCache(filePath, () => read(filePath)))) + return Promise.all(paths.map(async (filePath) => read(filePath))) } From 34c6fc7c3a2c77ac1c6ce3c9c07880eeca89f85b Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 25 Apr 2025 14:24:12 -0700 Subject: [PATCH 113/114] strip down pr --- .gitignore | 1 - CONTRIBUTING.md | 19 -- docs/index.mdx | 38 +-- docs/quickstarts/overview.mdx | 28 +-- docs/references/overview.mdx | 38 +-- package-lock.json | 431 ---------------------------------- package.json | 3 - 7 files changed, 52 insertions(+), 506 deletions(-) diff --git a/.gitignore b/.gitignore index dd334554f0..d39757dbbf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ # production /build -/dist # misc .DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 416c037d89..e07b67eb03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -379,25 +379,6 @@ You may also set `search` to a boolean value, which acts as an `exclude` value. -#### SDK - -the `sdk` frontmatter is the best way to define what sdks a page supports. If you are writing documentation that only works under certain sdks, setting this value will indicate to the docs to only make the page available when the reader has one of the specified sdks available. - -```diff - --- - title: -+ sdk: nextjs, react - --- -``` - -This does a couple things: - -- The pages url gets generated out, say the above page is at `/docs/clerk-provider.mdx` then `/docs/nextjs/clerk-provider` and `/docs/expo/clerk-provider` will be generated. - - The base url `/docs/clerk-provider` will still exist, but will show a grid of the available variants. -- The page will only show up in the sidebar navigation if the reader has one of the specified sdks active. -- Links to this page will be 'smart' and point the user towards the correct variant of the page. -- A variant selector will be shown in the top right of the page, allowing the user to switch between the different variants of the page. - ### Headings Headings should be nested by their rank. Headings with an equal or higher rank start a new section, headings with a lower rank start new subsections that are part of the higher ranked section. Please see the [Web Accessibility Initiative documentation](https://www.w3.org/WAI/tutorials/page-structure/headings/) for more information. diff --git a/docs/index.mdx b/docs/index.mdx index 030cf52c3d..be2868c462 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -37,73 +37,73 @@ Find all the guides and resources you need to develop with Clerk. - [Next.js](/docs/quickstarts/nextjs) - Easily add secure, beautiful, and fast authentication to Next.js with Clerk. - - + - {} --- - [React](/docs/quickstarts/react) - Get started installing and initializing Clerk in a new React + Vite app. - - + - {} --- - [Astro](/docs/quickstarts/astro) - Easily add secure and SSR-friendly authentication to your Astro application with Clerk. - - + - {} --- - [Chrome Extension](/docs/quickstarts/chrome-extension) - Use the Chrome Extension SDK to authenticate users in your Chrome extension. - - + - {} --- - [Expo](/docs/quickstarts/expo) - Use Clerk with Expo to authenticate users in your React Native application. - - + - {} --- - [iOS](/docs/quickstarts/ios) - Use the Clerk iOS SDK to authenticate users in your native Apple applications. - - + - {} --- - [JavaScript](/docs/quickstarts/javascript) - The Clerk JavaScript SDK gives you access to prebuilt components and helpers to make user authentication easier. - - + - {} --- - [Nuxt](/docs/quickstarts/nuxt) - Easily add secure, beautiful, and fast authentication to Nuxt with Clerk. - - + - {} --- - [React Router](/docs/quickstarts/react-router) - Easily add secure, edge- and SSR-friendly authentication to React Router with Clerk. - - + - {} --- - [Remix](/docs/quickstarts/remix) - Easily add secure, edge- and SSR-friendly authentication to Remix with Clerk. - - + - {} --- - [TanStack React Start (beta)](/docs/quickstarts/tanstack-react-start) - Easily add secure and SSR-friendly authentication to your TanStack React Start application with Clerk. - - + - {} --- - [Vue](/docs/quickstarts/vue) - Get started installing and initializing Clerk in a new Vue + Vite app. - - + - {} ## Explore by backend framework @@ -113,43 +113,43 @@ Find all the guides and resources you need to develop with Clerk. - [JS Backend SDK](/docs/references/backend/overview) - The Clerk Backend SDK exposes our Backend API resources and low-level authentication utilities for JavaScript environments. - - + - {} --- - [C#](https://github.com/clerk/clerk-sdk-csharp/blob/main/README.md) - The Clerk C# SDK is a wrapper around our Backend API to make it easier to integrate Clerk into your backend. - - + - {} --- - [Express](/docs/quickstarts/express) - Quickly add authentication and user management to your Express application. - - + - {} --- - [Go](/docs/references/go/overview) - The Clerk Go SDK is a wrapper around the Backend API written in Golang to make it easier to integrate Clerk into your backend. - - + - {} --- - [Fastify](/docs/quickstarts/fastify) - Build secure authentication and user management flows for your Fastify server. - - + - {} --- - [Python](https://github.com/clerk/clerk-sdk-python/blob/main/README.md) - The Clerk Python SDK is a wrapper around the Backend API written in Python to make it easier to integrate Clerk into your backend. - - + - {} --- - [Ruby on Rails](/docs/quickstarts/ruby) - Integrate authentication and user management into your Ruby application. - - + - {} ## Explore by feature diff --git a/docs/quickstarts/overview.mdx b/docs/quickstarts/overview.mdx index 28d31de4b1..0c3d1dc944 100644 --- a/docs/quickstarts/overview.mdx +++ b/docs/quickstarts/overview.mdx @@ -8,37 +8,37 @@ description: See the getting started guides and tutorials. - [Next.js](/docs/quickstarts/nextjs) - Easily add secure, beautiful, and fast authentication to your Next.js application with Clerk. - - + - {} --- - [Astro](/docs/quickstarts/astro) - Easily add secure and SSR-friendly authentication to your Astro application with Clerk. - - + - {} --- - [Nuxt](/docs/quickstarts/nuxt) - Easily add secure, beautiful, and fast authentication to Nuxt with Clerk. - - + - {} --- - [React Router (Beta)](/docs/quickstarts/react-router) - The Clerk React Router SDK provides prebuilt components, hooks, and stores to make it easy to integrate authentication and user management in your React Router app. - - + - {} --- - [Remix](/docs/quickstarts/remix) - Easily add secure, edge- and SSR-friendly authentication to your Remix application with Clerk. - - + - {} --- - [TanStack React Start (beta)](/docs/quickstarts/tanstack-react-start) - Easily add secure and SSR-friendly authentication to your TanStack React Start application with Clerk. - - + - {} ## Frontend @@ -46,37 +46,37 @@ description: See the getting started guides and tutorials. - [React](/docs/quickstarts/react) - Easily add secure, beautiful, and fast authentication to your React application with Clerk. - - + - {} --- - [Chrome Extension](/docs/quickstarts/chrome-extension) - Use the Chrome Extension SDK to authenticate users in your Chrome extension. - - + - {} --- - [Expo](/docs/quickstarts/expo) - Use Clerk with Expo to authenticate users in your React Native application. - - + - {} --- - [iOS](/docs/quickstarts/ios) - Use the Clerk iOS SDK to authenticate users in your native Apple applications. - - + - {} --- - [JavaScript](/docs/quickstarts/javascript) - Easily add secure, beautiful, and fast authentication to your JavaScript application with Clerk. - - + - {} --- - [Vue](/docs/quickstarts/vue) - Easily add secure, beautiful, and fast authentication to your Vue application with Clerk. - - + - {} ## Backend @@ -84,13 +84,13 @@ description: See the getting started guides and tutorials. - [Express](/docs/quickstarts/express) - Easily add secure, beautiful, and fast authentication to your Express application with Clerk. - - + - {} --- - [Fastify](/docs/quickstarts/fastify) - Easily add secure, beautiful, and fast authentication to your Fastify application with Clerk. - - + - {} diff --git a/docs/references/overview.mdx b/docs/references/overview.mdx index 795f8a19a0..3dba2cb406 100644 --- a/docs/references/overview.mdx +++ b/docs/references/overview.mdx @@ -10,73 +10,73 @@ description: Learn about the Clerk and community SDK's available for integrating - [Next.js](/docs/references/nextjs/overview) - Easily add secure, beautiful, and fast authentication to Next.js with Clerk. - - + - {} --- - [React](/docs/references/react/overview) - Get started installing and initializing Clerk in a new React + Vite app. - - + - {} --- - [Astro](/docs/references/astro/overview) - Easily add secure and SSR-friendly authentication to your Astro application with Clerk. - - + - {} --- - [Chrome Extension](/docs/references/chrome-extension/overview) - Use the Chrome Extension SDK to authenticate users in your Chrome extension. - - + - {} --- - [Expo](/docs/references/expo/overview) - Use Clerk with Expo to authenticate users in your React Native application. - - + - {} --- - [iOS](/docs/references/ios/overview) - Use the Clerk iOS SDK to authenticate users in your native Apple applications. - - + - {} --- - [JavaScript](/docs/references/javascript/overview) - The Clerk JavaScript SDK gives you access to prebuilt components and helpers to make user authentication easier. - - + - {} --- - [Nuxt](/docs/references/nuxt/overview) - Easily add secure, beautiful, and fast authentication to Nuxt with Clerk. - - + - {} --- - [React Router](/docs/references/react-router/overview) - Easily add secure, edge- and SSR-friendly authentication to React Router with Clerk. - - + - {} --- - [Remix](/docs/references/remix/overview) - Easily add secure, edge- and SSR-friendly authentication to Remix with Clerk. - - + - {} --- - [TanStack React Start (beta)](/docs/references/tanstack-react-start/overview) - Easily add secure and SSR-friendly authentication to your TanStack React Start application with Clerk. - - + - {} --- - [Vue](/docs/references/vue/overview) - Get started installing and initializing Clerk in a new Vue + Vite app. - - + - {} ## Backend SDKs @@ -84,43 +84,43 @@ description: Learn about the Clerk and community SDK's available for integrating - [JS Backend SDK](/docs/references/backend/overview) - The Clerk Backend SDK exposes our Backend API resources and low-level authentication utilities for JavaScript environments. - - + - {} --- - [C#](https://github.com/clerk/clerk-sdk-csharp/blob/main/README.md) - The Clerk C# SDK is a wrapper around our Backend API to make it easier to integrate Clerk into your backend. - - + - {} --- - [Express](/docs/references/express/overview) - Quickly add authentication and user management to your Express application. - - + - {} --- - [Go](/docs/references/go/overview) - The Clerk Go SDK is a wrapper around the Backend API written in Golang to make it easier to integrate Clerk into your backend. - - + - {} --- - [Fastify](/docs/references/fastify/overview) - Build secure authentication and user management flows for your Fastify server. - - + - {} --- - [Python](https://github.com/clerk/clerk-sdk-python/blob/main/README.md) - The Clerk Python SDK is a wrapper around the Backend API written in Python to make it easier to integrate Clerk into your backend. - - + - {} --- - [Ruby on Rails](/docs/references/ruby/overview) - Integrate authentication and user management into your Ruby application. - - + - {} ## Build with community-maintained SDKs diff --git a/package-lock.json b/package-lock.json index ceec96cbbf..70cb2b12de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "clerk-docs-2023", "version": "0.1.0", "devDependencies": { - "@parcel/watcher": "^2.5.1", "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", "concurrently": "^8.2.2", @@ -24,7 +23,6 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", - "unist-builder": "^4.0.0", "unist-util-filter": "^5.0.1", "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", @@ -719,315 +717,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.8", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", @@ -1614,19 +1303,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1914,19 +1590,6 @@ "node": ">=6" } }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -2068,19 +1731,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2168,16 +1818,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2187,29 +1827,6 @@ "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3550,20 +3167,6 @@ } ] }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", @@ -3616,13 +3219,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT" - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -4216,19 +3812,6 @@ "node": ">=14.0.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -4314,20 +3897,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-builder": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", - "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-filter": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz", diff --git a/package.json b/package.json index 3a1478be78..639fc027a6 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,12 @@ "lint:check-frontmatter": "node ./scripts/check-frontmatter.mjs", "lint:validation": "npm run build", "build": "tsx ./scripts/build-docs.ts", - "dev": "tsx ./scripts/build-docs.ts --watch", "test": "vitest --silent", "typedoc:download": "git clone https://github.com/clerk/generated-typedoc.git --single-branch --depth 1 clerk-typedoc && rm -rf clerk-typedoc/.git", "typedoc:update": "rm -rf clerk-typedoc && npm run typedoc:download", "move-doc": "node scripts/move-doc.mjs" }, "devDependencies": { - "@parcel/watcher": "^2.5.1", "@sindresorhus/slugify": "^2.2.1", "@types/node": "^22.13.2", "concurrently": "^8.2.2", @@ -34,7 +32,6 @@ "remark-mdx": "^3.0.1", "tsx": "^4.19.2", "typescript": "^5.7.3", - "unist-builder": "^4.0.0", "unist-util-filter": "^5.0.1", "unist-util-map": "^4.0.0", "unist-util-visit": "^5.0.0", From 9aee6e0d2363c4bcdbc3b12d32d665d2c2ff7ce4 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 25 Apr 2025 14:32:38 -0700 Subject: [PATCH 114/114] remove unused code --- scripts/lib/utils/scopeHrefToSDK.ts | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 scripts/lib/utils/scopeHrefToSDK.ts diff --git a/scripts/lib/utils/scopeHrefToSDK.ts b/scripts/lib/utils/scopeHrefToSDK.ts deleted file mode 100644 index 39de6fd363..0000000000 --- a/scripts/lib/utils/scopeHrefToSDK.ts +++ /dev/null @@ -1,20 +0,0 @@ -// if a link contains the :sdk: token, it will be replaced with the targetSDK - -import type { BuildConfig } from '../config' -import type { SDK } from '../schemas' - -export const scopeHrefToSDK = (config: BuildConfig) => (href: string, targetSDK: SDK | ':sdk:') => { - // This is external so can't change it - if (href.startsWith('/docs') === false) return href - - const hrefSegments = href.split('/') - - // This is a little hacky so we might change it - // if the url already contains the sdk, we don't need to change it - if (hrefSegments.includes(targetSDK)) { - return href - } - - // Add the sdk to the url - return `${config.baseDocsLink}${targetSDK}/${hrefSegments.slice(2).join('/')}` -}