From 2d26b76d6b0478aba618431651033c1fccf1e439 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 7 Feb 2025 23:01:30 +0800 Subject: [PATCH 01/74] 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 02/74] 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 03/74] 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 04/74] 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 05/74] 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 06/74] 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 07/74] 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 08/74] 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 09/74] 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 10/74] 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 11/74] 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 12/74] 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 13/74] 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 14/74] 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 15/74] 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 16/74] 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 17/74] 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 18/74] 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 19/74] 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 20/74] 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 21/74] 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 22/74] 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 23/74] 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 24/74] 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 25/74] 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 26/74] 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 27/74] 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 28/74] 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 29/74] 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 30/74] 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 31/74] (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 32/74] 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 33/74] 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 34/74] 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 35/74] 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 36/74] 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 37/74] 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 38/74] 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 39/74] 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 40/74] 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 41/74] 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 42/74] 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 43/74] 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 24cd69d4473fac29de304007b8ddd02eb4c3b98a Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 5 Mar 2025 23:45:21 +0800 Subject: [PATCH 44/74] 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 81332d613fce8a56e9470634efb38ef4194195d4 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 6 Mar 2025 01:03:19 +0800 Subject: [PATCH 45/74] 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 46/74] 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 47/74] 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 48/74] 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 49/74] 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 50/74] 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 51/74] 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 52/74] 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 53/74] 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 54/74] 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 55/74] 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 56/74] =?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 57/74] 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 58/74] 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 59/74] 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 60/74] 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 61/74] 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 62/74] 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 63/74] 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 64/74] 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 65/74] =?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 66/74] 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 67/74] 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 68/74] 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 69/74] =?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 5cc8deaf2b9132163da1018a5e1f200be7c78566 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 13 Mar 2025 03:13:49 +0800 Subject: [PATCH 70/74] Update scripts/build-docs.ts Co-authored-by: Jeff Escalante --- 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 c672991842..ba41f62fc2 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -28,7 +28,7 @@ import { Node } from 'unist' const VALID_SDKS = [ 'nextjs', 'react', - 'javascript-frontend', + 'js-frontend', 'chrome-extension', 'expo', 'ios', @@ -44,7 +44,7 @@ const VALID_SDKS = [ 'vue', 'ruby', 'python', - 'javascript-backend', + 'js-backend', 'sdk-development', 'community-sdk', ] as const From e5aefbb1092d04ee4873f15a759adb7a76b7aac2 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 28 Mar 2025 01:12:16 +0800 Subject: [PATCH 71/74] fix up types --- 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 ba41f62fc2..f091a19449 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -565,9 +565,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 cdf169a4529a0fc943c10ce69e0d28cc9e891e5d Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Tue, 1 Apr 2025 23:56:38 +0800 Subject: [PATCH 72/74] Add ability to ignore warnings on a per file basis --- scripts/build-docs.test.ts | 407 +++++++++++++++++++++++++++++++++++++ scripts/build-docs.ts | 279 ++++++++++++++++++------- 2 files changed, 617 insertions(+), 69 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 452ccf2933..206ba1ff53 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -96,6 +96,7 @@ const baseConfig = { manifestPath: '../docs/manifest.json', partialsPath: '../docs/_partials', ignorePaths: ['/docs/_partials'], + ignoreWarnings: {}, manifestOptions: { wrapDefault: true, collapseDefault: false, @@ -906,3 +907,409 @@ Content for section 2.`, expect(output).toContain('warning Hash "invalid-section" not found in /docs/target-document') }) }) + +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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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 f091a19449..5d43384193 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -22,8 +22,114 @@ 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' + +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', @@ -174,8 +280,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 => { @@ -198,7 +302,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) => { @@ -208,7 +312,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 } } @@ -238,7 +342,7 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: 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 @@ -252,7 +356,7 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: 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) }, ) @@ -271,7 +375,7 @@ const readPartialsMarkdown = (config: BuildConfig) => async (paths: string[]) => } if (partialNode === null) { - throw new Error(`Failed to parse the content of ${markdownPath}`) + throw new Error(errorMessages['partial-parse-error'](markdownPath)) } return { @@ -376,11 +480,13 @@ function flattenTree< } 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') { @@ -393,12 +499,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 } @@ -406,8 +516,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 } @@ -415,8 +525,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 } @@ -428,30 +538,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) - - 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] +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)) + 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) => @@ -460,7 +584,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, }) } @@ -475,17 +599,16 @@ const parseInMarkdownFile = const slugify = slugifyWithCounter() const headingsHashs: Array = [] + const filePath = `${href}.mdx` const vfile = await markdownProcessor() .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]) } }) .use(() => (tree, vfile) => { @@ -502,20 +625,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 = { @@ -527,19 +654,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 } @@ -548,7 +675,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 } @@ -602,7 +729,7 @@ const parseInMarkdownFile = }) if (frontmatter === undefined) { - throw new Error(`Frontmatter parsing failed for ${href}`) + throw new Error(errorMessages['frontmatter-parse-failed'](href)) } return { @@ -677,7 +804,11 @@ export const build = async (config: BuildConfig) => { 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 @@ -691,9 +822,10 @@ export const build = async (config: BuildConfig) => { 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)) + } } } @@ -720,9 +852,10 @@ export const build = async (config: BuildConfig) => { 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)) + } } } @@ -757,6 +890,7 @@ export const build = async (config: BuildConfig) => { 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 .use(() => (tree, vfile) => { @@ -775,7 +909,7 @@ export const build = async (config: BuildConfig) => { 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 } @@ -783,7 +917,7 @@ export const build = async (config: BuildConfig) => { 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) } } }) @@ -795,6 +929,7 @@ export const build = async (config: BuildConfig) => { const coreVFiles = await Promise.all( docsArray.map(async (doc) => { + const filePath = `${doc.href}.mdx` const vfile = await markdownProcessor() // Validate links between docs are valid .use(() => (tree: Node, vfile: VFile) => { @@ -813,7 +948,7 @@ export const build = async (config: BuildConfig) => { 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 } @@ -821,7 +956,7 @@ export const build = async (config: BuildConfig) => { 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) } } }) @@ -829,11 +964,11 @@ export const build = async (config: BuildConfig) => { // 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 @@ -851,8 +986,12 @@ export const build = async (config: BuildConfig) => { 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, ) } @@ -864,10 +1003,7 @@ export const build = async (config: BuildConfig) => { 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) } })() }) @@ -878,9 +1014,9 @@ export const build = async (config: BuildConfig) => { 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)) + } } return vfile @@ -899,6 +1035,7 @@ type BuildConfigOptions = { manifestPath: string partialsPath: string ignorePaths: string[] + ignoreWarnings?: Record manifestOptions: { wrapDefault: boolean collapseDefault: boolean @@ -928,6 +1065,7 @@ export function createConfig(config: BuildConfigOptions) { partialsPath: resolve(config.partialsPath), ignorePaths: config.ignorePaths, + ignoreWarnings: config.ignoreWarnings || {}, manifestOptions: config.manifestOptions ?? { wrapDefault: true, collapseDefault: false, @@ -956,6 +1094,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 76d488e4bc0a7a7b3044102cf178e4a2e6082cae Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Wed, 2 Apr 2025 00:03:11 +0800 Subject: [PATCH 73/74] merge in main --- 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 5d43384193..6436bef926 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -143,7 +143,7 @@ const VALID_SDKS = [ 'fastify', 'react-router', 'remix', - 'tanstack-start', + 'tanstack-react-start', 'go', 'astro', 'nuxt', From 7b97a0709feec3be4a3af67a9dd8aba18d129dad Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Mon, 7 Apr 2025 12:30:27 -0700 Subject: [PATCH 74/74] switch back to not failing on warnings --- 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 6436bef926..be59b712bb 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -1109,7 +1109,7 @@ const main = async () => { if (output !== '') { console.info(output) - process.exit(1) + process.exit(0) } }