diff --git a/docs-config.ts b/docs-config.ts index dd5e758b..2c9ceb49 100644 --- a/docs-config.ts +++ b/docs-config.ts @@ -31,4 +31,23 @@ export default { ltsVersions: { prometheus: ["2.53"], }, + + // Repositories for the downloads page. The order in this file is the + // order in which they will be displayed on the downloads page. + downloads: { + owner: "prometheus", + repos: [ + "prometheus", + "alertmanager", + "blackbox_exporter", + "consul_exporter", + "graphite_exporter", + "memcached_exporter", + "mysqld_exporter", + "node_exporter", + "promlens", + "pushgateway", + "statsd_exporter", + ], + }, } satisfies DocsConfig; diff --git a/eslint.config.mjs b/eslint.config.mjs index 3b910158..71592d4c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,10 @@ const eslintConfig = [ { rules: { "@next/next/no-img-element": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], }, }, ]; diff --git a/package-lock.json b/package-lock.json index 0e76614f..5ed640f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,13 @@ "name": "prometheus-next", "version": "0.1.0", "dependencies": { + "@docsearch/js": "^3.9.0", "@mantine/code-highlight": "^8.0.0", "@mantine/core": "^8.0.0", "@mantine/hooks": "^8.0.0", + "@mantine/spotlight": "^8.0.0", "@tabler/icons-react": "^3.31.0", + "@types/semver": "^7.7.0", "gray-matter": "^4.0.3", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", @@ -25,6 +28,7 @@ "rehype-slug": "^6.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", + "semver": "^7.7.1", "shiki": "^3.4.0" }, "devDependencies": { @@ -43,6 +47,231 @@ "typescript": "^5" } }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz", + "integrity": "sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.9", + "@algolia/autocomplete-shared": "1.17.9" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz", + "integrity": "sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.9" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz", + "integrity": "sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.9" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz", + "integrity": "sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.24.0.tgz", + "integrity": "sha512-pNTIB5YqVVwu6UogvdX8TqsRZENaflqMMjdY7/XIPMNGrBoNH9tewINLI7+qc9tIaOLcAp3ZldqoEwAihZZ3ig==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.24.0.tgz", + "integrity": "sha512-IF+r9RRQsIf0ylIBNFxo7c6hDxxuhIfIbffhBXEF1HD13rjhP5AVfiaea9RzbsAZoySkm318plDpH/nlGIjbRA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.24.0.tgz", + "integrity": "sha512-p8K6tiXQTebRBxbrzWIfGCvfkT+Umml+2lzI92acZjHsvl6KYH6igOfVstKqXJRei9pvRzEEvVDNDLXDVleGTA==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.24.0.tgz", + "integrity": "sha512-jOHF0+tixR3IZJMhZPquFNdCVPzwzzXoiqVsbTvfKojeaY6ZXybgUiTSB8JNX+YpsUT8Ebhu3UvRy4mw2PbEzw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.24.0.tgz", + "integrity": "sha512-Fx/Fp6d8UmDBHecTt0XYF8C9TAaA3qeCQortfGSZzWp4gVmtrUCFNZ1SUwb8ULREnO9DanVrM5hGE8R8C4zZTQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.24.0.tgz", + "integrity": "sha512-F8ypOedSMhz6W7zuT5O1SXXsdXSOVhY2U6GkRbYk/mzrhs3jWFR3uQIfeQVWmsJjUwIGZmPoAr9E+T/Zm2M4wA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.24.0.tgz", + "integrity": "sha512-k+nuciQuq7WERNNE+hsx3DX636zIy+9R4xdtvW3PANT2a2BDGOv3fv2mta8+QUMcVTVcGe/Mo3QCb4pc1HNoxA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.24.0.tgz", + "integrity": "sha512-/lqVxmrvwoA+OyVK4XLMdz/PJaCTW4qYchX1AZ+98fdnH3K6XM/kMydQLfP0bUNGBQbmVrF88MqhqZRnZEn/MA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.24.0.tgz", + "integrity": "sha512-cRisDXQJhvfZCXL4hD22qca2CmW52TniOx6L7pvkaBDx0oQk1k9o+3w11fgfcCG+47OndMeNx5CMpu+K+COMzg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.24.0.tgz", + "integrity": "sha512-JTMz0JqN2gidvKa2QCF/rMe8LNtdHaght03px2cluZaZfBRYy8TgHgkCeBspKKvV/abWJwl7J0FzWThCshqT3w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.24.0.tgz", + "integrity": "sha512-B2Gc+iSxct1WSza5CF6AgfNgmLvVb61d5bqmIWUZixtJIhyAC6lSQZuF+nvt+lmKhQwuY2gYjGGClil8onQvKQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.24.0.tgz", + "integrity": "sha512-6E5+hliqGc5w8ZbyTAQ+C3IGLZ/GiX623Jl2bgHA974RPyFWzVSj4rKqkboUAxQmrFY7Z02ybJWVZS5OhPQocA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.24.0.tgz", + "integrity": "sha512-zM+nnqZpiQj20PyAh6uvgdSz+hD7Rj7UfAZwizqNP+bLvcbGXZwABERobuilkCQqyDBBH4uv0yqIcPRl8dSBEg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", @@ -52,6 +281,54 @@ "node": ">=6.9.0" } }, + "node_modules/@docsearch/css": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.9.0.tgz", + "integrity": "sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==", + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.9.0.tgz", + "integrity": "sha512-4bKHcye6EkLgRE8ze0vcdshmEqxeiJM77M0JXjef7lrYZfSlMunrDOCqyLjiZyo1+c0BhUqA2QpFartIjuHIjw==", + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.9.0", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.9.0.tgz", + "integrity": "sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.9", + "@algolia/autocomplete-preset-algolia": "1.17.9", + "@docsearch/css": "3.9.0", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -1188,6 +1465,30 @@ "react": "^18.x || ^19.x" } }, + "node_modules/@mantine/spotlight": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/spotlight/-/spotlight-8.0.0.tgz", + "integrity": "sha512-E8YS+q78IYvXGcTvLX+OFUJPNkf73o/ZWmO4OKdI0Hxvw5XFELCzRkYnLGQ5npeTfn74t4X3fitWBfQT26lJzQ==", + "license": "MIT", + "dependencies": { + "@mantine/store": "8.0.0" + }, + "peerDependencies": { + "@mantine/core": "8.0.0", + "@mantine/hooks": "8.0.0", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/store": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.0.0.tgz", + "integrity": "sha512-42RWCsXMNuhpX+d/hwr5aHj+HWyi5ltbc0R0xdiUnAmqSB7CHbWxDDLh4+DbmqPrN9pTeYvpPGp3v/CG2vuGBg==", + "license": "MIT", + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", @@ -2001,6 +2302,12 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2544,6 +2851,30 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/algoliasearch": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.24.0.tgz", + "integrity": "sha512-CkaUygzZ91Xbw11s0CsHMawrK3tl+Ue57725HGRgRzKgt2Z4wvXVXRCtQfvzh8K7Tp4Zp7f1pyHAtMROtTJHxg==", + "license": "MIT", + "dependencies": { + "@algolia/client-abtesting": "5.24.0", + "@algolia/client-analytics": "5.24.0", + "@algolia/client-common": "5.24.0", + "@algolia/client-insights": "5.24.0", + "@algolia/client-personalization": "5.24.0", + "@algolia/client-query-suggestions": "5.24.0", + "@algolia/client-search": "5.24.0", + "@algolia/ingestion": "1.24.0", + "@algolia/monitoring": "1.24.0", + "@algolia/recommend": "5.24.0", + "@algolia/requester-browser-xhr": "5.24.0", + "@algolia/requester-fetch": "5.24.0", + "@algolia/requester-node-http": "5.24.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7557,6 +7888,16 @@ "postcss": "^8.2.1" } }, + "node_modules/preact": { + "version": "10.26.6", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.6.tgz", + "integrity": "sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8194,6 +8535,13 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", + "peer": true + }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -8211,7 +8559,6 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index 8d90c918..2ac7ec79 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,21 @@ "scripts": { "dev": "next dev --turbopack", "fetch-repo-docs": "tsx scripts/fetch-repo-docs.ts", + "fetch-downloads-info": "tsx scripts/fetch-downloads-info.ts", "clean": "rm -rf generated out .next", "build": "next build", + "build-all": "npm run clean && npm run fetch-repo-docs && npm run fetch-downloads-info && next build", "start": "next start", "lint": "next lint" }, "dependencies": { + "@docsearch/js": "^3.9.0", "@mantine/code-highlight": "^8.0.0", "@mantine/core": "^8.0.0", "@mantine/hooks": "^8.0.0", + "@mantine/spotlight": "^8.0.0", "@tabler/icons-react": "^3.31.0", + "@types/semver": "^7.7.0", "gray-matter": "^4.0.3", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", @@ -29,6 +34,7 @@ "rehype-slug": "^6.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", + "semver": "^7.7.1", "shiki": "^3.4.0" }, "devDependencies": { diff --git a/scripts/fetch-downloads-info.ts b/scripts/fetch-downloads-info.ts new file mode 100644 index 00000000..981e3542 --- /dev/null +++ b/scripts/fetch-downloads-info.ts @@ -0,0 +1,177 @@ +import { octokit } from "./githubClient"; +import { GetResponseDataTypeFromEndpointMethod } from "@octokit/types"; +import * as fs from "fs"; +import * as path from "path"; +import docsConfig from "../docs-config"; +import { Downloads, Release, Binary } from "@/downloads-metadata-types"; +import { compareFullVersion, filterUnique, majorMinor } from "./utils"; + +const OUTDIR = "./generated"; + +const assetChecksums: { [releaseID: string]: { [fileName: string]: string } } = + {}; + +type OctokitRelease = GetResponseDataTypeFromEndpointMethod< + typeof octokit.rest.repos.listReleases +>[number]; + +const downloads: Downloads = { + repos: [], + operatingSystems: [], + architectures: [], + releases: [], +}; + +const getBinaries = (r: OctokitRelease) => { + const binaries = r.assets + .filter( + (a) => + (a.name.endsWith(".tar.gz") || a.name.endsWith(".zip")) && + !a.name.includes("-web-ui-") + ) + .map((a): Binary => { + const baseName = a.name.replace(/(\.tar\.gz|\.zip)$/, ""); + const lastPart = baseName.split(".").slice(-1)[0]; + const [os, arch] = lastPart.split("-"); + + return { + name: a.name, + checksum: assetChecksums[r.id][a.name], + os, + arch, + url: a.browser_download_url, + sizeBytes: a.size, + }; + }); + + const grouped = Object.groupBy(binaries, (b) => `${b.os}/${b.arch}`); + + // Within each group, sort by name and then return the last one. + const sortedBinaries = Object.values(grouped).map((group) => { + const sorted = group!.sort((a, b) => a.name.localeCompare(b.name)); + return sorted[sorted.length - 1]; + }); + + return sortedBinaries; +}; + +for (const repoName of docsConfig.downloads.repos) { + console.group(`Fetching downloads info for ${repoName}`); + + // Fetch repo base information. + console.log(`Fetching repo info for ${repoName}`); + const repoInfo = await octokit.rest.repos.get({ + owner: docsConfig.downloads.owner, + repo: repoName, + }); + + // Fetch releases information for the repo. + console.log(`Fetching releases info for ${repoName}`); + const releasesInfo = await octokit.rest.repos.listReleases({ + owner: docsConfig.downloads.owner, + repo: repoName, + }); + + releasesInfo.data + .sort((a, b) => compareFullVersion(a.tag_name, b.tag_name)) + .reverse(); + + // Select the relevant stable, pre-release, and LTS versions to show. + const preReleases: string[] = []; + const stableReleases: string[] = []; + const shownReleases: OctokitRelease[] = []; + + for (const r of releasesInfo.data) { + const version = majorMinor(r.tag_name); + + if (r.prerelease) { + if (!preReleases.includes(version) && stableReleases.length === 0) { + shownReleases.push(r); + preReleases.push(version); + } + } else if ( + docsConfig.ltsVersions[repoName]?.includes(version) && + !stableReleases.includes(version) + ) { + shownReleases.push(r); + stableReleases.push(version); + } else if (stableReleases.length === 0) { + shownReleases.push(r); + stableReleases.push(version); + } + } + + for (const r of shownReleases) { + // Fetch and store checksums for each release. + const checksumsFile = r.assets.find((a) => a.name === "sha256sums.txt"); + if (!checksumsFile) { + console.warn( + `No sha256sums.txt asset found for release ${r.tag_name} in ${repoName}` + ); + continue; + } + const downloadURL = checksumsFile.browser_download_url; + assetChecksums[r.id] = {}; + + const response = await fetch(downloadURL); + if (!response.ok) { + throw new Error( + `Failed to fetch checksums for release ${r.tag_name} in ${repoName}: ${response.statusText}` + ); + } + + const checksums = await response.text(); + for (const line of checksums.split("\n")) { + const [checksum, fileName] = line.split(/\s+/); + if (fileName) { + assetChecksums[r.id][fileName] = checksum; + } + } + } + + downloads.repos.push({ + name: repoInfo.data.name, + fullName: repoInfo.data.full_name, + description: repoInfo.data.description || "", + url: repoInfo.data.html_url, + releases: shownReleases.map( + (r): Release => ({ + id: r.id, + name: r.name || "", + url: r.html_url, + prerelease: r.prerelease, + ltsRelease: docsConfig.ltsVersions[repoName]?.includes( + majorMinor(r.tag_name) + ), + majorMinor: majorMinor(r.tag_name), + binaries: getBinaries(r), + }) + ), + }); + + console.groupEnd(); +} + +downloads.operatingSystems = downloads.repos + .flatMap((repo) => + repo.releases + .flatMap((release) => release.binaries) + .flatMap((binary) => binary.os) + ) + .filter(filterUnique) + .sort(); + +downloads.architectures = downloads.repos + .flatMap((repo) => + repo.releases + .flatMap((release) => release.binaries) + .flatMap((binary) => binary.arch) + ) + .filter(filterUnique) + .sort(); + +console.log(`Writing downloads metadata to ${OUTDIR}/downloads-metadata.json`); +fs.writeFileSync( + path.join(OUTDIR, "downloads-metadata.json"), + JSON.stringify(downloads, null, 2) +); diff --git a/scripts/fetch-repo-docs.ts b/scripts/fetch-repo-docs.ts index 21385f2c..1535da60 100644 --- a/scripts/fetch-repo-docs.ts +++ b/scripts/fetch-repo-docs.ts @@ -1,4 +1,3 @@ -import { Octokit } from "octokit"; import { execSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; @@ -10,16 +9,11 @@ import { RepoDocMetadata, } from "@/docs-collection-types"; import matter from "gray-matter"; -import dotenv from "dotenv"; - -dotenv.config(); +import { octokit } from "./githubClient"; +import { compareFullVersion, filterUnique, majorMinor } from "./utils"; const OUTDIR = "./generated"; -const octokit = new Octokit({ - auth: `${process.env.GITHUB_TOKEN}`, -}); - const docsCollection: DocsCollection = {}; const allRepoVersions: AllRepoVersions = {}; @@ -130,33 +124,14 @@ const fetchRepoDocs = async ({ } } // We still need to sort the releases by version number, as e.g. "2.53.4" - // was released after "3.2.1". - // - // Sorting by major + minor should be sufficient, as patch versions and - // other suffixes should be in the expected order already when sorting by - // release date. - allReleaseTags - .sort((a, b) => { - const [majorA, minorA] = a.replace(/^v/, "").split(".").map(Number); - const [majorB, minorB] = b.replace(/^v/, "").split(".").map(Number); - return majorA === majorB ? minorA - minorB : majorA - majorB; - }) - .reverse(); - - function onlyUnique(value: string, index: number, array: string[]) { - return array.indexOf(value) === index; - } - - // "v3.4.1" -> "3.4" - function shortVersion(version: string) { - return version.replace(/^v/, "").split(".").slice(0, 2).join("."); - } + // was released after "3.2.1".. + allReleaseTags.sort(compareFullVersion).reverse(); // Get all . versions, regardless of the patch version. const allVersions = allReleaseTags .filter((tag) => tag.startsWith("v")) // Ignore prehistoric release tags like "0.1.0" - .map((tag) => shortVersion(tag)) - .filter(onlyUnique); // Remove dupes (e.g. "3.4.0" and "3.4.1" both become "3.4") + .map((tag) => majorMinor(tag)) + .filter(filterUnique); // Remove dupes (e.g. "3.4.0" and "3.4.1" both become "3.4") // First, get the last 10 versions, regardless of the major version. const recentVersions = allVersions.slice(0, minNumVersions); @@ -177,7 +152,7 @@ const fetchRepoDocs = async ({ if (!latestTag) { throw new Error(`No latest version found for ${owner}/${repo}.`); } - const latestVersion = shortVersion(latestTag); + const latestVersion = majorMinor(latestTag); // Store metadata about the repo and its versions. if (!allRepoVersions[owner]) { diff --git a/scripts/githubClient.ts b/scripts/githubClient.ts new file mode 100644 index 00000000..28afba1c --- /dev/null +++ b/scripts/githubClient.ts @@ -0,0 +1,8 @@ +import dotenv from "dotenv"; +import { Octokit } from "octokit"; + +dotenv.config(); + +export const octokit = new Octokit({ + auth: `${process.env.GITHUB_TOKEN}`, +}); diff --git a/scripts/utils.ts b/scripts/utils.ts new file mode 100644 index 00000000..d0b0da2c --- /dev/null +++ b/scripts/utils.ts @@ -0,0 +1,15 @@ +import { compare } from "semver"; + +// Takes a full Prometheus tag / version string and returns the major and minor version. +// "v3.4.0-rc.0" -> "3.4" +export const majorMinor = (version: string) => { + return version.replace(/^v/, "").split(".").slice(0, 2).join("."); +}; + +export const compareFullVersion = (a: string, b: string) => { + return compare(a.replace(/^v/, ""), b.replace(/^v/, "")); +}; + +export function filterUnique(value: string, index: number, array: string[]) { + return array.indexOf(value) === index; +} diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx new file mode 100644 index 00000000..fe4e6e91 --- /dev/null +++ b/src/app/blog/page.tsx @@ -0,0 +1,9 @@ +import { Title } from "@mantine/core"; + +export default function BlogPage() { + return ( + <> + BlogUnder construction - stay tuned. + + ); +} diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx new file mode 100644 index 00000000..005995f6 --- /dev/null +++ b/src/app/community/page.tsx @@ -0,0 +1,9 @@ +import { Title } from "@mantine/core"; + +export default function CommunityPage() { + return ( + <> + CommunityUnder construction - stay tuned. + + ); +} diff --git a/src/app/docs/[...slug]/page.tsx b/src/app/docs/[...slug]/page.tsx index 6e78f672..6ed5d11d 100644 --- a/src/app/docs/[...slug]/page.tsx +++ b/src/app/docs/[...slug]/page.tsx @@ -10,6 +10,7 @@ import rehypeShiki from "@shikijs/rehype"; import { IconLink } from "@tabler/icons-react"; import { docsCollection } from "@/docs-collection"; import { + em, Table, TableTbody, TableTd, @@ -40,8 +41,15 @@ export async function generateStaticParams() { return params; } -const h = (order: 1 | 2 | 3 | 4 | 5 | 6) => (props: TitleProps) => - ; +const h = (order: 1 | 2 | 3 | 4 | 5 | 6) => { + const HeadingComponent = (props: TitleProps) => ( + <Title order={order} id={props.id}> + {props.children} + + ); + HeadingComponent.displayName = `Heading${order}`; + return HeadingComponent; +}; export default async function DocsPage({ params, @@ -56,152 +64,159 @@ export default async function DocsPage({ const markdown = await fs.readFile(docMeta.filePath, "utf-8"); return ( -
- " }, - // Don't link top-level page headings (h1). - test: (el) => el.tagName !== "h1", + + rehypeAutolinkHeadings({ + properties: { + className: ["header-auto-link"], }, - ], - [ - rehypeShiki, - { - // or `theme` for a single theme - themes: { - light: "github-light", - dark: "vitesse-dark", - }, + behavior: "prepend", + // Don't link top-level page headings (h1). + test: (el) => el.tagName !== "h1", + }), + [ + rehypeShiki, + { + // or `theme` for a single theme + themes: { + light: "github-light", + dark: "vitesse-dark", }, - ], - - // Important: this has to run after rehypeSlug, since it - // relies on the headers to already have IDs. - rehypeConfigLinker, - ]} - components={{ - a: (props) => { - if ( - props.node?.children && - props.node.children[0].type === "text" && - props.node.children[0].value === "" - ) { - return ( - - - - ); - } - - const href = props.href; - if (!href || docMeta.type === "local-doc") { - return ; - } - - let normalizedHref = href; - if (href.startsWith(SITE_URL)) { - // Remove the "https://prometheus.io" from links that start with it. - normalizedHref = href.slice(SITE_URL.length); - } else if (href.startsWith("/")) { - // Turn "/" into "https://github.com/prometheus/prometheus/blob/release-3.3.0/" - normalizedHref = `https://github.com/prometheus/prometheus/blob/release-${docMeta.version}${href}`; - } else if (href.includes(".md") && !isAbsoluteUrl(href)) { - // Turn "foo/bar/baz.md" into "../foo/bar/baz" for relative links between Markdown pages. - normalizedHref = `${href.replace(/\.md($|#)/, "$1")}`; - } - - return {props.children}; }, - img: (props) => { - const src = props.src; - if ( - !src || - typeof src !== "string" || - isAbsoluteUrl(src) || - docMeta.type === "local-doc" - ) { - return ; - } - - return ; - }, - pre: (props) => { - const firstChild = props.node?.children[0]; - if ( - !firstChild || - firstChild?.type !== "element" || - firstChild?.tagName !== "code" - ) { - return
{props.children}
; - } + ], + // Important: this has to run after rehypeSlug, since it + // relies on the headers to already have IDs. + rehypeConfigLinker, + ]} + components={{ + a: (props) => { + // Replace header auto-links with a custom link icon. + const { children, node: _node, ...rest } = props; + if (rest.className === "header-auto-link") { return ( -
-                {props.children}
-              
+ + + ); - }, - h1: h(1), - h2: h(2), - h3: h(3), - h4: h(4), - h5: h(5), - h6: h(6), - table: (props) => ( - - {props.children} -
- ), - th: TableTh, - td: TableTd, - tr: TableTr, - thead: TableThead, - tbody: TableTbody, - // For
 tags that contain  tags, we need to extract the content
-          // of the  tag and pass it to a  Mantine component. If it's
-          // a 
 tag that doesn't contain a  tag, we just render it as is.
-          // pre: (props) => {
-          //   const firstChild = props.node?.children[0];
-          //   if (
-          //     firstChild?.type === "element" &&
-          //     firstChild?.tagName === "code"
-          //   ) {
-          //     const contentElement = firstChild.children[0];
-          //     if (contentElement.type !== "text") {
-          //       throw new Error("Code content is not text");
-          //     }
-          //     const content = contentElement.value;
-          //     const language = firstChild.properties?.className?.[0]?.replace(
-          //       "language-",
-          //       ""
-          //     );
+          }
 
-          //     return (
-          //       
-          //     );
-          //   } else {
-          //     return 
;
-          //   }
-          // },
-        }}
-      >
-        {markdown}
-      
-    
+ // For local docs, keep links as is. + const href = props.href; + if (!href || docMeta.type === "local-doc") { + return {children}; + } + + // For external docs, do some postprocessing on the hrefs to make + // sure they point to the right place. + let normalizedHref = href; + if (href.startsWith(SITE_URL)) { + // Remove the "https://prometheus.io" from links that start with it. + normalizedHref = href.slice(SITE_URL.length); + } else if (href.startsWith("/")) { + // Turn "/" into e.g. "https://github.com/prometheus/prometheus/blob/release-3.3.0/" + normalizedHref = `https://github.com/prometheus/prometheus/blob/release-${docMeta.version}${href}`; + } else if (href.includes(".md") && !isAbsoluteUrl(href)) { + // Turn "foo/bar/baz.md" into "foo/bar/baz" for relative links between Markdown pages. + normalizedHref = `${href.replace(/\.md($|#)/, "$1")}`; + } + + return ( + + {children} + + ); + }, + img: (props) => { + const { src, node: _node, ...rest } = props; + if ( + !src || + typeof src !== "string" || + isAbsoluteUrl(src) || + docMeta.type === "local-doc" + ) { + // eslint-disable-next-line jsx-a11y/alt-text + return ; + } + + // eslint-disable-next-line jsx-a11y/alt-text + return ; + }, + pre: (props) => { + const firstChild = props.node?.children[0]; + if ( + !firstChild || + firstChild?.type !== "element" || + firstChild?.tagName !== "code" + ) { + return
{props.children}
; + } + + return ( +
+              {props.children}
+            
+ ); + }, + h1: h(1), + h2: h(2), + h3: h(3), + h4: h(4), + h5: h(5), + h6: h(6), + table: (props) => ( + + {props.children} +
+ ), + th: TableTh, + td: TableTd, + tr: TableTr, + thead: TableThead, + tbody: TableTbody, + // For
 tags that contain  tags, we need to extract the content
+        // of the  tag and pass it to a  Mantine component. If it's
+        // a 
 tag that doesn't contain a  tag, we just render it as is.
+        // pre: (props) => {
+        //   const firstChild = props.node?.children[0];
+        //   if (
+        //     firstChild?.type === "element" &&
+        //     firstChild?.tagName === "code"
+        //   ) {
+        //     const contentElement = firstChild.children[0];
+        //     if (contentElement.type !== "text") {
+        //       throw new Error("Code content is not text");
+        //     }
+        //     const content = contentElement.value;
+        //     const language = firstChild.properties?.className?.[0]?.replace(
+        //       "language-",
+        //       ""
+        //     );
+
+        //     return (
+        //       
+        //     );
+        //   } else {
+        //     return 
;
+        //   }
+        // },
+      }}
+    >
+      {markdown}
+    
   );
 }
diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx
index 44589f4c..c0a8b2d2 100644
--- a/src/app/docs/layout.tsx
+++ b/src/app/docs/layout.tsx
@@ -7,15 +7,15 @@ import {
   RepoDocMetadata,
 } from "@/docs-collection-types";
 import {
-  Container,
   Group,
   Box,
-  List,
-  ListItem,
-  Text,
   Select,
   NavLink,
   Alert,
+  ScrollAreaAutosize,
+  Button,
+  Popover,
+  Text,
 } from "@mantine/core";
 import Link from "next/link";
 import { usePathname, useRouter } from "next/navigation";
@@ -33,12 +33,13 @@ import {
   IconFileDescription,
   IconProps,
   IconInfoCircle,
-  IconVersions,
   IconTag,
+  IconMenu2,
 } from "@tabler/icons-react";
-import { ReactElement } from "react";
+import { ReactElement, useEffect, useRef } from "react";
+import TOC from "@/components/TOC";
 
-const iconMap: Record> = {
+const iconMap: Record> = {
   flask: IconFlask,
   server: IconServer,
   code: IconCode,
@@ -52,10 +53,7 @@ const iconMap: Record> = {
   "file-description": IconFileDescription,
 };
 
-export function NavIcon({
-  iconName,
-  ...props
-}: { iconName: string } & IconProps) {
+function NavIcon({ iconName, ...props }: { iconName: string } & IconProps) {
   const Icon = iconMap[iconName];
   return Icon ? (
     
@@ -203,9 +201,11 @@ function buildRecursiveNav(
 
         const navIcon =
           node.document.type === "local-doc" && node.document.navIcon;
+        const active = currentPageSlug.startsWith(node.path);
 
         return (
           
             {level === 0 && repoVersions && (
                {
+              console.log(value);
+              setOs(value || "all");
+            }}
+          />
+