diff --git a/assets/images/site/evergreens/balsam.png b/assets/images/site/evergreens/balsam.png index 88680dede3d7..9683d53b3a19 100644 Binary files a/assets/images/site/evergreens/balsam.png and b/assets/images/site/evergreens/balsam.png differ diff --git a/assets/images/site/evergreens/boxwood.png b/assets/images/site/evergreens/boxwood.png index 9640fac31337..0deaa1e38c32 100644 Binary files a/assets/images/site/evergreens/boxwood.png and b/assets/images/site/evergreens/boxwood.png differ diff --git a/assets/images/site/evergreens/cedar.png b/assets/images/site/evergreens/cedar.png index 82958f74587a..feae56d06a3e 100644 Binary files a/assets/images/site/evergreens/cedar.png and b/assets/images/site/evergreens/cedar.png differ diff --git a/assets/images/site/evergreens/cypress.png b/assets/images/site/evergreens/cypress.png index 05841a7cc6ce..01776d475f3e 100644 Binary files a/assets/images/site/evergreens/cypress.png and b/assets/images/site/evergreens/cypress.png differ diff --git a/assets/images/site/evergreens/fir.png b/assets/images/site/evergreens/fir.png index df8c79376eea..ecca2f93d9d6 100644 Binary files a/assets/images/site/evergreens/fir.png and b/assets/images/site/evergreens/fir.png differ diff --git a/assets/images/site/evergreens/hemlock.png b/assets/images/site/evergreens/hemlock.png index 2287b406d681..1ee152c3307c 100644 Binary files a/assets/images/site/evergreens/hemlock.png and b/assets/images/site/evergreens/hemlock.png differ diff --git a/assets/images/site/evergreens/hinoki.png b/assets/images/site/evergreens/hinoki.png index 69ac7584ca58..f05c61be7ffe 100644 Binary files a/assets/images/site/evergreens/hinoki.png and b/assets/images/site/evergreens/hinoki.png differ diff --git a/assets/images/site/evergreens/holly.png b/assets/images/site/evergreens/holly.png index 497eb92e8883..365452958325 100644 Binary files a/assets/images/site/evergreens/holly.png and b/assets/images/site/evergreens/holly.png differ diff --git a/assets/images/site/evergreens/juniper.png b/assets/images/site/evergreens/juniper.png index 37e9404c4cc5..410af5cc2f53 100644 Binary files a/assets/images/site/evergreens/juniper.png and b/assets/images/site/evergreens/juniper.png differ diff --git a/assets/images/site/evergreens/laurel.png b/assets/images/site/evergreens/laurel.png index 4516472de094..d005f8595b3d 100644 Binary files a/assets/images/site/evergreens/laurel.png and b/assets/images/site/evergreens/laurel.png differ diff --git a/assets/images/site/evergreens/pine.png b/assets/images/site/evergreens/pine.png index d6b98e73d6ea..a58eb67ab557 100644 Binary files a/assets/images/site/evergreens/pine.png and b/assets/images/site/evergreens/pine.png differ diff --git a/assets/images/site/evergreens/redwood.png b/assets/images/site/evergreens/redwood.png index 08d75c7c9304..4e1d12d24cf0 100644 Binary files a/assets/images/site/evergreens/redwood.png and b/assets/images/site/evergreens/redwood.png differ diff --git a/assets/images/site/evergreens/sequoia.png b/assets/images/site/evergreens/sequoia.png index 08453acb97a7..d4db23230c79 100644 Binary files a/assets/images/site/evergreens/sequoia.png and b/assets/images/site/evergreens/sequoia.png differ diff --git a/assets/images/site/evergreens/spruce.png b/assets/images/site/evergreens/spruce.png index c104f49b5ccb..77dc98a4827b 100644 Binary files a/assets/images/site/evergreens/spruce.png and b/assets/images/site/evergreens/spruce.png differ diff --git a/assets/images/site/evergreens/yew.png b/assets/images/site/evergreens/yew.png index 10e06807efbc..96049edf72af 100644 Binary files a/assets/images/site/evergreens/yew.png and b/assets/images/site/evergreens/yew.png differ diff --git a/data/reusables/organizations/repository-defaults.md b/data/reusables/organizations/repository-defaults.md index 6bc1c5a6f064..04e574e30151 100644 --- a/data/reusables/organizations/repository-defaults.md +++ b/data/reusables/organizations/repository-defaults.md @@ -1 +1 @@ -1. {% data reusables.user-settings.code-planning-automation %} select **{% octicon "repo" aria-hidden="true" aria-label="repo" %} Repository**, then click **Repository defaults**. +1. {% data reusables.user-settings.code-planning-automation %} select **{% octicon "repo" aria-hidden="true" aria-label="repo" %} Repository**, then click **General**. diff --git a/package-lock.json b/package-lock.json index d7e61b307e2b..2408c2b61588 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,6 @@ "@github/hydro-analytics-client": "^2.4.1", "@gr2m/gray-matter": "4.0.3-with-pr-137", "@horizon-rs/language-guesser": "0.1.1", - "@octokit/graphql": "9.0.1", - "@octokit/plugin-retry": "8.0.1", - "@octokit/request-error": "7.0.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.8.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", @@ -36,22 +33,16 @@ "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", "azure-kusto-data": "^7.0.4", - "bottleneck": "2.19.5", - "boxen": "8.0.1", + "chalk": "^5.0.1", "cheerio": "^1.2.0", - "classnames": "^2.5.1", "clsx": "^2.1.1", "cookie-parser": "^1.4.7", "cuss": "2.2.0", "dayjs": "^1.11.19", "dotenv": "^17.3.1", - "escape-string-regexp": "5.0.0", "express": "^5.2.1", "fastest-levenshtein": "1.0.16", - "file-type": "22.0.0", - "flat": "^6.0.1", "github-slugger": "^2.0.0", - "glob": "13.0.2", "hast-util-from-html": "^2.0.3", "hast-util-from-parse5": "^8.0.3", "hast-util-to-jsx-runtime": "^2.3.6", @@ -64,24 +55,17 @@ "html-entities": "^2.5.6", "http-proxy-middleware": "^4.1.1", "imurmurhash": "^0.1.4", - "is-svg": "6.0.0", "javascript-stringify": "^2.1.0", "js-cookie": "^3.0.7", "js-yaml": "^5.2.2", "liquidjs": "^10.27.0", - "lodash": "^4.18.0", "lodash-es": "^4.18.0", - "lowdb": "7.0.1", "lowlight": "^3.3.0", - "markdownlint-rule-helpers": "^0.25.0", "mdast-util-from-markdown": "^2.0.3", - "mdast-util-gfm": "^3.1.0", "mdast-util-to-hast": "^13.2.1", "mdast-util-to-markdown": "2.1.2", "mdast-util-to-string": "^4.0.0", - "micromark-extension-gfm": "^3.0.0", "next": "^16.3.3", - "ora": "^9.3.0", "parse5": "8.0.1", "quick-lru": "7.0.1", "react": "^19.2.5", @@ -91,7 +75,7 @@ "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", - "remark-gemoji-to-emoji": "^1.1.0", + "remark-gemoji": "^8.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", @@ -104,10 +88,10 @@ "swr": "^2.4.0", "tcp-port-used": "1.0.2", "tsx": "^4.21.0", - "typescript": "^6.0.2", "unified": "^11.0.5", "unist-util-find": "^3.0.0", "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "6.0.1", "url-template": "^3.1.1", "walk-sync": "^4.0.1" }, @@ -119,6 +103,9 @@ "@github/markdownlint-github": "^0.6.3", "@graphql-inspector/core": "^8.0.0", "@graphql-tools/load": "^8.1.8", + "@octokit/graphql": "9.0.1", + "@octokit/plugin-retry": "8.0.1", + "@octokit/request-error": "7.0.0", "@octokit/rest": "22.0.0", "@playwright/test": "^1.60.0", "@types/accept-language-parser": "1.5.7", @@ -126,6 +113,7 @@ "@types/cookie-parser": "1.4.8", "@types/eslint-plugin-jsx-a11y": "^6.10.1", "@types/express": "^5.0.6", + "@types/hast": "^3.0.4", "@types/imurmurhash": "^0.1.4", "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", @@ -136,15 +124,18 @@ "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "@types/tcp-port-used": "1.0.4", + "@types/unist": "^3.0.3", "@types/website-scraper": "^1.2.11", "@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/parser": "^8.33.1", - "chalk": "^5.0.1", + "bottleneck": "2.19.5", + "boxen": "8.0.1", "change-case": "^5.4.4", "commander": "^15.0.0", "cross-env": "^10.1.0", - "csp-parse": "0.0.2", "csv-parse": "7.0.0", + "domhandler": "^5.0.3", + "escape-string-regexp": "5.0.0", "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", @@ -158,29 +149,36 @@ "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-primer-react": "^9.0.0", + "file-type": "22.0.0", + "flat": "^6.0.1", "globals": "^17.3.0", "gpt-tokenizer": "^4.0.0", "graphql": "^16.12.0", - "http-status-code": "^2.1.0", "husky": "^9.1.7", + "is-svg": "6.0.0", "jiti": "^2.6.1", "json-schema-merge-allof": "^0.8.1", "lint-staged": "^17.0.4", + "lowdb": "7.0.1", "markdownlint": "^0.34.0", + "markdownlint-rule-helpers": "^0.25.0", "markdownlint-rule-search-replace": "^1.2.0", + "mdast-util-gfm": "^3.1.0", + "micromark-extension-gfm": "^3.0.0", "mkdirp": "^3.0.1", "mockdate": "^3.0.5", "nock": "^14.0.11", "nodemon": "3.1.10", + "ora": "^9.3.0", "patch-package": "^8.0.1", "prettier": "^3.8.1", "rimraf": "^6.1.3", "sass": "^1.97.3", "start-server-and-test": "^3.0.0", + "typescript": "^6.0.2", "unist-util-remove": "^4.0.0", - "unist-util-visit-parents": "6.0.1", "vitest": "^5.0.0", - "website-scraper": "^5.3.1" + "website-scraper": "^6.0.0" }, "engines": { "node": "^24 || ^26" @@ -481,6 +479,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -1249,6 +1248,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@file-type/xml/-/xml-0.4.3.tgz", "integrity": "sha512-pGRmkHf+NofNy/52r06HOTsEwdNnBsFEhN6U95s33P+ezuoxZEyBTV9lOB1/Zr0So6/9vDVfWZXLpgd0fy8cOQ==", + "dev": true, "dependencies": { "sax": "^1.4.1", "strtok3": "^10.2.2" @@ -2263,6 +2263,13 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@lit-labs/react": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.2.1.tgz", @@ -2476,6 +2483,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, "engines": { "node": ">= 20" } @@ -2484,6 +2492,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/auth-token": "^6.0.0", @@ -2502,6 +2511,7 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/request": "^10.0.6", @@ -2516,6 +2526,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^16.0.0" @@ -2528,6 +2539,7 @@ "version": "11.0.2", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz", "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^16.0.0", @@ -2541,6 +2553,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/request": "^10.0.2", @@ -2554,12 +2567,14 @@ "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==" + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "dev": true }, "node_modules/@octokit/graphql/node_modules/@octokit/types": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "dev": true, "dependencies": { "@octokit/openapi-types": "^25.0.0" } @@ -2568,6 +2583,7 @@ "version": "27.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { @@ -2646,6 +2662,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.1.tgz", "integrity": "sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==", + "dev": true, "dependencies": { "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", @@ -2661,12 +2678,14 @@ "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==" + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "dev": true }, "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "dev": true, "dependencies": { "@octokit/openapi-types": "^25.0.0" } @@ -2675,6 +2694,7 @@ "version": "10.0.7", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz", "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/endpoint": "^11.0.2", @@ -2691,6 +2711,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "dev": true, "dependencies": { "@octokit/types": "^14.0.0" }, @@ -2702,12 +2723,14 @@ "version": "25.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "dev": true, "license": "MIT" }, "node_modules/@octokit/request-error/node_modules/@octokit/types": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/openapi-types": "^25.0.0" @@ -2717,6 +2740,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^16.0.0" @@ -2744,6 +2768,7 @@ "version": "16.0.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, "license": "MIT", "dependencies": { "@octokit/openapi-types": "^27.0.0" @@ -4477,12 +4502,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { - "version": "5.3.0", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" @@ -4510,17 +4544,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, "node_modules/@tanstack/react-virtual": { "version": "3.14.5", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.5.tgz", @@ -4552,6 +4575,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -4568,7 +4592,8 @@ "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", @@ -4722,10 +4747,11 @@ } }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" }, "node_modules/@types/http-errors": { "version": "2.0.5", @@ -4940,7 +4966,9 @@ "dev": true }, "node_modules/@types/unist": { - "version": "2.0.6", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, "node_modules/@types/uuid": { @@ -5676,6 +5704,7 @@ }, "node_modules/ansi-align": { "version": "3.0.1", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.1.0" @@ -6024,7 +6053,8 @@ "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==" + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -6103,12 +6133,14 @@ }, "node_modules/bottleneck": { "version": "2.19.5", + "dev": true, "license": "MIT" }, "node_modules/boxen": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "dev": true, "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", @@ -6129,12 +6161,14 @@ "node_modules/boxen/node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true }, "node_modules/boxen/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -6237,6 +6271,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6248,6 +6295,8 @@ }, "node_modules/cacheable-lookup": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", "dev": true, "license": "MIT", "engines": { @@ -6255,20 +6304,62 @@ } }, "node_modules/cacheable-request": { - "version": "10.2.10", + "version": "13.0.19", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.19.tgz", + "integrity": "sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.2", + "@types/http-cache-semantics": "^4.2.0", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.6.0", "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" }, "engines": { - "node": ">=14.16" + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" } }, "node_modules/call-bind": { @@ -6333,6 +6424,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "dev": true, "engines": { "node": ">=16" }, @@ -6548,13 +6640,9 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/cli-boxes": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -6567,6 +6655,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" @@ -6582,6 +6671,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.2.0.tgz", "integrity": "sha512-pXftdQloMZzjCr3pCTIRniDcys6dDzgpgVhAHHk6TKBDbRuP1MkuetTF5KSv4YUutbOPa7+7ZrAJ2kVtbMqyXA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.20" @@ -6895,11 +6985,6 @@ "node": ">= 8" } }, - "node_modules/csp-parse": { - "version": "0.0.2", - "dev": true, - "license": "ISC" - }, "node_modules/css-select": { "version": "5.1.0", "license": "BSD-2-Clause", @@ -7066,25 +7151,16 @@ } }, "node_modules/decompress-response": { - "version": "6.0.0", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", "dev": true, "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" + "mimic-response": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7129,14 +7205,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -8442,7 +8510,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, @@ -8580,6 +8650,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "dev": true, "funding": [ { "type": "github", @@ -8653,6 +8724,7 @@ "version": "22.0.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.0.tgz", "integrity": "sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw==", + "dev": true, "license": "MIT", "dependencies": { "@tokenizer/inflate": "^0.4.1", @@ -8727,6 +8799,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", "integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==", + "dev": true, "bin": { "flat": "cli.js" }, @@ -8814,11 +8887,13 @@ } }, "node_modules/form-data-encoder": { - "version": "2.1.4", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.17" + "node": ">= 18" } }, "node_modules/forwarded": { @@ -8909,8 +8984,14 @@ } }, "node_modules/gemoji": { - "version": "4.2.1", - "license": "MIT" + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/gemoji/-/gemoji-8.1.0.tgz", + "integrity": "sha512-HA4Gx59dw2+tn+UAa7XEV4ufUKI4fH1KgcbenVA9YKSj1QJTT0xh5Mwv5HMFNN3l2OtUe3ZIfuRwSyZS5pLIWw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -8925,6 +9006,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9015,23 +9097,6 @@ "version": "2.0.0", "license": "ISC" }, - "node_modules/glob": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.2.tgz", - "integrity": "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "dev": true, @@ -9085,6 +9150,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", + "integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/gpt-tokenizer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-4.0.0.tgz", @@ -9244,11 +9346,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-from-parse5/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/hast-util-from-parse5/node_modules/property-information": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", @@ -9319,11 +9416,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-raw/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/hast-util-raw/node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -9359,11 +9451,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-html/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -9391,12 +9478,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/hast-util-to-jsx-runtime/node_modules/property-information": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", @@ -9452,11 +9533,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-text/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -9625,7 +9701,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, "license": "BSD-2-Clause" }, @@ -9677,30 +9755,12 @@ "node": "^22.15.0 || ^24.0.0 || >=26.0.0" } }, - "node_modules/http-status-code": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "strip-json-comments": "^1.0.2" - } - }, - "node_modules/http-status-code/node_modules/strip-json-comments": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "bin": { - "strip-json-comments": "cli.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dev": true, + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -9714,6 +9774,7 @@ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9788,6 +9849,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -10204,6 +10266,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, "engines": { "node": ">=12" }, @@ -10365,6 +10428,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-6.0.0.tgz", "integrity": "sha512-dGyV+26JXUyoWF8Su3LfFS9tcmrR/uYYSyABE66iv4zJnS77y8T3PZB0nWRKOJJiU1lelGMGuYMylqrFu8D7yQ==", + "dev": true, "dependencies": { "@file-type/xml": "^0.4.3" }, @@ -10413,6 +10477,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10893,13 +10958,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, "node_modules/listr2/node_modules/string-width": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", @@ -10955,6 +11013,7 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, "license": "MIT" }, "node_modules/lodash-es": { @@ -11049,6 +11108,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0", @@ -11145,6 +11205,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", + "dev": true, "dependencies": { "steno": "^4.0.2" }, @@ -11157,6 +11218,8 @@ }, "node_modules/lowercase-keys": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", "dev": true, "license": "MIT", "engines": { @@ -11185,6 +11248,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true, "engines": { "node": "20 || >=22" } @@ -11267,6 +11331,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.2.tgz", "integrity": "sha512-jRxlQg8KpOfM2IbCL9RXM8ZiYWz2rv6DlZAnGv8ASJQpUh6byTBnEsbuMZ6T2/uIgntyf7SKg/mEaEBo1164fQ==", + "dev": true, "engines": { "node": ">=14.18.0" } @@ -11275,6 +11340,7 @@ "version": "0.25.0", "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.25.0.tgz", "integrity": "sha512-2vH6ee3UCPUuKVOxrinHxCT4bLjpCLRdtzTu4UO7Qb4Gl2vlGb70Un8byDv8dyJ8ZqZtxANd2XxvAumoG2nYcw==", + "dev": true, "dependencies": { "markdownlint-micromark": "0.1.2" }, @@ -11378,11 +11444,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-find-and-replace/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/mdast-util-find-and-replace/node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -11419,11 +11480,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-from-markdown/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/mdast-util-gfm": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", @@ -11562,12 +11618,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/mdast-util-mdxjs-esm": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", @@ -11599,11 +11649,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-phrasing/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/mdast-util-phrasing/node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -11658,11 +11703,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/mdast-util-to-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", @@ -12290,6 +12330,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12300,6 +12341,8 @@ }, "node_modules/mimic-response": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", "dev": true, "license": "MIT", "engines": { @@ -12336,6 +12379,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -12603,10 +12647,11 @@ } }, "node_modules/normalize-url": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", - "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -12808,6 +12853,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.6.2", @@ -12830,6 +12876,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, "license": "MIT", "dependencies": { "get-east-asian-width": "^1.3.0", @@ -12868,11 +12915,13 @@ } }, "node_modules/p-cancelable": { - "version": "3.0.0", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.20" + "node": ">=14.16" } }, "node_modules/p-limit": { @@ -12890,26 +12939,30 @@ } }, "node_modules/p-queue": { - "version": "7.3.4", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", "dev": true, "license": "MIT", "dependencies": { - "eventemitter3": "^4.0.7", - "p-timeout": "^5.0.2" + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-timeout": { - "version": "5.1.0", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -12954,6 +13007,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -13167,6 +13226,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -13663,30 +13723,19 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/remark-gemoji-to-emoji": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "gemoji": "^4.0.0", - "unist-util-visit": "^1.0.0" - } - }, - "node_modules/remark-gemoji-to-emoji/node_modules/unist-util-is": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/remark-gemoji-to-emoji/node_modules/unist-util-visit": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "node_modules/remark-gemoji-to-emoji/node_modules/unist-util-visit-parents": { - "version": "2.1.2", + "node_modules/remark-gemoji": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/remark-gemoji/-/remark-gemoji-8.0.0.tgz", + "integrity": "sha512-/fL9rc72FYwFGtOKcT+QeQdx9Q9t5v4N6KLXSDOTEgaedzK85I9judBqB2eqz+g4b0ERMejlwSOuPK+wket6aA==", "license": "MIT", "dependencies": { - "unist-util-is": "^3.0.0" + "@types/mdast": "^4.0.0", + "gemoji": "^8.0.0", + "mdast-util-find-and-replace": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/remark-gfm": { @@ -13817,6 +13866,8 @@ }, "node_modules/resolve-alpn": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true, "license": "MIT" }, @@ -13839,14 +13890,16 @@ } }, "node_modules/responselike": { - "version": "3.0.0", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", "dev": true, "license": "MIT", "dependencies": { "lowercase-keys": "^3.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -13856,6 +13909,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, "license": "MIT", "dependencies": { "onetime": "^7.0.0", @@ -13872,6 +13926,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" @@ -13887,6 +13942,7 @@ "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" @@ -14151,7 +14207,8 @@ "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true }, "node_modules/scheduler": { "version": "0.27.0", @@ -14657,6 +14714,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -14669,6 +14727,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", + "dev": true, "engines": { "node": ">=18" }, @@ -14922,6 +14981,7 @@ "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0" @@ -15179,6 +15239,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, "license": "MIT", "dependencies": { "@borewit/text-codec": "^0.2.1", @@ -15339,6 +15400,7 @@ "version": "4.26.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, "engines": { "node": ">=16" }, @@ -15480,6 +15542,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -15524,6 +15587,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -15590,11 +15654,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unified/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/unist-util-find": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unist-util-find/-/unist-util-find-3.0.0.tgz", @@ -15622,11 +15681,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-find-after/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/unist-util-find-after/node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -15639,12 +15693,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-find/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/unist-util-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", @@ -15657,11 +15705,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-position/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/unist-util-remove": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", @@ -15677,12 +15720,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-remove/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==", - "dev": true - }, "node_modules/unist-util-remove/node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -15709,12 +15746,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-stringify-position/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/unist-util-visit": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", @@ -15743,11 +15774,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-visit-parents/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -15760,11 +15786,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-visit/node_modules/@types/unist": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", - "integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==" - }, "node_modules/unist-util-visit/node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -15780,7 +15801,8 @@ "node_modules/universal-user-agent": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true }, "node_modules/universalify": { "version": "2.0.0", @@ -15998,6 +16020,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/vfile-message": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", @@ -16012,18 +16040,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/vfile-message/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/vfile/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -16330,139 +16346,39 @@ } }, "node_modules/website-scraper": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/website-scraper/-/website-scraper-5.3.1.tgz", - "integrity": "sha512-gogqPXD2gVsxoyd2yRiympw3rA5GuEpD1CaDEJ/J8zzanx7hkbTtneoO1SGs436PpLbWVcUge+6APGLhzsuZPA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/website-scraper/-/website-scraper-6.0.0.tgz", + "integrity": "sha512-woNX2xyjPTBGpxvvtxJg20p1K2ScwlIC9zHnJAtSmUqnTb1bQebSreHYaph7v1VEz+V43MdENelMCN8iHxRypA==", "dev": true, + "license": "MIT", "dependencies": { - "cheerio": "1.0.0-rc.12", + "cheerio": "^1.1.2", "css-url-parser": "^1.0.0", "debug": "^4.3.1", - "fs-extra": "^10.0.0", - "got": "^12.0.0", - "normalize-url": "^7.0.2", - "p-queue": "^7.1.0", + "fs-extra": "^11.1.0", + "got": "^14.4.7", + "normalize-url": "^8.0.0", + "p-queue": "^9.0.0", "sanitize-filename": "^1.6.3", "srcset": "^5.0.0" }, "engines": { - "node": ">=14.14" - } - }, - "node_modules/website-scraper/node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/website-scraper/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/website-scraper/node_modules/got": { - "version": "12.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/website-scraper/node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/website-scraper/node_modules/normalize-url": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=20.18.1" } }, - "node_modules/website-scraper/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "node_modules/website-scraper/node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/website-scraper/node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=14.14" } }, "node_modules/whatwg-encoding": { @@ -16622,6 +16538,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "dev": true, "dependencies": { "string-width": "^7.0.0" }, @@ -16635,12 +16552,14 @@ "node_modules/widest-line/node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true }, "node_modules/widest-line/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -16657,6 +16576,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -16673,6 +16593,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "engines": { "node": ">=12" }, @@ -16683,12 +16604,14 @@ "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -16779,6 +16702,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 995fc6ee7dec..4d3640682b4b 100644 --- a/package.json +++ b/package.json @@ -175,9 +175,6 @@ "@github/hydro-analytics-client": "^2.4.1", "@gr2m/gray-matter": "4.0.3-with-pr-137", "@horizon-rs/language-guesser": "0.1.1", - "@octokit/graphql": "9.0.1", - "@octokit/plugin-retry": "8.0.1", - "@octokit/request-error": "7.0.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.8.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", @@ -198,22 +195,16 @@ "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", "azure-kusto-data": "^7.0.4", - "bottleneck": "2.19.5", - "boxen": "8.0.1", + "chalk": "^5.0.1", "cheerio": "^1.2.0", - "classnames": "^2.5.1", "clsx": "^2.1.1", "cookie-parser": "^1.4.7", "cuss": "2.2.0", "dayjs": "^1.11.19", "dotenv": "^17.3.1", - "escape-string-regexp": "5.0.0", "express": "^5.2.1", "fastest-levenshtein": "1.0.16", - "file-type": "22.0.0", - "flat": "^6.0.1", "github-slugger": "^2.0.0", - "glob": "13.0.2", "hast-util-from-html": "^2.0.3", "hast-util-from-parse5": "^8.0.3", "hast-util-to-jsx-runtime": "^2.3.6", @@ -226,24 +217,17 @@ "html-entities": "^2.5.6", "http-proxy-middleware": "^4.1.1", "imurmurhash": "^0.1.4", - "is-svg": "6.0.0", "javascript-stringify": "^2.1.0", "js-cookie": "^3.0.7", "js-yaml": "^5.2.2", "liquidjs": "^10.27.0", - "lodash": "^4.18.0", "lodash-es": "^4.18.0", - "lowdb": "7.0.1", "lowlight": "^3.3.0", - "markdownlint-rule-helpers": "^0.25.0", "mdast-util-from-markdown": "^2.0.3", - "mdast-util-gfm": "^3.1.0", "mdast-util-to-hast": "^13.2.1", "mdast-util-to-markdown": "2.1.2", "mdast-util-to-string": "^4.0.0", - "micromark-extension-gfm": "^3.0.0", "next": "^16.3.3", - "ora": "^9.3.0", "parse5": "8.0.1", "quick-lru": "7.0.1", "react": "^19.2.5", @@ -253,7 +237,7 @@ "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", - "remark-gemoji-to-emoji": "^1.1.0", + "remark-gemoji": "^8.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", @@ -266,10 +250,10 @@ "swr": "^2.4.0", "tcp-port-used": "1.0.2", "tsx": "^4.21.0", - "typescript": "^6.0.2", "unified": "^11.0.5", "unist-util-find": "^3.0.0", "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "6.0.1", "url-template": "^3.1.1", "walk-sync": "^4.0.1" }, @@ -281,6 +265,9 @@ "@github/markdownlint-github": "^0.6.3", "@graphql-inspector/core": "^8.0.0", "@graphql-tools/load": "^8.1.8", + "@octokit/graphql": "9.0.1", + "@octokit/plugin-retry": "8.0.1", + "@octokit/request-error": "7.0.0", "@octokit/rest": "22.0.0", "@playwright/test": "^1.60.0", "@types/accept-language-parser": "1.5.7", @@ -288,6 +275,7 @@ "@types/cookie-parser": "1.4.8", "@types/eslint-plugin-jsx-a11y": "^6.10.1", "@types/express": "^5.0.6", + "@types/hast": "^3.0.4", "@types/imurmurhash": "^0.1.4", "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", @@ -298,15 +286,18 @@ "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "@types/tcp-port-used": "1.0.4", + "@types/unist": "^3.0.3", "@types/website-scraper": "^1.2.11", "@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/parser": "^8.33.1", - "chalk": "^5.0.1", + "bottleneck": "2.19.5", + "boxen": "8.0.1", "change-case": "^5.4.4", "commander": "^15.0.0", "cross-env": "^10.1.0", - "csp-parse": "0.0.2", "csv-parse": "7.0.0", + "domhandler": "^5.0.3", + "escape-string-regexp": "5.0.0", "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", @@ -320,29 +311,36 @@ "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-primer-react": "^9.0.0", + "file-type": "22.0.0", + "flat": "^6.0.1", "globals": "^17.3.0", "gpt-tokenizer": "^4.0.0", "graphql": "^16.12.0", - "http-status-code": "^2.1.0", "husky": "^9.1.7", + "is-svg": "6.0.0", "jiti": "^2.6.1", "json-schema-merge-allof": "^0.8.1", "lint-staged": "^17.0.4", + "lowdb": "7.0.1", "markdownlint": "^0.34.0", + "markdownlint-rule-helpers": "^0.25.0", "markdownlint-rule-search-replace": "^1.2.0", + "mdast-util-gfm": "^3.1.0", + "micromark-extension-gfm": "^3.0.0", "mkdirp": "^3.0.1", "mockdate": "^3.0.5", "nock": "^14.0.11", "nodemon": "3.1.10", + "ora": "^9.3.0", "patch-package": "^8.0.1", "prettier": "^3.8.1", "rimraf": "^6.1.3", "sass": "^1.97.3", "start-server-and-test": "^3.0.0", + "typescript": "^6.0.2", "unist-util-remove": "^4.0.0", - "unist-util-visit-parents": "6.0.1", "vitest": "^5.0.0", - "website-scraper": "^5.3.1" + "website-scraper": "^6.0.0" }, "overrides": { "esbuild": "^0.28.0", diff --git a/src/article-api/lib/strip-html-comments.ts b/src/article-api/lib/strip-html-comments.ts index 1f15e67fc084..43342f4b881d 100644 --- a/src/article-api/lib/strip-html-comments.ts +++ b/src/article-api/lib/strip-html-comments.ts @@ -1,6 +1,19 @@ -// Removes single-line HTML comments such as . +// HTML also closes a comment with --!>, and treats and as empty comments. +// An unclosed /g, '').trim() + let previous + do { + previous = content + content = content.replace(HTML_COMMENT, '') + } while (content !== previous) + + return content.trim() } // Strips HTML comments, then collapses the blank lines their removal leaves behind. diff --git a/src/article-api/tests/secret-scanning-transformer.ts b/src/article-api/tests/secret-scanning-transformer.ts index c5daac87568e..45c8d23eb9f4 100644 --- a/src/article-api/tests/secret-scanning-transformer.ts +++ b/src/article-api/tests/secret-scanning-transformer.ts @@ -16,7 +16,8 @@ describe('secret scanning article body api', () => { expect(res.body).toContain('# Supported secret scanning patterns') expect(res.body).toContain('## Supported secrets') - expect(res.body).not.toMatch(//) + // A substring check also catches multiline, empty, and unterminated comments. + expect(res.body).not.toContain('\nAfter' + const expected = 'Before\n\nAfter' + expect(stripHtmlComments(input)).toBe(expected) + }) + + test('removes the abrupt-close comment forms', () => { + expect(stripHtmlComments('ab')).toBe('ab') + expect(stripHtmlComments('ab')).toBe('ab') + }) + + test('does not leave a comment behind when removal splices one together', () => { + const input = '- still a comment -->' + expect(stripHtmlComments(input)).toBe('') + }) + + test('ends a comment at the first closing marker', () => { + const input = '-->' + expect(stripHtmlComments(input)).toBe('-->') + }) + + test('keeps the remaining text when a comment is never closed', () => { + const input = 'Keep me --tail' + expect(stripHtmlComments(input)).toBe('c')).toBe('ac') + expect(stripHtmlComments('ac')).toBe('ac') + expect(stripHtmlComments(`a as a comment terminator', () => { + const input = 'beforeVISIBLEafter' + expect(stripHtmlComments(input)).toBe('beforeVISIBLEafter') + }) + + test('strips nesting deep enough to need many passes', () => { + // Each layer becomes a fresh comment when the inner one goes, forcing one pass per layer. + let input = '' + for (let i = 1; i < 12; i++) { + input = `` + } + + expect(stripHtmlComments(input)).toBe('') + }) }) describe('stripHtmlCommentsAndNormalizeWhitespace', () => { diff --git a/src/article-api/transformers/github-apps-transformer.ts b/src/article-api/transformers/github-apps-transformer.ts index bcc49cb9eae7..c93fa67efab0 100644 --- a/src/article-api/transformers/github-apps-transformer.ts +++ b/src/article-api/transformers/github-apps-transformer.ts @@ -2,6 +2,7 @@ import type { Context, Page } from '@/types' import type { PageTransformer } from './types' import { renderContent } from '@/content-render/index' import { loadTemplate } from '@/article-api/lib/load-template' +import { stripHtmlCommentsAndNormalizeWhitespace } from '@/article-api/lib/strip-html-comments' import matter from '@gr2m/gray-matter' const DEBUG = process.env.RUNNER_DEBUG === '1' || process.env.DEBUG === '1' @@ -133,10 +134,7 @@ export class GithubAppsTransformer implements PageTransformer { markdownRequested: true, }) - manualContent = manualContent - .replace(//gs, '') - .replace(/\n{3,}/g, '\n\n') - .trim() + manualContent = stripHtmlCommentsAndNormalizeWhitespace(manualContent) } } diff --git a/src/article-api/transformers/secret-scanning-transformer.ts b/src/article-api/transformers/secret-scanning-transformer.ts index e1ecaeed4bbf..2b1d57525720 100644 --- a/src/article-api/transformers/secret-scanning-transformer.ts +++ b/src/article-api/transformers/secret-scanning-transformer.ts @@ -5,6 +5,7 @@ import path from 'path' import { liquid } from '@/content-render/index' import { allVersions } from '@/versions/lib/all-versions' import { loadTemplate } from '@/article-api/lib/load-template' +import { stripHtmlComments } from '@/article-api/lib/strip-html-comments' import { getSecretScanningData } from '@/secret-scanning/lib/get-secret-scanning-data' /** @@ -101,7 +102,7 @@ export class SecretScanningTransformer implements PageTransformer { content += table } - content = content.replace(//gs, '') + content = stripHtmlComments(content) content = content.replace(/]*aria-label="Supported"[^>]*>[^<]*<\/span>/g, '✓') content = content.replace(/]*aria-label="Unsupported"[^>]*>[^<]*<\/span>/g, '✗') diff --git a/src/automated-pipelines/components/parameter-table/ChildBodyParametersRows.tsx b/src/automated-pipelines/components/parameter-table/ChildBodyParametersRows.tsx index 3484e0ab19ac..7c98bb28a7ec 100644 --- a/src/automated-pipelines/components/parameter-table/ChildBodyParametersRows.tsx +++ b/src/automated-pipelines/components/parameter-table/ChildBodyParametersRows.tsx @@ -1,4 +1,4 @@ -import cx from 'classnames' +import cx from 'clsx' import { useTranslation } from '@/languages/components/useTranslation' import { ParameterRow } from './ParameterRow' diff --git a/src/automated-pipelines/components/parameter-table/ParameterRow.tsx b/src/automated-pipelines/components/parameter-table/ParameterRow.tsx index 823b8f361dc6..aaf3c91809b1 100644 --- a/src/automated-pipelines/components/parameter-table/ParameterRow.tsx +++ b/src/automated-pipelines/components/parameter-table/ParameterRow.tsx @@ -1,4 +1,4 @@ -import cx from 'classnames' +import cx from 'clsx' import { useTranslation } from '@/languages/components/useTranslation' import { ChildBodyParametersRows } from './ChildBodyParametersRows' diff --git a/src/automated-pipelines/components/parameter-table/ParameterTable.tsx b/src/automated-pipelines/components/parameter-table/ParameterTable.tsx index 90caf7804738..a72dae94216b 100644 --- a/src/automated-pipelines/components/parameter-table/ParameterTable.tsx +++ b/src/automated-pipelines/components/parameter-table/ParameterTable.tsx @@ -1,4 +1,4 @@ -import cx from 'classnames' +import cx from 'clsx' import { useTranslation } from '@/languages/components/useTranslation' import { ParameterRow } from './ParameterRow' @@ -43,11 +43,9 @@ export function ParameterTable({ {headers.length > 0 && ( <> - {/*

{t('headers')}

*/} @@ -71,11 +69,9 @@ export function ParameterTable({ {pathParams.length > 0 && ( <> - {/*

{t('path')}

*/}
{t('headers')}
@@ -107,11 +103,9 @@ export function ParameterTable({ {queryParams.length > 0 && ( <> - {/*

{t('query')}

*/}
{t('path')}
@@ -143,11 +137,9 @@ export function ParameterTable({ {bodyParameters.length > 0 && ( <> - {/*

{variant === 'rest' ? t('body') : t('webhook-body')}

*/}
{t('query')}
{variant === 'rest' ? t('body') : t('webhook-body')} diff --git a/src/codeql-cli/scripts/convert-markdown-for-docs.ts b/src/codeql-cli/scripts/convert-markdown-for-docs.ts index 8c7045f91207..2462ba5d927e 100644 --- a/src/codeql-cli/scripts/convert-markdown-for-docs.ts +++ b/src/codeql-cli/scripts/convert-markdown-for-docs.ts @@ -30,8 +30,9 @@ interface ConversionResult { data: Frontmatter } -// The conversion mutates the mdast tree as loosely typed node bags (changing -// node.type, value, url, etc.), so model the handful of fields we touch. +// The conversion mutates the mdast tree as loosely typed node bags, +// changing node.type, value, url and so on. +// Model only the fields we touch. interface MdNode { type: string value: string @@ -53,7 +54,6 @@ const HEADING_BEGIN = '::: {.option}\n' const END_SECTION = '\n:::' const PROGRAM_SECTION = '::: {.program}\n' -// Updates several properties of the Markdown file using the AST export async function convertContentToDocs( content: string, frontmatterDefaults: FrontmatterDefaults = {}, @@ -66,26 +66,22 @@ export async function convertContentToDocs( const frontmatter: Frontmatter = { title: '', ...frontmatterDefaults } const akaMsLinkMatches: MdNode[] = [] - // Visit all heading nodes visit(ast, 'heading', (rawNode) => { const node = rawNode as unknown as MdNode - // This is the title of the article, so we want to store it to - // the frontmatter + // A level 1 heading is the article title. if (node.depth === 1) { frontmatter.title = node.children[0].value } - // There are some headings that include a title followed by - // some markup that looks like - // {#options-to-configure-the-package-manager.} + // Some headings end with markup like {#options-to-configure-the-package-manager.} if (node.children[0].value.includes('{#')) { node.children[0].value = node.children[0].value.split('{#')[0].trim() } - // This is a workaround for the secondary options that are at the - // wrong heading level in the source rst files. Everything after the - // headings "Synopsis", "Description", and "Options" should be - // one level higher in Markdown. + // Works around secondary options sitting at the wrong heading level + // in the source rst files. + // Everything after the "Synopsis", "Description", and "Options" + // headings moves up one level, so h4 becomes h3. if (secondaryOptions) { node.depth = Math.max(1, Math.min(6, node.depth - 1)) } @@ -97,15 +93,13 @@ export async function convertContentToDocs( } }) - // Visit heading and paragraph nodes to get intro text let currentNodeIsDescription = false visit(ast, (rawNode) => { const node = rawNode as unknown as MdNode if (node.type !== 'heading' && node.type !== 'paragraph') return false - // The first paragraph sibling to the heading "Description" is the - // node that contains the first string of the description text. We - // want to use that first string as the intro frontmatter + // The first paragraph after the "Description" heading + // becomes the intro frontmatter. if (node.children[0]?.value === 'Description' && node.children[0]?.type === 'text') { currentNodeIsDescription = true } @@ -116,7 +110,6 @@ export async function convertContentToDocs( } }) - // Modify the text, code, and link nodes const matchNodeTypes = ['text', 'code', 'link'] visitParents( ast, @@ -127,26 +120,24 @@ export async function convertContentToDocs( (rawNode, rawAncestors) => { const node = rawNode as unknown as MdNode const ancestors = rawAncestors as unknown as MdNode[] - // Add the copy button to the example command if (node.type === 'code' && node.value.startsWith(`codeql ${frontmatter.title}`)) { node.lang = 'shell' node.meta = 'copy' } - // This is the beginning of a secondary options section. For example, - // "Output format options." The rst file doesn't have a heading level - // for these, so we want to make it a Markdown heading at one level - // higher than the previous heading (which is a level lower than Options) + // The start of a secondary options section, for example + // "Output format options." + // The rst file gives these no heading level, so nest them one level + // under `depth`, the last heading level seen by the walk above. if (node.type === 'text' && node.value && node.value.includes(HEADING_BEGIN)) { node.value = node.value.replace(HEADING_BEGIN, '') - // Ancestors are ordered from the furthest away (root) to the closest. - // Make the text node's parent a heading node. + // Ancestors run root first, so the last one is the parent. ancestors[ancestors.length - 1].type = 'heading' ancestors[ancestors.length - 1].depth = Math.max(1, Math.min(6, depth + 1)) } - // There are some keywords like [Plumbing] used by the code comments - // but we don't want to render them in the docs. + // Keywords like [Plumbing] come from the source code comments + // and should not render in the docs. if (node.type === 'text' && node.value) { for (const keyword of removeKeywords) { if (node.value.includes(keyword)) { @@ -155,9 +146,8 @@ export async function convertContentToDocs( } } - // The subsections under the main headings (level 2) are commands - // and start with either `-` or `<`. We want to make these inline code - // instead of text. + // Subsections under the level 2 headings are commands + // starting with `-` or `<`, so render them as inline code. if ( node.type === 'text' && ancestors[ancestors.length - 1].type === 'heading' && @@ -166,56 +156,45 @@ export async function convertContentToDocs( node.type = 'inlineCode' } - // Removes the strings that denote the end of an options sections. These - // strings were added during the pandoc conversion. + // Pandoc adds a marker at the end of each options section. Remove it. if (node.type === 'text' && node.value && node.value.includes(END_SECTION)) { node.value = node.value.replace(END_SECTION, '') } - // These are links to other CodeQL CLI docs. We want to convert them to - // Markdown links. Pandoc converts the rst links to a format that - // looks like this: - // `codeql test run`{.interpreted-text role=\"doc\"} - // Link title: codeql test run - // Relative path: test-run - // And the rest can be removed. - // The inline code tag `codeql test run` is one node and the - // string {.interpreted-text role=\"doc\"} is another node. + // Links to other CodeQL CLI docs, which need to become Markdown links. + // Pandoc converts the rst links to this shape: + // `codeql test run`{.interpreted-text role="doc"} + // giving a link title of `codeql test run` and a relative path of + // `test-run`. The rest can be dropped. + // The inline code tag is one node and the {.interpreted-text} string + // is another. if (node.type === 'text' && node.value.includes('{.interpreted-text')) { const paragraph = ancestors[ancestors.length - 1].children const docRoleTagChild = paragraph.findIndex( (child: MdNode) => child.value && child.value.includes('{.interpreted-text'), ) const link = paragraph[docRoleTagChild - 1] - // If child node is already a link node, skip it if (link.type === 'link') { return } - // Currently, this applies to the Markdown files generated by Pandoc, - // but it may not always be the case. If we find an exception to this - // rule, we may need to modify this code to handle it. if (link.type !== 'inlineCode') { throw new Error( 'Unexpected node type. The node before a text node with {.interpreted-text role="doc"} should be an inline code or link node.', ) } - // Sometimes there are newline characters in the middle of the title - // or in the link path. We want to remove those. + // Titles and link paths sometimes contain newlines. const linkText = link.value.split('<')[0].replace(/\n/g, ' ').trim() const linkPath = link.value.split('<')[1].split('>')[0].replace(/'\n/g, '').trim() - // Remove the string {.interpreted-text role="doc"} from this node node.value = node.value.replace(/\n/g, ' ').replace('{.interpreted-text role="doc"}', '') - // Check for circular links - if the link points to the same file we're processing + // A link to the file being converted would be circular. const currentFileBaseName = currentFileName.replace('.md', '') if (currentFileBaseName && linkPath === currentFileBaseName) { - // Convert circular link to plain text instead of creating a link link.type = 'text' link.value = linkText } else { - // Make the previous sibling node a link link.type = 'link' link.url = `${RELATIVE_LINK_PATH}/${linkPath}` link.children = [{ type: 'text', value: linkText }] as unknown as MdNode[] @@ -223,18 +202,17 @@ export async function convertContentToDocs( } } - // Save any nodes that contain aka.ms links so we can convert them later + // Collect aka.ms links to resolve after the tree walk. if (node.type === 'link' && node.url.includes('aka.ms')) { akaMsLinkMatches.push(node) } - // There are example links in the format https://containers.GHEHOSTNAME - // that we don't want our link checker to check so we need to make them - // inline code instead of links. Ideally, this should be done in the - // Java program that generates the rst files, but we can do it here for now. + // Example links like https://containers.GHEHOSTNAME should not be + // checked by the link checker, so render them as inline code. + // The Java program that generates the rst files should do this instead. // See https://github.com/syntax-tree/mdast#inlinecode if (node.type === 'link' && node.url.startsWith('https://containers')) { - // The nodes before and after contain double quotes that we want to remove + // Strip the double quotes from the nodes either side. const nodeBefore = ancestors[ancestors.length - 1].children[0] const nodeAfter = ancestors[ancestors.length - 1].children[2] if (nodeBefore.value && nodeBefore.value.endsWith('"')) { @@ -243,7 +221,6 @@ export async function convertContentToDocs( if (nodeAfter.value && nodeAfter.value.startsWith('"')) { nodeAfter.value = nodeAfter.value.slice(1) } - // Change the node to an inline code node const inlineCode = node as { type: string value: string @@ -264,9 +241,8 @@ export async function convertContentToDocs( await Promise.all( akaMsLinkMatches.map(async (node: MdNode) => { const url = await getRedirect(node.url) - // The aka.ms urls are Markdown links in the ast already, - // so we only need to update the url and description - // rewrite the aka.ms link + // These are already Markdown links in the ast, + // so only the url and the link text need updating. if (node.children[0]) { node.children[0].value = 'AUTOTITLE' } @@ -274,12 +250,11 @@ export async function convertContentToDocs( }), ) - // remove the program section from the AST remove(ast, (rawNode) => { const node = rawNode as unknown as MdNode return Boolean(node.value && node.value.startsWith(PROGRAM_SECTION)) }) - // remove the first heading from the AST because that becomes frontmatter + // Level 1 headings become the frontmatter title, so drop them from the body. remove(ast, (rawNode) => { const node = rawNode as unknown as MdNode return node.type === 'heading' && node.depth === 1 @@ -291,7 +266,6 @@ export async function convertContentToDocs( } } -// performs a get request for a aka.ms url and returns the redirect url async function getRedirect(url: string): Promise { let response: Response try { @@ -305,20 +279,16 @@ async function getRedirect(url: string): Promise { throw new Error(errorMsg) } - // Get the redirect location from the response header const redirectLocation = response.headers.get('location') if (!redirectLocation) { throw new Error(`No redirect location found for ${url}`) } - // Parse the URL to get the pathname const redirect = new URL(redirectLocation).pathname - // Some of the aka.ms links have the /en language prefix. - // This removes all language prefixes from the redirect url. + // Some aka.ms links redirect to a URL with a language prefix such as /en. const parts = redirect.split('/') if (parts.length > 1 && languageKeys.includes(parts[1])) { - // delete language prefix (e.g., /en, /ja, etc.) parts.splice(1, 1) } diff --git a/src/codeql-cli/scripts/sync.ts b/src/codeql-cli/scripts/sync.ts index d7a273980178..f675954b555a 100755 --- a/src/codeql-cli/scripts/sync.ts +++ b/src/codeql-cli/scripts/sync.ts @@ -22,7 +22,6 @@ main() async function main() { await setupEnvironment() - // convert the rst files to Markdown using pandoc await rstToMarkdown(sourceDirectory) const markdownFiles = walk(TEMP_DIRECTORY, { @@ -33,11 +32,10 @@ async function main() { for (const file of markdownFiles) { const sourceContent = await readFile(file, 'utf8') - // There is a missing heading in the source content called "Primary Options" - // It should be directory under the "Options" heading. - // It's a quite a bit more complicated to add new nodes in the AST when - // the node isn't a child of the previous heading. It's pretty easy to - // just append a second heading here. + // The source content is missing a "Primary Options" heading directly + // under "Options". + // Adding a node to the AST is fiddly when it is not a child of the + // previous heading, so append the heading to the raw Markdown instead. const matchHeading = '## Options\n' const primaryHeadingSourceContent = sourceContent.replace( matchHeading, @@ -55,7 +53,6 @@ async function main() { const finalSourceContent = MARKDOWN_PREFIX + content cliMarkdownContents[targetFilename] = { data: sourceData, content: finalSourceContent } } - // Begin updating Markdown files in the content directory await updateContentDirectory({ targetDirectory, sourceContent: cliMarkdownContents, @@ -63,7 +60,6 @@ async function main() { }) } -// Separates out steps that need to be done before the sync can begin async function setupEnvironment() { if (!existsSync(SOURCE_REPO)) { const errorMessage = `Source directory ${SOURCE_REPO} does not exist. Please clone the repo.` @@ -76,13 +72,10 @@ async function setupEnvironment() { ) } - // refresh the temp directory await rimraf(TEMP_DIRECTORY) await mkdirp(TEMP_DIRECTORY) } -// copy the raw rst files to the temp directory and convert them -// to Markdownusing pandoc async function rstToMarkdown(rstSourceDirectory: string) { const sourceFiles = walk(rstSourceDirectory, { includeBasePath: true, @@ -97,7 +90,6 @@ async function rstToMarkdown(rstSourceDirectory: string) { } await copyFile(file, tempFilePath) - // Convert the rst files to Markdown const markdownFilename = path.basename(file).replace('.rst', '.md') const outputFilepath = `${TEMP_DIRECTORY}/${markdownFilename}` if (outputFilepath.includes(' ') || outputFilepath.includes('..')) { diff --git a/src/codeql-cli/tests/convert-markdown-for-docs.ts b/src/codeql-cli/tests/convert-markdown-for-docs.ts index 3b3f76616d48..86b7661fc4e4 100644 --- a/src/codeql-cli/tests/convert-markdown-for-docs.ts +++ b/src/codeql-cli/tests/convert-markdown-for-docs.ts @@ -46,19 +46,16 @@ For more information, see \`codeql database analyze\`{.interpr test('converts circular links to plain text', async () => { const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md') - // Should not contain circular link expect(result.content).not.toContain( `[codeql bqrs interpret](${RELATIVE_LINK_PATH}/bqrs-interpret)`, ) - // Should contain plain text instead expect(result.content).toContain('codeql bqrs interpret') }) test('preserves non-circular links', async () => { const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md') - // Should preserve valid cross-reference link expect(result.content).toContain( `[codeql database analyze](${RELATIVE_LINK_PATH}/database-analyze)`, ) @@ -67,12 +64,10 @@ For more information, see \`codeql database analyze\`{.interpr test('converts aka.ms links', async () => { const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md') - // Should convert aka.ms link to redirect expect(result.content).toContain( '[AUTOTITLE](/enterprise-server@latest/code-security/reference/code-scanning/sarif-files/sarif-support)', ) - // Should not still contain aka.ms link expect(result.content).not.toContain('https://aka.ms/') }) @@ -89,7 +84,6 @@ For more information, see \`codeql database analyze\`{.interpr const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md') - // Should strip language prefix from aka.ms redirect link expect(result.content).toContain( '[AUTOTITLE](/enterprise-server@latest/code-security/reference/code-scanning/sarif-files/sarif-support)', ) @@ -98,7 +92,6 @@ For more information, see \`codeql database analyze\`{.interpr test('handles edge case: no filename provided', async () => { const result = await convertContentToDocs(testContent, {}, '') - // Should preserve link when no filename is provided expect(result.content).toContain( `[codeql bqrs interpret](${RELATIVE_LINK_PATH}/bqrs-interpret)`, ) @@ -107,7 +100,6 @@ For more information, see \`codeql database analyze\`{.interpr test('handles edge case: different filename', async () => { const result = await convertContentToDocs(testContent, {}, 'different-file.md') - // Should preserve link when filename is different expect(result.content).toContain( `[codeql bqrs interpret](${RELATIVE_LINK_PATH}/bqrs-interpret)`, ) @@ -116,17 +108,14 @@ For more information, see \`codeql database analyze\`{.interpr test('processes both circular and non-circular links correctly in same content', async () => { const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md') - // Circular link should be plain text expect(result.content).not.toContain( `[codeql bqrs interpret](${RELATIVE_LINK_PATH}/bqrs-interpret)`, ) - // Non-circular link should be preserved expect(result.content).toContain( `[codeql database analyze](${RELATIVE_LINK_PATH}/database-analyze)`, ) - // Both should have their text content present expect(result.content).toContain('codeql bqrs interpret') expect(result.content).toContain('codeql database analyze') }) diff --git a/src/codeql-cli/tests/test-circular-links.ts b/src/codeql-cli/tests/test-circular-links.ts deleted file mode 100644 index 651d5ebc0dd9..000000000000 --- a/src/codeql-cli/tests/test-circular-links.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { convertContentToDocs } from '../scripts/convert-markdown-for-docs' - -// Test content that simulates a circular link scenario -const testContent = ` -# bqrs interpret - -[Plumbing] Interpret data in a single BQRS. - -## Description - -A command that interprets a single BQRS file according to the provided -metadata and generates output in the specified format. - -## Options - -### Primary Options - -This option has no effect when passed to \`codeql bqrs interpret\`{.interpreted-text role="doc"}. - -For more information, see \`codeql database analyze\`{.interpreted-text role="doc"}. -` - -async function testCircularLinkFix(): Promise { - console.log('Testing circular link fix...') - - try { - // Test with circular link (should convert to plain text) - const result1 = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md') - console.log('✅ Conversion completed successfully') - - // Check if circular link was converted to plain text - const hasCircularLink = result1.content.includes( - '[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)', - ) - const hasPlainText = result1.content.includes('codeql bqrs interpret') - - if (hasCircularLink) { - console.log('❌ FAIL: Circular link still present in output') - console.log('Content:', result1.content) - return false - } else if (hasPlainText) { - console.log('✅ PASS: Circular link converted to plain text') - } else { - console.log('⚠️ WARNING: Could not find expected text in output') - } - - // Check if non-circular link is preserved - const hasValidLink = result1.content.includes( - '[codeql database analyze](/code-security/codeql-cli/codeql-cli-manual/database-analyze)', - ) - - if (hasValidLink) { - console.log('✅ PASS: Non-circular link preserved correctly') - } else { - console.log('❌ FAIL: Valid cross-reference link was incorrectly removed') - } - - console.log('\n--- Generated content preview ---') - console.log(`${result1.content.substring(0, 800)}...`) - - return !hasCircularLink && hasValidLink - } catch (error) { - console.error('❌ Test failed with error:', error) - return false - } -} - -async function testEdgeCases(): Promise { - console.log('\nTesting edge cases...') - - // Test with no filename (should not crash) - const result1 = await convertContentToDocs(testContent, {}, '') - const hasLink1 = result1.content.includes( - '[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)', - ) - if (hasLink1) { - console.log('✅ PASS: No filename provided - link preserved as expected') - } else { - console.log('❌ FAIL: Link incorrectly removed when no filename provided') - return false - } - - // Test with different filename (should preserve link) - const result2 = await convertContentToDocs(testContent, {}, 'different-file.md') - const hasLink2 = result2.content.includes( - '[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)', - ) - if (hasLink2) { - console.log('✅ PASS: Different filename - link preserved correctly') - } else { - console.log('❌ FAIL: Link incorrectly removed for different filename') - return false - } - - return true -} - -// Run all tests -async function runAllTests(): Promise { - const test1 = await testCircularLinkFix() - const test2 = await testEdgeCases() - - if (test1 && test2) { - console.log('\n🎉 All tests passed!') - process.exit(0) - } else { - console.log('\n💥 Tests failed!') - process.exit(1) - } -} - -runAllTests() diff --git a/src/color-schemes/components/useTheme.ts b/src/color-schemes/components/useTheme.ts index 6d34612987f4..b51c1daff79d 100644 --- a/src/color-schemes/components/useTheme.ts +++ b/src/color-schemes/components/useTheme.ts @@ -96,8 +96,6 @@ export function getCssTheme(cookieValue = ''): CssColorTheme { export function getComponentTheme(cookieValue = ''): ComponentColorTheme { const { colorMode, lightTheme, darkTheme } = getCssTheme(cookieValue) return { - // The cookie value is a primer/css color_mode. - // We need to convert that to a primer/react compatible version. colorMode: cssColorModeToComponentColorMode[colorMode], dayScheme: lightTheme, nightScheme: darkTheme, @@ -111,18 +109,10 @@ export function useTheme() { }) useEffect(() => { - // Using setTimeout with a default delay value of 0 interjects one - // additional event cycle, which works around a bug that is the - // result of a timing issue. Without the setTimeout function - // the page loads, then the docs site switches the color mode to - // match the user's GitHub color mode. Primer React has a useEffect - // call that overrides this change, causing the site to ignore the - // user's GitHub color mode and revert to auto. - // As a temporary workaround, this code that fetches the user's GitHub - // color mode will be called after Primer React's useEffect call. - // The long term solution to this theming issue is to migrate to CSS variables - // under the hood, which Primer is planning to do in the next couple quarters. - // Reference: https://github.com/primer/react/issues/2229 + // setTimeout(0) defers this past Primer React's own useEffect, + // which otherwise overrides the cookie's color mode and reverts the page to auto. + // Primer's migration to CSS variables should remove the need for this. + // https://github.com/primer/react/issues/2229 setTimeout(() => { const cookieValue = Cookies.get(COLOR_MODE_COOKIE_NAME) const css = getCssTheme(cookieValue) diff --git a/src/color-schemes/tests/color-mode-script.ts b/src/color-schemes/tests/color-mode-script.ts index a97b1773e24a..ad6ce2fcb158 100644 --- a/src/color-schemes/tests/color-mode-script.ts +++ b/src/color-schemes/tests/color-mode-script.ts @@ -56,7 +56,6 @@ function runScript( } function cookieFor(value: object) { - // The real cookie value is URL-encoded JSON, like the browser stores it. return `color_mode=${encodeURIComponent(JSON.stringify(value))}` } diff --git a/src/content-linter/lib/helpers/get-rules.ts b/src/content-linter/lib/helpers/get-rules.ts index a27587067fc0..16f6c0b0ba44 100644 --- a/src/content-linter/lib/helpers/get-rules.ts +++ b/src/content-linter/lib/helpers/get-rules.ts @@ -3,7 +3,8 @@ import { baseConfig } from '@/content-linter/style/base' import { customConfig } from '@/content-linter/style/github-docs' import type { Rule, RuleConfig } from '@/content-linter/types' -// Import markdownlint rules - external library without TypeScript declarations +// markdownlint's rule list is not a public export and has no type declarations, +// so this reaches into node_modules directly. import markdownlintRules from '../../../../node_modules/markdownlint/lib/rules' export const customRules: Rule[] = gitHubDocsMarkdownlint.rules diff --git a/src/content-linter/lib/helpers/liquid-utils.ts b/src/content-linter/lib/helpers/liquid-utils.ts index 8afbfd290f0d..76c43b0b5147 100644 --- a/src/content-linter/lib/helpers/liquid-utils.ts +++ b/src/content-linter/lib/helpers/liquid-utils.ts @@ -3,10 +3,8 @@ import type { TopLevelToken, TagToken } from 'liquidjs' import { deprecated } from '@/versions/lib/enterprise-server-releases' -// Cache for liquid tokens to improve performance const liquidTokenCache = new Map() -// Returns TopLevelToken array from liquidjs which is a union of TagToken, OutputToken, and HTMLToken export function getLiquidTokens( content: string, { noCache = false }: { noCache?: boolean } = {}, @@ -33,7 +31,6 @@ export const OUTPUT_CLOSE = '%}' export const conditionalTags = ['if', 'elseif', 'unless', 'case', 'ifversion'] -// Token parameter uses TopLevelToken which has begin and end properties export function getPositionData( token: TopLevelToken, lines: string[], @@ -69,7 +66,6 @@ export function getPositionData( * by Markdownlint: * [ { lineNumber: 1, column: 1, deleteCount: 3, }] */ -// Token parameter uses TopLevelToken from liquidjs export function getContentDeleteData( token: TopLevelToken, tokenEnd: number, diff --git a/src/content-linter/lib/helpers/print-annotations.ts b/src/content-linter/lib/helpers/print-annotations.ts index c252f8c253d4..0076e2d784cb 100644 --- a/src/content-linter/lib/helpers/print-annotations.ts +++ b/src/content-linter/lib/helpers/print-annotations.ts @@ -1,9 +1,6 @@ -/** - * This function is meant to be used by the code that runs the linter, - * but only within Actions workflows. That means, if it works, it - * posts all the annotations as inline comments on the PR. - * - */ +// Meant to be used by the code that runs the linter, but only within Actions +// workflows. When it works, it posts all the annotations as inline comments +// on the pull request. interface LintFlaw { ruleNames: string[] @@ -58,7 +55,7 @@ export function printAnnotationResults( } // Why console.log and not `core.error()` (from @actions/core)? - // Because, this way you can debug this more easily on you own + // Because, this way you can debug this more easily on your own // terminal. console.log(annotation) } diff --git a/src/content-linter/lib/helpers/utils.ts b/src/content-linter/lib/helpers/utils.ts index d848cc98ec58..3e051e20d28d 100644 --- a/src/content-linter/lib/helpers/utils.ts +++ b/src/content-linter/lib/helpers/utils.ts @@ -48,20 +48,13 @@ export function isStringQuoted(text: string): boolean { } export function isStringPunctuated(text: string): boolean { - // String ends with punctuation of either - // . ? ! and optionally ends with single - // or double quotes. This also allows - // for single or double quotes before - // the punctuation. + // String ends with a period, question mark, or exclamation point, optionally + // followed by a single or double quote. return /^.*[.?!]['"]?$/.test(text) } export function doesStringEndWithPeriod(text: string): boolean { - // String ends with punctuation of either - // . ? ! and optionally ends with single - // or double quotes. This also allows - // for single or double quotes before - // the punctuation. + // String ends with a period, optionally followed by a single or double quote. return /^.*\.['"]?$/.test(text) } @@ -73,9 +66,7 @@ export function quotePrecedesLinkOpen(text: string | undefined): boolean { // Lines is an array of strings read from a // Markdown file a split around new lines. // This is the format we get from Markdownlint. -// Returns null if the lines do not contain -// frontmatter properties. -// Returns frontmatter as a Record with unknown values since YAML can contain various types +// Returns null if the lines do not contain frontmatter properties. export function getFrontmatter(lines: string[]): Record | null { const fmString = lines.join('\n') const { data } = matter(fmString) diff --git a/src/content-linter/lib/linting-rules/code-annotation-comment-spacing.ts b/src/content-linter/lib/linting-rules/code-annotation-comment-spacing.ts index f205c799486c..e0f5c8a89392 100644 --- a/src/content-linter/lib/linting-rules/code-annotation-comment-spacing.ts +++ b/src/content-linter/lib/linting-rules/code-annotation-comment-spacing.ts @@ -22,19 +22,16 @@ export const codeAnnotationCommentSpacing = { const trimmedLine = line.trim() if (!trimmedLine) continue - // Define a map of comment patterns const commentPatterns: Record = { '//': /^(\/\/)(.*)/, // JavaScript/TypeScript/Java/C# style comments '#': /^(#)(.*)/, // Python/Ruby/Shell/YAML style comments '--': /^(--)(.*)/, // SQL/Lua style comments } - // Check for different comment patterns let commentMatch: RegExpMatchArray | null = null let commentChar: string | null = null let restOfLine: string | null = null - // Iterate over the map to find a matching comment style for (const [char, pattern] of Object.entries(commentPatterns)) { if (trimmedLine.startsWith(char)) { commentMatch = trimmedLine.match(pattern) @@ -50,11 +47,9 @@ export const codeAnnotationCommentSpacing = { continue } - // Allow empty comments or comments with exactly one space if (restOfLine === '' || restOfLine.startsWith(' ')) { // If it starts with a space, make sure it's exactly one space if (restOfLine.startsWith(' ') && restOfLine.length > 1 && restOfLine[1] === ' ') { - // Multiple spaces - this is an error const lineNumber: number = token.lineNumber + index + 1 const fixedLine: string = line.replace( new RegExp(`^(\\s*${commentChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\s+`), @@ -75,10 +70,8 @@ export const codeAnnotationCommentSpacing = { }, ) } - // Single space or empty - this is correct continue } else { - // No space after comment character - this is an error const lineNumber: number = token.lineNumber + index + 1 const leadingWhitespace: string = line.match(/^\s*/)![0] const fixedLine: string = `${leadingWhitespace + commentChar} ${restOfLine}` diff --git a/src/content-linter/lib/linting-rules/ctas-schema.ts b/src/content-linter/lib/linting-rules/ctas-schema.ts index 72c58a70ec75..95a7419996c1 100644 --- a/src/content-linter/lib/linting-rules/ctas-schema.ts +++ b/src/content-linter/lib/linting-rules/ctas-schema.ts @@ -14,7 +14,6 @@ export const ctasSchema: Rule = { tags: ['ctas', 'schema', 'urls'], function: (params: RuleParams, onError: RuleErrorCallback) => { // Find all URLs in the content that might be CTAs - // Updated regex to properly handle URLs in quotes and other contexts const urlRegex = /https?:\/\/[^\s)\]{}'">]+/g const content = params.lines.join('\n') @@ -22,7 +21,7 @@ export const ctasSchema: Rule = { while ((match = urlRegex.exec(content)) !== null) { const url = match[0] - // Check if this URL has ref_ parameters and is on a GitHub domain (indicating it's a CTA URL) + // A ref_ parameter is what marks a URL as a CTA. if (!url.includes('ref_')) continue // Only validate CTA URLs on GitHub domains @@ -56,7 +55,6 @@ export const ctasSchema: Rule = { if (!hasRefParams) continue - // Collect all ref_ parameters for (const [key, value] of searchParams.entries()) { if (key.startsWith('ref_')) { refParams[key] = value @@ -70,7 +68,6 @@ export const ctasSchema: Rule = { if (hasOldParams) { const result = convertOldCTAUrl(url) if (result && result.newUrl !== url) { - // Find the line and create fix info const lineIndex = params.lines.findIndex((line) => line.includes(url)) const lineNumber = lineIndex >= 0 ? lineIndex + 1 : 1 const line = lineIndex >= 0 ? params.lines[lineIndex] : '' @@ -92,7 +89,6 @@ export const ctasSchema: Rule = { ) } } else { - // Validate new format URLs against schema const isValid = validateCTASchema(refParams) if (!isValid) { @@ -100,7 +96,6 @@ export const ctasSchema: Rule = { const lineNumber = lineIndex >= 0 ? lineIndex + 1 : 1 const line = lineIndex >= 0 ? params.lines[lineIndex] : '' - // Process AJV errors manually for CTA URLs const errors = validateCTASchema.errors || [] for (const error of errors) { let message = '' @@ -108,7 +103,6 @@ export const ctasSchema: Rule = { message = `Missing required parameter: ${(error.params as { missingProperty?: string })?.missingProperty}` } else if (error.keyword === 'enum') { const paramName = error.instancePath.substring(1) - // Get the actual invalid value from refParams and allowed values from params const invalidValue = refParams[paramName] const allowedValues = (error.params as { allowedValues?: unknown[] })?.allowedValues || [] diff --git a/src/content-linter/lib/linting-rules/early-access-references.ts b/src/content-linter/lib/linting-rules/early-access-references.ts index 8e9971add2c7..2ee8089ac92b 100644 --- a/src/content-linter/lib/linting-rules/early-access-references.ts +++ b/src/content-linter/lib/linting-rules/early-access-references.ts @@ -73,8 +73,8 @@ export const frontmatterEarlyAccessReferences: Rule = { if (filepath === 'content/index.md' || filepath.includes('fixtures/content/index.md')) delete fm.children - // Convert updated frontmatter back to a string - // to search for 'early-access'.' + // Convert the updated frontmatter back to a string so we can search it + // for 'early-access'. const fmStrings = dump(fm).split('\n') for (const line of fmStrings) { diff --git a/src/content-linter/lib/linting-rules/expired-content.ts b/src/content-linter/lib/linting-rules/expired-content.ts index 3198581b1fac..1cbde7351f6f 100644 --- a/src/content-linter/lib/linting-rules/expired-content.ts +++ b/src/content-linter/lib/linting-rules/expired-content.ts @@ -54,11 +54,8 @@ export const DAYS_TO_WARN_BEFORE_EXPIRED = 14 // days. The rule looks for opening and closing HTML comment tags that // contain an expiration date in the format: // -// This is content that is -// expired that does not expire. -// -// The `end expires` closing tag closes the content that is expired -// and must be removed. +// This is content that is scheduled +// to expire that does not expire. export const expiringSoon: Rule = { names: ['GHD039', 'expiring-soon'], description: 'Content that expires soon should be proactively addressed.', diff --git a/src/content-linter/lib/linting-rules/frontmatter-children.ts b/src/content-linter/lib/linting-rules/frontmatter-children.ts index 1c4d0a5b1ce2..49be26d85003 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-children.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-children.ts @@ -23,7 +23,7 @@ function isValidChildPath(childPath: string, currentFilePath: string): boolean { let resolvedPath: string if (childPath.startsWith('/content/')) { - // Absolute path from content root - strip /content/ prefix + // Absolute path from the content root: strip the /content/ prefix. const absoluteChildPath = childPath.slice('/content/'.length) resolvedPath = path.resolve(contentDir, absoluteChildPath) } else { @@ -39,13 +39,11 @@ function isValidChildPath(childPath: string, currentFilePath: string): boolean { return false } - // Check for direct .md file const mdPath = `${resolvedPath}.md` if (fs.existsSync(mdPath) && fs.statSync(mdPath).isFile()) { return true } - // Check for index.md file in directory const indexPath = path.join(resolvedPath, 'index.md') if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) { return true diff --git a/src/content-linter/lib/linting-rules/frontmatter-content-type.ts b/src/content-linter/lib/linting-rules/frontmatter-content-type.ts index 7130d18edd12..109490a08c7a 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-content-type.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-content-type.ts @@ -21,7 +21,7 @@ const KNOWN_CONTENT_TYPE_DIRS = new Set([ ]) // Lazily computed set of product directories whose subdirectories all follow -// the content-type directory pattern. Once computed the set is reused for +// the content-type directory pattern. Once computed the set is reused for // every file processed in the same lint run. let qualifyingProducts: Set | null = null @@ -48,7 +48,7 @@ function getQualifyingProducts(): Set { if (subdirs.length === 0) continue // A product qualifies when ALL of its subdirectories are known - // content-type directories. Use .includes() for responsible-use so + // content-type directories. Use .includes() for responsible-use so // that variations like "responsible-use-of-…" are recognised, matching // the logic in dirToContentType(). const isKnownDir = (name: string) => @@ -110,7 +110,6 @@ export const frontmatterContentType = { const fm = getFrontmatter(params.lines) if (!fm) return - // Determine expected contentType let expectedType: string if (segments.length === 2 && segments[1] === 'index.md') { // Product-level index.md is always a landing page @@ -135,7 +134,6 @@ export const frontmatterContentType = { const fixHint = `Run \`npx tsx src/content-render/scripts/add-content-type.ts --paths ${product}\` to fix.` - // Check: contentType must exist if (!fm.contentType) { addError( onError, @@ -148,7 +146,6 @@ export const frontmatterContentType = { return } - // Check: contentType must match the directory-derived value if (fm.contentType !== expectedType) { addError( onError, diff --git a/src/content-linter/lib/linting-rules/frontmatter-docs-team-metrics.ts b/src/content-linter/lib/linting-rules/frontmatter-docs-team-metrics.ts index 39ad64f26521..1cd9ddd29d4f 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-docs-team-metrics.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-docs-team-metrics.ts @@ -20,7 +20,6 @@ export const frontmatterDocsTeamMetrics = { const filePath = params.name - // Determine which path-enforced metrics values match this file's path. const expectedValues = PATH_ENFORCED_METRICS.filter((value) => filePath.includes(value)) if (expectedValues.length === 0) return diff --git a/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts b/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts index 28f3cf26cfce..914940f0214e 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts @@ -37,7 +37,6 @@ export const frontmatterHeroImage: Rule = { 'Hero image paths must be absolute, extensionless, and point to valid images in /assets/images/banner-images/', tags: ['frontmatter', 'images'], function: (params: RuleParams, onError: RuleErrorCallback) => { - // Only check index.md files if (!params.name.endsWith('index.md')) return const fm = getFrontmatter(params.lines) as Frontmatter | null @@ -45,7 +44,6 @@ export const frontmatterHeroImage: Rule = { const heroImage = fm.heroImage - // Check if heroImage is an absolute path if (!heroImage.startsWith('/')) { const line = params.lines.find((ln: string) => ln.trim().startsWith('heroImage:')) const lineNumber = line ? params.lines.indexOf(line) + 1 : 1 @@ -59,7 +57,6 @@ export const frontmatterHeroImage: Rule = { return } - // Check if heroImage points to banner-images directory if (!heroImage.startsWith('/assets/images/banner-images/')) { const line = params.lines.find((ln: string) => ln.trim().startsWith('heroImage:')) const lineNumber = line ? params.lines.indexOf(line) + 1 : 1 @@ -88,7 +85,6 @@ export const frontmatterHeroImage: Rule = { return } - // Check if a file with this base name actually exists const validHeroImages = getValidHeroImages() if (validHeroImages.length > 0 && !validHeroImages.includes(heroImage)) { const line = params.lines.find((ln: string) => ln.trim().startsWith('heroImage:')) diff --git a/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts b/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts index 8ffb098d5adc..e498fa0bd320 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-hidden-docs.ts @@ -31,7 +31,7 @@ export const frontmatterHiddenDocs = { ', ', )}`, hiddenLine, - [1, hiddenLine.length], // No range applicable + [1, hiddenLine.length], null, // No fix possible ) }, diff --git a/src/content-linter/lib/linting-rules/frontmatter-intro-links.ts b/src/content-linter/lib/linting-rules/frontmatter-intro-links.ts index 3e368582818f..c20c3c78df09 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-intro-links.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-intro-links.ts @@ -18,7 +18,6 @@ function getValidIntroLinksKeys(): string[] { return [] } - // Get all keys from product_landing in ui.yml return Object.keys(ui.product_landing) } catch (error) { console.error('Error loading ui.yml data:', error) @@ -43,10 +42,8 @@ export const frontmatterIntroLinks: Rule = { return } - // Check each key in introLinks for (const key of Object.keys(introLinks)) { if (!validKeys.includes(key)) { - // Find the line with this key const line = params.lines.find((ln: string) => { const trimmed = ln.trim() return trimmed.startsWith(`${key}:`) && !trimmed.startsWith('introLinks:') diff --git a/src/content-linter/lib/linting-rules/frontmatter-landing-carousels.ts b/src/content-linter/lib/linting-rules/frontmatter-landing-carousels.ts index cad6bd9f25c2..184cc9a715d1 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-landing-carousels.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-landing-carousels.ts @@ -18,13 +18,12 @@ function isValidArticlePath(articlePath: string, currentFilePath: string): boole const contentDir = path.join(ROOT, 'content') const normalizedPath = articlePath.startsWith('/') ? articlePath.substring(1) : articlePath - // Check for direct .md file const absolutePath: string = path.join(contentDir, `${normalizedPath}.md`) if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) { return true } - // Check for index.md file in directory (for landing pages) + // A directory with an index.md in it is a landing page. const indexPath: string = path.join(contentDir, normalizedPath, 'index.md') if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) { return true @@ -33,7 +32,6 @@ function isValidArticlePath(articlePath: string, currentFilePath: string): boole // Strategy 2: Fall back to relative path from current file's directory const currentDir: string = path.dirname(currentFilePath) - // Check for relative .md file const relativePath: string = path.join(currentDir, `${normalizedPath}.md`) try { if (fs.existsSync(relativePath) && fs.statSync(relativePath).isFile()) { @@ -43,7 +41,6 @@ function isValidArticlePath(articlePath: string, currentFilePath: string): boole // Continue to next strategy } - // Check for relative index.md file const relativeIndexPath: string = path.join(currentDir, normalizedPath, 'index.md') try { return fs.existsSync(relativeIndexPath) && fs.statSync(relativeIndexPath).isFile() @@ -58,7 +55,6 @@ export const frontmatterLandingCarousels = { 'Only landing pages can have carousels, there should be no duplicate articles, and all articles must exist', tags: ['frontmatter', 'landing', 'carousels'], function: (params: RuleParams, onError: RuleErrorCallback) => { - // Using any for frontmatter as it's a dynamic YAML object with varying properties const fm = getFrontmatter(params.lines) as Frontmatter | null if (!fm) return @@ -100,7 +96,6 @@ export const frontmatterLandingCarousels = { seen.add(item) } - // Validate that the article path exists if (!isValidArticlePath(item, params.name)) { invalidPaths.push(item) } diff --git a/src/content-linter/lib/linting-rules/frontmatter-rest-api-category.ts b/src/content-linter/lib/linting-rules/frontmatter-rest-api-category.ts index 58d33787c6e5..7217281fd827 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-rest-api-category.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-rest-api-category.ts @@ -48,13 +48,12 @@ export const frontmatterRestApiCategory = { // Skip files that aren't under content/rest/ if (relativePath.startsWith('..')) return - // Skip index files — they are category-level pages, not endpoint pages + // Skip index files: they are category-level pages, not endpoint pages. if (path.basename(resolved) === 'index.md') return const fm = getFrontmatter(params.lines) if (!fm) return - // Only apply to autogenerated REST files if (fm.autogenerated !== 'rest') return // Find error line: prefer the category line, fall back to opening --- @@ -65,7 +64,6 @@ export const frontmatterRestApiCategory = { const errorLine = categoryLineIndex !== -1 ? categoryLineIndex + 1 : fmOpenLine !== -1 ? fmOpenLine + 1 : 1 - // Check: category must exist if (!fm.category) { addError( onError, @@ -78,7 +76,6 @@ export const frontmatterRestApiCategory = { return } - // Check: category must be an array if (!Array.isArray(fm.category)) { addError( onError, @@ -91,7 +88,6 @@ export const frontmatterRestApiCategory = { return } - // Check: category must have at least one value if (fm.category.length === 0) { addError( onError, @@ -104,7 +100,6 @@ export const frontmatterRestApiCategory = { return } - // Check: each value must be in the valid list const valid = getValidCategories() for (const value of fm.category) { if (!valid.includes(value as string)) { diff --git a/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.ts b/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.ts index 30ed2dc89921..005a51d2a612 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.ts +++ b/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.ts @@ -18,17 +18,14 @@ export const frontmatterVersionsWhitespace: Rule = { const versionsObj = fm.versions if (typeof versionsObj !== 'object') return - // Find the frontmatter section in the file const fmStartIndex = params.lines.findIndex((line) => line.trim() === '---') if (fmStartIndex === -1) return - // Check each version entry for whitespace issues for (const [key, value] of Object.entries(versionsObj)) { if (typeof value !== 'string') continue const hasUnwantedWhitespace = checkForUnwantedWhitespace(value) if (hasUnwantedWhitespace) { - // Find the line containing this version key const versionLineIndex = params.lines.findIndex((line, index) => { return index > fmStartIndex && line.trim().startsWith(`${key}:`) && line.includes(value) }) @@ -38,7 +35,6 @@ export const frontmatterVersionsWhitespace: Rule = { const lineNumber = versionLineIndex + 1 const cleanedValue = getCleanedValue(value) - // Create fix info to remove unwanted whitespace const fixInfo = { editColumn: line.indexOf(value) + 1, deleteCount: value.length, @@ -59,24 +55,19 @@ export const frontmatterVersionsWhitespace: Rule = { }, } -/** - * Check if a version string has unwanted whitespace - * Allows whitespace in complex expressions like '<3.6 >3.8' - * but disallows leading/trailing whitespace - */ +// Allows whitespace in complex expressions like '<3.6 >3.8' but disallows +// leading and trailing whitespace. function checkForUnwantedWhitespace(value: string): boolean { // Don't flag if the value is just whitespace or empty if (!value || value.trim() === '') return false - // Check for leading or trailing whitespace if (value !== value.trim()) return true - // Allow whitespace around operators in complex expressions - // This regex matches patterns like '<3.6 >3.8', '>=2.19', etc. + // Values containing <, > or = are treated as ranges like '<3.6 >3.8', where + // internal whitespace is meaningful. const hasOperators = /[<>=]/.test(value) if (hasOperators) { - // For operator expressions, we're more lenient about internal whitespace - // Only flag if there's leading/trailing whitespace (already checked above) + // Leading and trailing whitespace was already checked above. return false } @@ -85,11 +76,9 @@ function checkForUnwantedWhitespace(value: string): boolean { return /\s/.test(value) } -/** - * Get the cleaned version of a value by removing appropriate whitespace - */ function getCleanedValue(value: string): string { - // For values with operators, just trim leading/trailing whitespace + // Values containing <, > or = keep their internal whitespace and are only + // trimmed at the ends. const hasOperators = /[<>=]/.test(value) if (hasOperators) { return value.trim() diff --git a/src/content-linter/lib/linting-rules/index.ts b/src/content-linter/lib/linting-rules/index.ts index 18a630d3cdfb..0a8c51517bef 100644 --- a/src/content-linter/lib/linting-rules/index.ts +++ b/src/content-linter/lib/linting-rules/index.ts @@ -106,7 +106,7 @@ export const gitHubDocsMarkdownlint = { thirdPartyActionPinning, // GHD041 liquidTagWhitespace, // GHD042 linkQuotation, // GHD043 - // GHD044 removed - octicon aria-labels are now auto-generated + // GHD044 removed: octicon aria-labels are now auto-generated. codeAnnotationCommentSpacing, // GHD045 outdatedReleasePhaseTerminology, // GHD046 tableColumnIntegrity, // GHD047 diff --git a/src/content-linter/lib/linting-rules/internal-links-no-lang.ts b/src/content-linter/lib/linting-rules/internal-links-no-lang.ts index 1df11e4d4c38..6334f6c6b5e1 100644 --- a/src/content-linter/lib/linting-rules/internal-links-no-lang.ts +++ b/src/content-linter/lib/linting-rules/internal-links-no-lang.ts @@ -19,19 +19,15 @@ export const internalLinksNoLang: Rule = { // ['href', 'get-started'], ['target', '_blank'], // ['rel', 'canonical'], // ] - // Attribute arrays are tuples of [attributeName, attributeValue] from markdownit parser - const hrefsMissingSlashes = child - .attrs! // The attribute could also be `target` or `rel` - .filter((attr: [string, string]) => attr[0] === 'href') + const hrefsWithLanguageCode = child + // The attribute could also be `target` or `rel`. + .attrs!.filter((attr: [string, string]) => attr[0] === 'href') .filter((attr: [string, string]) => attr[1].startsWith('/') || !attr[1].startsWith('//')) - // Filter out link paths that start with language code .filter((attr: [string, string]) => languageKeys.some((lang) => attr[1].split('/')[1] === lang), ) - // Get the link path from the attribute .map((attr: [string, string]) => attr[1]) - // Create errors for each link path that includes a language code - for (const linkPath of hrefsMissingSlashes) { + for (const linkPath of hrefsWithLanguageCode) { const range = getRange(child.line, linkPath) const languageCode = linkPath.split('/')[1] const replaceChar = linkPath === `/${languageCode}` ? '/' : '' diff --git a/src/content-linter/lib/linting-rules/internal-links-old-version.ts b/src/content-linter/lib/linting-rules/internal-links-old-version.ts index f61b26ca7097..e47b24bf3845 100644 --- a/src/content-linter/lib/linting-rules/internal-links-old-version.ts +++ b/src/content-linter/lib/linting-rules/internal-links-old-version.ts @@ -33,17 +33,14 @@ export const internalLinksOldVersion: Rule = { // ['href', 'get-started'], ['target', '_blank'], // ['rel', 'canonical'], // ] - const hrefsMissingSlashes = child.attrs + const hrefsWithHardcodedVersion = child.attrs // The attribute could also be `target` or `rel` .filter((attr) => attr[0] === 'href') .filter((attr) => attr[1].startsWith('/') || !attr[1].startsWith('//')) - // Filter out link paths that matches the version link regex .filter((attr) => attr[1].match(versionLinkRegEx)) - // Get the link path from the attribute .map((attr) => attr[1]) - // Create errors for each link path that includes a hardcoded version - for (const linkPath of hrefsMissingSlashes) { + for (const linkPath of hrefsWithHardcodedVersion) { const range = getRange(child.line, linkPath) addError( onError, diff --git a/src/content-linter/lib/linting-rules/internal-links-slash.ts b/src/content-linter/lib/linting-rules/internal-links-slash.ts index 2a4101ee195d..cba78378ebcc 100644 --- a/src/content-linter/lib/linting-rules/internal-links-slash.ts +++ b/src/content-linter/lib/linting-rules/internal-links-slash.ts @@ -19,7 +19,6 @@ export const internalLinksSlash: Rule = { // ['href', '/get-started'], ['target', '_blank'], // ['rel', 'canonical'], // ] - // Attribute arrays are tuples of [attributeName, attributeValue] from markdownit parser if (!child.attrs) continue const hrefsMissingSlashes = child.attrs // The attribute could also be `target` or `rel` @@ -33,10 +32,8 @@ export const internalLinksSlash: Rule = { ) // We can ignore empty links because MD042 from markdownlint catches empty links .filter((attr: [string, string]) => attr[1] !== '') - // Get the link path from the attribute .map((attr: [string, string]) => attr[1]) - // Create errors for each link path that doesn't start with a / for (const linkPath of hrefsMissingSlashes) { const range = getRange(child.line, linkPath) addFixErrorDetail(onError, child.lineNumber, `/${linkPath}`, linkPath, range, { diff --git a/src/content-linter/lib/linting-rules/journey-tracks-guide-path-exists.ts b/src/content-linter/lib/linting-rules/journey-tracks-guide-path-exists.ts index d890b24c3f05..05e354e416e6 100644 --- a/src/content-linter/lib/linting-rules/journey-tracks-guide-path-exists.ts +++ b/src/content-linter/lib/linting-rules/journey-tracks-guide-path-exists.ts @@ -5,7 +5,8 @@ import { addError } from 'markdownlint-rule-helpers' import { getFrontmatter } from '../helpers/utils' import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' -// Yoink path validation approach from frontmatter-landing-carousels +// Same two-strategy path resolution as isValidArticlePath in +// frontmatter-landing-carousels.ts. function isValidGuidePath(guidePath: string, currentFilePath: string): boolean { const ROOT = process.env.ROOT || '.' @@ -13,13 +14,12 @@ function isValidGuidePath(guidePath: string, currentFilePath: string): boolean { const contentDir = path.join(ROOT, 'content') const normalizedPath = guidePath.startsWith('/') ? guidePath.substring(1) : guidePath - // Check for direct .md file const absolutePath = path.join(contentDir, `${normalizedPath}.md`) if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) { return true } - // Check for index.md file in directory (for landing pages) + // A directory with an index.md in it is a landing page. const indexPath = path.join(contentDir, normalizedPath, 'index.md') if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) { return true @@ -28,7 +28,6 @@ function isValidGuidePath(guidePath: string, currentFilePath: string): boolean { // Strategy 2: Fall back to relative path from current file's directory const currentDir = path.dirname(currentFilePath) - // Check for relative .md file const relativePath = path.join(currentDir, `${normalizedPath}.md`) try { if (fs.existsSync(relativePath) && fs.statSync(relativePath).isFile()) { @@ -38,7 +37,6 @@ function isValidGuidePath(guidePath: string, currentFilePath: string): boolean { // Continue to next strategy } - // Check for relative index.md file const relativeIndexPath = path.join(currentDir, normalizedPath, 'index.md') try { return fs.existsSync(relativeIndexPath) && fs.statSync(relativeIndexPath).isFile() @@ -73,10 +71,8 @@ export const journeyTracksGuidePathExists = { for (let guideIndex = 0; guideIndex < trackObj.guides.length; guideIndex++) { const guideObj = trackObj.guides[guideIndex] - // Validate guide is an object with expected properties if (!guideObj || typeof guideObj !== 'object') continue - // Validate href property if ('href' in guideObj && typeof guideObj.href === 'string') { if (!isValidGuidePath(guideObj.href, params.name)) { addError( diff --git a/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts b/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts index f3a3dfd547c9..2a815f3b9856 100644 --- a/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts +++ b/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts @@ -13,7 +13,6 @@ export const journeyTracksLiquid = { if (!fm || !fm.journeyTracks || !Array.isArray(fm.journeyTracks)) return if (!fm.layout || fm.layout !== 'journey-landing') return - // Find the base journeyTracks line const journeyTracksLine: string | undefined = params.lines.find((line: string) => line.trim().startsWith('journeyTracks:'), ) @@ -24,7 +23,7 @@ export const journeyTracksLiquid = { for (let trackIndex = 0; trackIndex < fm.journeyTracks.length; trackIndex++) { const track = (fm.journeyTracks as Array>)[trackIndex] // Try to find the line number for this specific journey track so we can use that for the error - // line number. Getting the exact line number is probably more work than it's worth for this + // line number. Getting the exact line number is probably more work than it's worth for this // particular rule. // Look for the track by finding the nth occurrence of track-like patterns after journeyTracks @@ -51,7 +50,7 @@ export const journeyTracksLiquid = { } } - // Simple validation - just check if liquid can parse each string property + // The only check is that Liquid can parse each string property. const properties = [ { name: 'title', value: track.title }, { name: 'description', value: track.description }, @@ -76,10 +75,8 @@ export const journeyTracksLiquid = { for (let guideIndex = 0; guideIndex < track.guides.length; guideIndex++) { const guideObj = track.guides[guideIndex] - // Validate guide is an object with expected properties if (!guideObj || typeof guideObj !== 'object') continue - // Validate href property if ('href' in guideObj && typeof guideObj.href === 'string') { try { liquid.parse(guideObj.href) @@ -93,7 +90,6 @@ export const journeyTracksLiquid = { } } - // Validate alternativeNextStep property if present if ( 'alternativeNextStep' in guideObj && typeof guideObj.alternativeNextStep === 'string' diff --git a/src/content-linter/lib/linting-rules/journey-tracks-unique-ids.ts b/src/content-linter/lib/linting-rules/journey-tracks-unique-ids.ts index ff61b1e310d5..44b496f37b03 100644 --- a/src/content-linter/lib/linting-rules/journey-tracks-unique-ids.ts +++ b/src/content-linter/lib/linting-rules/journey-tracks-unique-ids.ts @@ -3,20 +3,17 @@ import { addError } from 'markdownlint-rule-helpers' import { getFrontmatter } from '../helpers/utils' import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' -// GHD060 export const journeyTracksUniqueIds = { names: ['GHD060', 'journey-tracks-unique-ids'], description: 'Journey track IDs must be unique within a page', tags: ['frontmatter', 'journey-tracks', 'unique-ids'], function: function GHD060(params: RuleParams, onError: RuleErrorCallback) { - // Using any for frontmatter as it's a dynamic YAML object with varying properties const fm: unknown = getFrontmatter(params.lines) if (!fm || typeof fm !== 'object' || !('journeyTracks' in fm)) return const fmObj = fm as Record if (!Array.isArray(fmObj.journeyTracks)) return if (!('layout' in fmObj) || fmObj.layout !== 'journey-landing') return - // Find the base journeyTracks line const journeyTracksLine: string | undefined = params.lines.find((line: string) => line.trim().startsWith('journeyTracks:'), ) @@ -24,14 +21,12 @@ export const journeyTracksUniqueIds = { ? params.lines.indexOf(journeyTracksLine) + 1 : 1 - // Helper function to find line number for a specific track by index function getTrackLineNumber(trackIndex: number): number { if (!journeyTracksLine) return baseLineNumber let trackCount = 0 for (let i = params.lines.indexOf(journeyTracksLine) + 1; i < params.lines.length; i++) { const line = params.lines[i].trim() - // Look for any "- id:" line (journey track indicator) if (line.startsWith('- id:')) { if (trackCount === trackIndex) { return i + 1 diff --git a/src/content-linter/lib/linting-rules/liquid-data-tags.ts b/src/content-linter/lib/linting-rules/liquid-data-tags.ts index 5a626fa8f5b0..d0809801c502 100644 --- a/src/content-linter/lib/linting-rules/liquid-data-tags.ts +++ b/src/content-linter/lib/linting-rules/liquid-data-tags.ts @@ -32,8 +32,8 @@ export const liquidDataReferencesDefined = { for (const token of tokens) { // When the liquid tag is indented_data_reference, there are - // two arguments: the path in the data directory and the number - // of spaces to indent. We only want the first argument to + // two arguments: the path in the data directory and the number of + // spaces to indent. We only want the first argument to // validate if the data reference is defined. const dataDirectoryReference = token.args.split(/\s+/)[0] if (hasData(dataDirectoryReference)) continue diff --git a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts index 68b7d225854f..c0ee0a0bb467 100644 --- a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts +++ b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts @@ -163,8 +163,7 @@ function setLiquidErrors(condTagItems: CondTagItem[], onError: RuleErrorCallback const itemErrorName = tagNameNoCond ? item.name : `${item.name} ${item.cond}` if (item.action?.type === 'delete') { - // There is no next stack item, the endif tag is alway the - // last in a conditional + // There is no next stack item; the endif tag is always last in a conditional. const nextStackItem = item.name === 'endif' ? condTagItems[i].end : condTagItems[i + 1].begin const deleteItems = getContentDeleteData( condTagItems[i] as unknown as TopLevelToken, @@ -189,7 +188,7 @@ function setLiquidErrors(condTagItems: CondTagItem[], onError: RuleErrorCallback } if (item.action?.type === 'all') { - // position is just the tag + // Position is just the tag. const { lineNumber, column, length } = getPositionData( { begin: item.begin, @@ -214,7 +213,7 @@ function setLiquidErrors(condTagItems: CondTagItem[], onError: RuleErrorCallback } if (item.action?.type === 'change') { - // position is just the inside of tag + // Position is just the inside of the tag. const { lineNumber, column, length } = getPositionData( { begin: item.contentrange[0], @@ -246,8 +245,8 @@ async function getApplicableVersionFromLiquidTag(conditionStr: string): Promise< const condition = conditionStr.replace('not ', '') const liquidTagVersions = condition.split(' or ').map((item) => item.trim()) for (const ver of liquidTagVersions) { - // When the version is not a release e.g. fpt or ghec or - // or a feature version + // When the version is not a release, e.g. fpt or ghec, or is a + // feature version. if (ver.split(' ').length === 1) { // handle feature versions (only supports a single feature version) if (ver !== 'fpt' && ver !== 'ghec' && ver !== 'ghes') { @@ -346,15 +345,9 @@ async function initTagObject( return condTagItem } -/* - Rather than filtering out noVersion items, populate - each item with content, newContent, action (delete, update, etc) - cond would be empty if the conditional is removed. - content would be empty if the content is to be deleted. - decorate with line number, length, and column. - Then create flaws per stack item. - newCond - */ +// Rather than filtering out items with no versions, give every item a blank +// action and let updateConditionals decide which ones become delete or change. +// setLiquidErrors turns the resulting actions into flaws later on. function decorateCondTagItems(condTagItems: CondTagItem[]) { for (const item of condTagItems) { item.action = { @@ -384,8 +377,8 @@ function updateConditionals(condTagItems: CondTagItem[]) { // the liquid should always be removed regardless // of whether it's a feature version or a nested // condition. - // NOTE: Original code referenced `item.versionObj` (no `s`), which was always - // undefined; preserved as-is to avoid changing runtime behavior in this PR. + // `item.versionObj` (no `s`) is not a property of CondTagItem, so the + // fallback is always undefined and could be dropped. if ( isAllVersions( item.featureVersionsObj || @@ -396,7 +389,7 @@ function updateConditionals(condTagItems: CondTagItem[]) { break } - /** START check feature versions **/ + // START feature versions // Feature versions that have all versions were removed above // Deprecatable features are those that are either available @@ -440,7 +433,7 @@ function updateConditionals(condTagItems: CondTagItem[]) { ) continue - /** END Feature versions we DON'T want to remove **/ + // END feature versions // Check if a nested condition has all versions // compared to it's parent. @@ -467,8 +460,8 @@ function updateConditionals(condTagItems: CondTagItem[]) { continue } - // If the else condition hasn't already been marked as available - // in all veresions or delete, then there are no other changes possible. + // If the else condition hasn't already been marked as available in all + // versions or as a delete, then there are no other changes possible. if (item.name === 'else') continue // Does the condition contain any versions not defined in the frontmatter diff --git a/src/content-linter/lib/linting-rules/liquid-quoted-conditional-arg.ts b/src/content-linter/lib/linting-rules/liquid-quoted-conditional-arg.ts index 16eb8afd9734..8c06d732976c 100644 --- a/src/content-linter/lib/linting-rules/liquid-quoted-conditional-arg.ts +++ b/src/content-linter/lib/linting-rules/liquid-quoted-conditional-arg.ts @@ -8,7 +8,7 @@ import type { RuleParams, RuleErrorCallback, Rule } from '../../types' /* Checks for instances where a Liquid conditional tag's argument is - quoted because it will always evaluate to true. + quoted because it will always evaluate to true. For example, the following would be flagged: {% if "foo" %} diff --git a/src/content-linter/lib/linting-rules/liquid-syntax.ts b/src/content-linter/lib/linting-rules/liquid-syntax.ts index 5e3a93ea8dda..741d0ad874e0 100644 --- a/src/content-linter/lib/linting-rules/liquid-syntax.ts +++ b/src/content-linter/lib/linting-rules/liquid-syntax.ts @@ -42,8 +42,8 @@ export const frontmatterLiquidSyntax = { if (!isLiquidError(error)) throw error const { errorDescription, columnNumber } = getErrorMessageInfo((error as Error).message) const lineNumber = params.lines.findIndex((line) => line.trim().startsWith(`${key}:`)) + 1 - // Add the key length plus 3 to the column number to account colon and - // for the space after the key and column number starting at 1. + // Add the key length plus 3 to the column number to account for the colon, + // the space after the key, and column numbers starting at 1. // If there is no space after the colon, a YAMLException will be thrown. const startRange = columnNumber + key.length + 3 // If the range is greater than the length of the line, we need to adjust the range to the end of the line diff --git a/src/content-linter/lib/linting-rules/liquid-versioning.ts b/src/content-linter/lib/linting-rules/liquid-versioning.ts index bebbba7bdfa8..2607f9ff0a10 100644 --- a/src/content-linter/lib/linting-rules/liquid-versioning.ts +++ b/src/content-linter/lib/linting-rules/liquid-versioning.ts @@ -22,7 +22,7 @@ type AllFeatures = Record const allShortnames: string[] = Object.keys(allVersionShortnames) const getAllPossibleVersionNames = memoize((): Set => { // This function might appear "slow" but it's wrapped in a memoizer - // so it's only every executed once for all files that the + // so it's only ever executed once for all files that the // Liquid linting rule functions on. // The third argument passed to getDeepDataByLanguage() is only // there for the sake of being able to write a unit test on these @@ -109,7 +109,7 @@ export const liquidIfVersionTags = { lineNumber, ifVersionErrors.join('. '), token.content, - null, // getRange(token.content, args), + null, null, // No fix possible ) } @@ -122,7 +122,8 @@ function validateIfversionConditionals(cond: string, possibleVersionNames: Set` (example: `fpt`) // * Length 2: `not ` (example: `not ghae`) // * Length 3: ` ` (example: `ghes > 3.0`) @@ -176,10 +177,9 @@ function validateIfversionConditionals(cond: string, possibleVersionNames: Set (match!.index >= existing.start && match!.index < existing.end) || @@ -105,7 +99,6 @@ function findOutdatedTerminologyMatches(line: string): MatchInfo[] { } } - // Sort matches by position for consistent ordering return foundMatches.sort((a, b) => a.start - b.start) } @@ -116,7 +109,6 @@ export const outdatedReleasePhaseTerminology = { tags: ['terminology', 'consistency', 'release-phases'], severity: 'error', function: (params: RuleParams, onError: RuleErrorCallback) => { - // Skip excluded files for (const filepath of EXCLUDED_PATHS) { if (params.name.startsWith(filepath)) { return @@ -128,20 +120,16 @@ export const outdatedReleasePhaseTerminology = { const fm = frontmatter(frontmatterString).data if (fm && fm.autogenerated) return - // Check all lines for outdated terminology for (let i = 0; i < params.lines.length; i++) { const line = params.lines[i] const lineNumber = i + 1 - // Find all matches on this line const foundMatches = findOutdatedTerminologyMatches(line) - // Report all found matches for (const matchInfo of foundMatches) { const range = getRange(line, matchInfo.text) const errorMessage = `Replace outdated terminology "${matchInfo.text}" with "${matchInfo.replacement}"` - // Provide a fix suggestion const fixInfo = { editColumn: matchInfo.start + 1, deleteCount: matchInfo.text.length, diff --git a/src/content-linter/lib/linting-rules/rai-app-card-structure.ts b/src/content-linter/lib/linting-rules/rai-app-card-structure.ts index 0e4ca02fdbbb..5d7dc3004f65 100644 --- a/src/content-linter/lib/linting-rules/rai-app-card-structure.ts +++ b/src/content-linter/lib/linting-rules/rai-app-card-structure.ts @@ -7,7 +7,7 @@ import { getFrontmatter } from '../helpers/utils' import type { RuleParams, RuleErrorCallback, Rule } from '../../types' // --------------------------------------------------------------------------- -// Template parser — derives all validation data from templates.md +// Template parser: derives all validation data from templates.md // --------------------------------------------------------------------------- const TEMPLATES_PATH = path.resolve('content/contributing/writing-for-github-docs/templates.md') @@ -34,11 +34,8 @@ export interface ParsedTemplate { reusables: string[] } -/** - * Extract the RAI card template code block from templates.md. - * Finds the sentinel HTML comment, then captures the next fenced code block. - * Strips {% raw %} / {% endraw %} and {% comment %}...{% endcomment %} blocks. - */ +// Finds the sentinel HTML comment, then captures the first fenced yaml block +// after it. Strips {% raw %} / {% endraw %} and {% comment %} blocks. function extractTemplateBlock(): string { const content = fs.readFileSync(TEMPLATES_PATH, 'utf-8') const sentinelIndex = content.indexOf(SENTINEL) @@ -63,11 +60,8 @@ function extractTemplateBlock(): string { .replace(/\{%\s*comment\s*%\}[\s\S]*?\{%\s*endcomment\s*%\}/g, '') } -/** - * Build a regex pattern from a template heading text. - * Headings containing the placeholder get a pattern that matches any text - * in place of the placeholder. Fixed headings get an exact match. - */ +// Headings containing the placeholder get a pattern that matches any text in +// place of the placeholder. Fixed headings get an exact match. function headingToPattern(text: string): RegExp { if (text.includes(PLACEHOLDER)) { const escaped = text @@ -78,20 +72,15 @@ function headingToPattern(text: string): RegExp { return new RegExp(`^${text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') } -/** - * Build a human-readable label for error messages. - * Replaces the placeholder with "..." to keep messages concise. - */ +// Replaces the placeholder with "..." to keep error messages concise. function headingLabel(level: number, text: string): string { const prefix = '#'.repeat(level) const label = text.includes(PLACEHOLDER) ? text.replace(PLACEHOLDER, '...') : text return `${prefix} ${label}` } -/** - * Parse the RAI template into structured heading and reusable data. - * This is the single source of truth for validation — no hardcoded constants. - */ +// Heading text and required reusable paths all come from the template rather +// than from constants in this file. function parseTemplate(): ParsedTemplate { const block = extractTemplateBlock() const lines = block.split('\n') @@ -140,7 +129,6 @@ function parseTemplate(): ParsedTemplate { continue } - // Parse reusable references const reusableMatches = line.matchAll(/\{%\s*data\s+([\w.-]+)\s*%\}/g) for (const m of reusableMatches) { if (!reusables.includes(m[1])) { @@ -157,7 +145,7 @@ function parseTemplate(): ParsedTemplate { return { h2s, h3s, reusables } } -// Lazy singleton — parsed once on first use +// Lazy singleton: parsed once on first use. let _parsed: ParsedTemplate | null = null export function getTemplate(): ParsedTemplate { @@ -194,9 +182,7 @@ function extractHeadings(lines: string[]): Heading[] { // Validators // --------------------------------------------------------------------------- -/** - * Validate required H2 sections exist and appear in the correct order. - */ +// Validate that the required H2 sections exist and appear in the correct order. function validateH2Sections( headings: Heading[], template: ParsedTemplate, @@ -232,10 +218,8 @@ function validateH2Sections( } } -/** - * Validate H3 subsections in a single pass: required ones must exist, - * and all H3s under structured parents must match a known template heading. - */ +// Required H3s must exist, and every H3 under a structured parent must match a +// known template heading. function validateH3Subsections( headings: Heading[], template: ParsedTemplate, @@ -256,14 +240,12 @@ function validateH3Subsections( const parentIndex = headings.findIndex((h) => h.level === 2 && parentPattern.test(h.text)) if (parentIndex === -1) continue // Missing parent caught by validateH2Sections - // Collect actual H3s under this parent const childH3s: Heading[] = [] for (let i = parentIndex + 1; i < headings.length; i++) { if (headings[i].level <= 2) break if (headings[i].level === 3) childH3s.push(headings[i]) } - // Check required H3s exist for (const required of templateH3s) { if (required.optional) continue const found = childH3s.some((h) => required.pattern.test(h.text)) @@ -279,7 +261,6 @@ function validateH3Subsections( } } - // Check all actual H3s match a known template heading for (const child of childH3s) { const matchesKnown = templateH3s.some((t) => t.pattern.test(child.text)) if (!matchesKnown) { @@ -296,9 +277,7 @@ function validateH3Subsections( } } -/** - * Validate that all required boilerplate reusable references are present. - */ +// Validate that all required boilerplate reusable references are present. function validateReusables( lines: string[], template: ParsedTemplate, diff --git a/src/content-linter/lib/linting-rules/rai-reusable-usage.ts b/src/content-linter/lib/linting-rules/rai-reusable-usage.ts index 753a30570e20..ec3fcc8a587b 100644 --- a/src/content-linter/lib/linting-rules/rai-reusable-usage.ts +++ b/src/content-linter/lib/linting-rules/rai-reusable-usage.ts @@ -39,7 +39,7 @@ export const raiReusableUsage: Rule = { .filter((token: LiquidToken) => !token.args.startsWith('variables')) for (const token of tokens) { - // if token is 'data foo.bar` or `indented_data_reference foo.bar depth=3` + // If the token is `data foo.bar` or `indented_data_reference foo.bar spaces=3`, // we only want the `foo.bar` part. const dataDirectoryReference = token.args.split(/\s+/)[0] if (dataDirectoryReference.startsWith('reusables.rai')) continue diff --git a/src/content-linter/lib/linting-rules/table-column-integrity.ts b/src/content-linter/lib/linting-rules/table-column-integrity.ts index 8df6d77a8adb..d60daf3ed717 100644 --- a/src/content-linter/lib/linting-rules/table-column-integrity.ts +++ b/src/content-linter/lib/linting-rules/table-column-integrity.ts @@ -4,28 +4,24 @@ import frontmatter from '@/frame/lib/read-frontmatter' import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' -// Regex to detect table rows (must start with |, contain at least one more |, and end with optional whitespace) +// After optional leading whitespace, a candidate table row opens and closes +// with a pipe. const TABLE_ROW_REGEX = /^\s*\|.*\|\s*$/ -// Regex to detect table separator rows (contains only |, :, -, and whitespace) +// Between the boundary pipes, a candidate separator holds only pipes, colons, +// hyphens, and whitespace. const TABLE_SEPARATOR_REGEX = /^\s*\|[\s\-:|\s]*\|\s*$/ -// Regex to detect Liquid-only cells (whitespace, liquid tag, whitespace) +// A Liquid-only cell is whitespace, a Liquid tag, then whitespace. const LIQUID_ONLY_CELL_REGEX = /^\s*{%\s*(ifversion|else|endif|elsif|for|endfor).*%}\s*$/ -// Regex to use for splitting on non-escaped pipes only +// Escaped pipes (\|) are cell content, not separators. const NON_ESCAPED_PIPE_REGEX = /(? 0 && filteredCells.every((cell) => LIQUID_ONLY_CELL_REGEX.test(cell)) ) @@ -79,7 +71,6 @@ export const tableColumnIntegrity = { for (let i = 0; i < lines.length; i++) { const line = lines[i] - // Toggle code fence state if (line.trim().startsWith('```')) { inCodeFence = !inCodeFence continue @@ -92,7 +83,6 @@ export const tableColumnIntegrity = { const isTableRow = TABLE_ROW_REGEX.test(line) const isSeparatorRow = TABLE_SEPARATOR_REGEX.test(line) - // Check if we're starting a new table if (!inTable && isTableRow) { // Look ahead to see if next line is a separator (confirming this is a table) const nextLine = lines[i + 1] @@ -103,14 +93,12 @@ export const tableColumnIntegrity = { } } - // Check if we're ending a table if (inTable && !isTableRow) { inTable = false expectedColumnCount = null continue } - // If we're in a table, validate column count if (inTable && isTableRow && !isSeparatorRow) { // Skip Liquid-only rows as they're allowed to have different column counts if (isLiquidOnlyRow(line)) { @@ -135,7 +123,7 @@ export const tableColumnIntegrity = { errorMessage, line, range, - null, // No auto-fix available due to complexity + null, // No fix possible ) } } diff --git a/src/content-linter/lib/linting-rules/third-party-action-pinning.ts b/src/content-linter/lib/linting-rules/third-party-action-pinning.ts index 1651650829cc..59abba7009c2 100644 --- a/src/content-linter/lib/linting-rules/third-party-action-pinning.ts +++ b/src/content-linter/lib/linting-rules/third-party-action-pinning.ts @@ -9,7 +9,6 @@ import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '@/conte const actionRegex = /[\w-]+\/[\w-]+@[\w-]+/ // Detects a full-length commit SHA (40 hexadecimal characters) const shaRegex = /[\w-]+\/[\w-]+@[0-9a-fA-F]{40}/ -// Detects first-party actions const firstPartyPrefixes = ['actions/', './.github/actions/', 'github/', 'octo-org/', 'OWNER/'] interface WorkflowStep { diff --git a/src/content-linter/lib/linting-rules/third-party-actions-reusable.ts b/src/content-linter/lib/linting-rules/third-party-actions-reusable.ts index b0cd8c3dada1..3bd0ccaf83b4 100644 --- a/src/content-linter/lib/linting-rules/third-party-actions-reusable.ts +++ b/src/content-linter/lib/linting-rules/third-party-actions-reusable.ts @@ -7,7 +7,6 @@ export const thirdPartyActionsReusable = { description: 'Code examples with third-party actions must include disclaimer reusable', tags: ['actions', 'reusable', 'third-party'], function: (params: RuleParams, onError: RuleErrorCallback) => { - // Find all code fence blocks filterTokens(params, 'fence', (token: MarkdownToken) => { // Only check YAML code blocks (GitHub Actions workflows) if (token.info !== 'yaml' && token.info !== 'yaml copy') return @@ -17,12 +16,10 @@ export const thirdPartyActionsReusable = { const lineNumber = token.lineNumber - // Find third-party actions in the code block const thirdPartyActions = findThirdPartyActions(codeContent) if (thirdPartyActions.length === 0) return - // Check if the required disclaimer reusable is present before this code block or inside it const hasDisclaimer = checkForDisclaimer(params.lines, lineNumber, codeContent) if (!hasDisclaimer) { @@ -33,17 +30,15 @@ export const thirdPartyActionsReusable = { `Code examples with third-party actions must include the disclaimer reusable. Found third-party actions: ${actionList}. Add '{% data reusables.actions.actions-not-certified-by-github-comment %}' before or inside this code block.`, token.line, null, // No specific range within the line - null, // No fix possible - requires manual addition of reusable + null, // No fix possible: the reusable has to be added by hand ) } }) }, } -/** - * Find third-party actions in YAML content - * Third-party actions are identified by actions that are not GitHub-owned or documentation examples - */ +// Third-party actions are the ones that are neither GitHub-owned nor +// documentation examples. function findThirdPartyActions(yamlContent: string): string[] { const thirdPartyActions: string[] = [] const actionPattern = /uses:\s+([^{\s]+\/[^@\s]+@[^\s]+)/g @@ -60,11 +55,7 @@ function findThirdPartyActions(yamlContent: string): string[] { return thirdPartyActions } -/** - * Check if an action should be skipped (GitHub-owned or documentation example) - */ function isExampleOrGitHubAction(actionRef: string): boolean { - // List of patterns to exclude (GitHub-owned and documentation examples) const excludePatterns = [ // GitHub-owned /^actions\//, @@ -76,16 +67,12 @@ function isExampleOrGitHubAction(actionRef: string): boolean { /\/octo-repo[/@]/, /\/hello-world-composite-action[/@]/, /\/monorepo[/@]/, - // Monorepo patterns ] return excludePatterns.some((pattern) => pattern.test(actionRef)) } -/** - * Check if the disclaimer reusable is present before the given line number or inside the code block - * Looks backward from the code block and also inside the code block content - */ +// Looks inside the code block, then backward from it. function checkForDisclaimer( lines: string[], codeBlockLineNumber: number, @@ -93,7 +80,6 @@ function checkForDisclaimer( ): boolean { const disclaimerPattern = /{% data reusables\.actions\.actions-not-certified-by-github-comment %}/ - // First, check inside the code block content if (disclaimerPattern.test(codeContent)) { return true } diff --git a/src/content-pipelines/README.md b/src/content-pipelines/README.md index 7f4707229eda..0d70f5969b69 100644 --- a/src/content-pipelines/README.md +++ b/src/content-pipelines/README.md @@ -40,10 +40,10 @@ You can request changes on the automated PR as you would any other PR. If you se The config file (defined in `src/content-pipelines/config.yml`) contains the important information, so you only need to pass an `--id` to the script: ```bash -# Dry run — run the agent and update content locally, but don't save the SHA +# Dry run: run the agent and update content locally, but don't save the SHA npx tsx src/content-pipelines/scripts/update.ts --id copilot-cli --dry-run -# Full run — save the SHA (affects the next run) +# Full run: save the SHA (affects the next run) npx tsx src/content-pipelines/scripts/update.ts --id copilot-cli # Force a full scan (ignore stored SHA) diff --git a/src/content-pipelines/scripts/update.ts b/src/content-pipelines/scripts/update.ts index 17d18297d52d..f8b0f67ccc71 100644 --- a/src/content-pipelines/scripts/update.ts +++ b/src/content-pipelines/scripts/update.ts @@ -1,7 +1,7 @@ // [start-readme] // // This script clones an external source repository, detects whether its docs -// have changed since the last processed commit, and — if so — runs the +// have changed since the last processed commit, and if so runs the // content-pipeline-update Copilot agent to update our reference articles. // // The workflow (.github/workflows/content-pipelines.yml) calls this script in CI. @@ -22,10 +22,6 @@ import path from 'path' import { load } from 'js-yaml' import { program } from 'commander' -// --------------------------------------------------------------------------- -// Config -// --------------------------------------------------------------------------- - type ContentPipelineConfig = { name?: string 'source-repo': string @@ -43,10 +39,6 @@ function loadConfig(id: string): ContentPipelineConfig | null { return raw[id] ?? null } -// --------------------------------------------------------------------------- -// CLI -// --------------------------------------------------------------------------- - program .description( 'Clone a source repo, detect doc changes, and run the content-pipeline-update agent.\n\n' + @@ -82,7 +74,6 @@ const opts = program.opts<{ fullScan?: boolean }>() -// Load config defaults, then layer CLI overrides on top const config = loadConfig(opts.id) const ID = opts.id @@ -132,10 +123,6 @@ const STATE_DIR = path.join(process.cwd(), 'src/content-pipelines/state') const SHA_FILE = path.join(STATE_DIR, `${ID}.sha`) const DIFF_FILE = path.join(STATE_DIR, `${ID}.diff`) -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - function run(cmd: string, options?: { cwd?: string; silent?: boolean }): string { try { return execSync(cmd, { @@ -149,12 +136,7 @@ function run(cmd: string, options?: { cwd?: string; silent?: boolean }): string } } -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - async function main(): Promise { - // ---- Clone source repo ---- const sourceDir = path.join('/tmp', `content-pipeline-source-${ID}`) if (fs.existsSync(sourceDir)) { @@ -167,8 +149,8 @@ async function main(): Promise { const repoUrl = `https://github.com/${SOURCE_REPO}.git` try { - // Use execFileSync so we can pass the token via env/header instead of - // embedding it in the URL, which would leak in error messages or logs. + // execFileSync passes the token as an argument instead of embedding it in the URL, + // where it would leak into error messages and logs. const args = ['clone'] if (token) { args.push( @@ -186,24 +168,21 @@ async function main(): Promise { const currentSha = run('git rev-parse HEAD', { cwd: sourceDir, silent: true }) console.log(`Source repo HEAD: ${currentSha}`) - // ---- Read stored SHA ---- let storedSha = '' if (!FULL_SCAN && fs.existsSync(SHA_FILE)) { storedSha = fs.readFileSync(SHA_FILE, 'utf-8').trim() console.log(`Stored SHA: ${storedSha}`) } else if (FULL_SCAN) { - console.log('Full scan requested — ignoring stored SHA') + console.log('Full scan requested, ignoring stored SHA') } else { console.log('No stored SHA found (first run)') } - // ---- Check for changes ---- if (currentSha === storedSha) { console.log('No changes detected. Nothing to do.') return } - // ---- Generate diff ---- fs.mkdirSync(STATE_DIR, { recursive: true }) let diffContent: string @@ -217,11 +196,13 @@ async function main(): Promise { }) diff = run(`git diff ${storedSha} HEAD -- ${SOURCE_PATH}`, { cwd: sourceDir, silent: true }) } catch { - nameStatus = '(unable to diff — stored SHA may have been force-pushed away)' + nameStatus = '(unable to diff: stored SHA may have been force-pushed away)' diff = '(diff unavailable)' } - // No source doc files changed — skip the agent. + // Empty means no doc files changed. + // A leading "(" means the diff itself failed, + // so fall through and run the agent anyway. if (!nameStatus.startsWith('(') && !nameStatus.trim()) { console.log( `No changes in ${SOURCE_PATH} between ${storedSha.slice(0, 7)} and ${currentSha.slice(0, 7)}. Skipping agent run.`, @@ -241,9 +222,8 @@ async function main(): Promise { diff, ].join('\n') } else { - // Initial run or full scan — list all source doc files so the agent - // has a concrete inventory (mirrors what git diff --name-status provides - // for incremental runs). + // Initial run or full scan, so list every source doc. + // Incremental runs get this inventory from git diff --name-status instead. const sourceDocs = path.join(sourceDir, SOURCE_PATH) let fileList: string try { @@ -253,7 +233,7 @@ async function main(): Promise { } diffContent = [ - '# Source docs (full scan — no previous SHA)', + '# Source docs (full scan, no previous SHA)', '', 'No previous SHA stored. Perform a full scan of all source docs.', '', @@ -270,17 +250,15 @@ async function main(): Promise { if (DRY_RUN) { console.log('\n--- Diff preview ---') - // Show first 80 lines of the diff to keep output manageable const lines = diffContent.split('\n') const preview = lines.slice(0, 80).join('\n') console.log(preview) if (lines.length > 80) { console.log(`\n... (${lines.length - 80} more lines, see ${DIFF_FILE})`) } - console.log('\nDry run — agent will run but SHA will not be saved.\n') + console.log('\nDry run: agent will run but SHA will not be saved.\n') } - // ---- Run the agent ---- const sourceDocs = path.join(sourceDir, SOURCE_PATH) const prompt = [ @@ -322,9 +300,8 @@ async function main(): Promise { }, ) - // ---- Update stored SHA ---- if (DRY_RUN) { - console.log(`\nDry run — skipping SHA update (${SHA_FILE} not modified).`) + console.log(`\nDry run: skipping SHA update (${SHA_FILE} not modified).`) } else { fs.writeFileSync(SHA_FILE, `${currentSha}\n`) console.log(`\nUpdated ${SHA_FILE} to ${currentSha}`) diff --git a/src/content-render/unified/module-types.d.ts b/src/content-render/unified/module-types.d.ts index 298ecdc023c5..c40e0619c094 100644 --- a/src/content-render/unified/module-types.d.ts +++ b/src/content-render/unified/module-types.d.ts @@ -1,9 +1,3 @@ -declare module 'remark-gemoji-to-emoji' { - import type { Plugin } from 'unified' - const plugin: Plugin - export default plugin -} - declare module 'remark-remove-comments' { import type { Plugin } from 'unified' const plugin: Plugin diff --git a/src/content-render/unified/processor.ts b/src/content-render/unified/processor.ts index b114437519f9..9b6b03a1ebd3 100644 --- a/src/content-render/unified/processor.ts +++ b/src/content-render/unified/processor.ts @@ -1,7 +1,7 @@ import { unified } from 'unified' import remarkParse from 'remark-parse' import gfm from 'remark-gfm' -import emoji from 'remark-gemoji-to-emoji' +import emoji from 'remark-gemoji' import remark2rehype from 'remark-rehype' import raw from 'rehype-raw' import slug from 'rehype-slug' diff --git a/src/deployments/production/build-scripts/clone-or-use-cached-repo.sh b/src/deployments/production/build-scripts/clone-or-use-cached-repo.sh index 6bac6c38288b..a908885ab652 100644 --- a/src/deployments/production/build-scripts/clone-or-use-cached-repo.sh +++ b/src/deployments/production/build-scripts/clone-or-use-cached-repo.sh @@ -1,8 +1,7 @@ set -e -# We use this function to use the cached version of the repo if it exists from -# a previous Dockerfile build. Otherwise, we clone the repo and check out the -# specified branch/SHA. +# Reuses the repo cached by a previous Dockerfile build, or clones it fresh +# and checks out the given branch/SHA. # Arguments: # $1 - Repository name (for directory naming) # $2 - Repository URL @@ -18,7 +17,6 @@ clone_or_use_cached_repo() { echo "Repository '$repo_name' already exists. Fetching updates..." cd "$repo_name" - # Fetch latest changes if ! git fetch origin "$branch"; then echo "❌ Failed to fetch repository '$repo_name'" cd .. @@ -41,7 +39,7 @@ clone_or_use_cached_repo() { else echo "Cloning repository '$repo_name' from branch '$branch'..." - # We only need the most recent change for production deploys, so we use --depth 1 + # Production deploys only need the most recent commit. if ! git clone --depth 1 --branch "$branch" "https://${GITHUB_TOKEN}@github.com/github/$repo_url.git" "$repo_name"; then echo "❌ Failed to clone repository '$repo_name'" return 1 diff --git a/src/deployments/production/build-scripts/fetch-repos.sh b/src/deployments/production/build-scripts/fetch-repos.sh index c2a9107cbbee..3f2239fc448e 100644 --- a/src/deployments/production/build-scripts/fetch-repos.sh +++ b/src/deployments/production/build-scripts/fetch-repos.sh @@ -1,35 +1,23 @@ #!/usr/bin/env sh -# -# This script is intended to be called from the production Dockerfile -# Though it isn't working with all of the files from docs-internal (it only COPYs what is needed), -# it is useful to think of these scripts running from the root of the docs-internal repo. -# +# Called from the production Dockerfile. The Dockerfile only COPYs what it +# needs, but these scripts still run as if from the docs-internal root. -# Fetches and resolves docs-internal, early-access, and translations repos echo "Fetching and resolving early-access, and translations repos" -# Exit immediately if a command exits with a non-zero status set -e -# Import the clone_or_use_cached_repo function . ./build-scripts/clone-or-use-cached-repo.sh -# Set the GITHUB_TOKEN environment variable from the mounted --secret passed to Docker build +# From the --secret mounted by the Docker build. GITHUB_TOKEN=$(cat /run/secrets/DOCS_BOT_PAT_BASE) -# - - - - - - - - - - -# Early access -# - - - - - - - - - - echo "Fetching early access..." clone_or_use_cached_repo "docs-early-access" "docs-early-access" "main" echo "Merging early access..." . ./build-scripts/merge-early-access.sh -# - - - - - - - - - - -# Clone the translations repos -# - - - - - - - - - - -# Make sure to clone each translation repo into the `translations` directory inside the root of docs-internal (the Dockerfile's WORKDIR) +# Clone into `translations/` inside the Dockerfile's WORKDIR, the docs-internal root. mkdir -p translations cd translations @@ -60,8 +48,5 @@ fi # Go back to the root of the docs-internal repo cd .. -# - - - - - - - - - - -# Cleanup -# - - - - - - - - - - -# Delete GITHUB_TOKEN from the environment +# Don't leave the token in the environment. unset GITHUB_TOKEN diff --git a/src/deployments/production/build-scripts/merge-early-access.sh b/src/deployments/production/build-scripts/merge-early-access.sh index b7e711b1660c..317f81a9dc87 100755 --- a/src/deployments/production/build-scripts/merge-early-access.sh +++ b/src/deployments/production/build-scripts/merge-early-access.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh -# Takes docs-early-access files and merges them into docs-internal -# Assumed that it is being run from the root of the docs-internal repo +# Merges docs-early-access files into docs-internal. Runs from the +# docs-internal root. mv docs-early-access/assets/images assets/images/early-access mv docs-early-access/content content/early-access diff --git a/src/dev-toc/generate.ts b/src/dev-toc/generate.ts index 5bc80594a1ce..f883ccbbad96 100644 --- a/src/dev-toc/generate.ts +++ b/src/dev-toc/generate.ts @@ -82,17 +82,13 @@ async function main(): Promise { for (const version of allVersionKeys) { req.pagePath = version === fpt ? '/' : `/${version}` - // Create a subdir for the version if one doesn't exist yet. const versionStaticDir = path.posix.join(staticDir, version) if (!fs.existsSync(versionStaticDir)) fs.mkdirSync(versionStaticDir) - // Create a versioned filename. const filename = path.posix.join(versionStaticDir, 'index.html') - // Create a minimal context object. await contextualize(req, res, next) - // Add the tree to the req.context. if (req.context && req.context.siteTree && req.context.currentVersion) { req.context.currentEnglishTree = req.context.siteTree.en[req.context.currentVersion] } @@ -101,19 +97,15 @@ async function main(): Promise { await recurse(req.context.currentEnglishTree as PageTreeNode) } - // Add any defaultOpenSections to the context. if (req.context) { req.context.defaultOpenSections = defaultOpenSections } - // Parse the layout in src/dev-toc/layout.html with the context we created above. const outputHtml = await liquid.parseAndRender(layout, Object.assign({}, req.context)) - // Write a static file for each version. fs.writeFileSync(filename, outputHtml) } - // Default to FPT for the file to open. const fptFile = path.posix.join(staticDirName, fpt, 'index.html') execSync(`open ${fptFile}`) diff --git a/src/early-access/middleware/early-access-links.ts b/src/early-access/middleware/early-access-links.ts index 95676a3ab7ad..a09dfdc939f5 100644 --- a/src/early-access/middleware/early-access-links.ts +++ b/src/early-access/middleware/early-access-links.ts @@ -29,7 +29,7 @@ export default function earlyAccessContext( if (!req.context || !req.context.pages) throw new Error('request not contextualized') - // Get a list of all hidden pages per version + // Hidden early access pages don't appear in normal navigation, so list them here. const earlyAccessPageLinks = uniq( Object.values(req.context.pages) .filter( @@ -41,14 +41,11 @@ export default function earlyAccessContext( .map((page) => page.permalinks) .flat(), ) - // Get links for the current version .filter((permalink) => req.context!.currentVersion === permalink.pageVersion) .sort() - // Create Markdown links .map((permalink) => `- [${permalink.title}](${permalink.href})`) - // Add to the rendering context - // This is only used in the separate EA repo on local development + // Only read by the separate EA repo, in local development. req.context.earlyAccessPageLinks = earlyAccessPageLinks.length ? earlyAccessPageLinks.join('\n') : '_None for this version!_' diff --git a/src/early-access/scripts/migrate-early-access-product.ts b/src/early-access/scripts/migrate-early-access-product.ts index b5df7a3eff59..ef7e6cf2ae6f 100644 --- a/src/early-access/scripts/migrate-early-access-product.ts +++ b/src/early-access/scripts/migrate-early-access-product.ts @@ -54,7 +54,7 @@ if (!filesToMigrate.length) { const migratePath: string = path.posix.join(contentDir, newPathId) -// 1. Update the image and data refs in the to-be-migrated early access files BEFORE moving them. +// Update the image and data refs in the to-be-migrated early access files BEFORE moving them. try { execFileSync('tsx', [ 'src/early-access/scripts/update-data-and-image-paths.ts', @@ -71,7 +71,7 @@ const variablesToMove: string[] = [] const reusablesToMove: string[] = [] const imagesToMove: string[] = [] -// 2. Add redirects to and update frontmatter in the to-be-migrated early access files BEFORE moving them. +// Add redirects to and update frontmatter in the to-be-migrated early access files BEFORE moving them. for (const filepath of filesToMigrate) { const { content, data } = frontmatter(fs.readFileSync(filepath, 'utf8')) const redirectString: string = filepath @@ -86,7 +86,7 @@ for (const filepath of filesToMigrate) { fs.writeFileSync(filepath, frontmatter.stringify(content || '', data)) } - // 4. Find the data files and images referenced in the early access files so we can move them over. + // Find the data files and images referenced in the early access files so we can move them over. const dataRefs: string[] = content ? content.match(patterns.dataReference) || [] : [] const variables: string[] = dataRefs.filter((ref) => ref.includes('variables')) const reusables: string[] = dataRefs.filter((ref) => ref.includes('reusables')) @@ -97,7 +97,7 @@ for (const filepath of filesToMigrate) { imagesToMove.push(...images) } -// 3. Move the data files and images. +// Move the data files and images. for (const varRef of Array.from(new Set(variablesToMove))) { moveVariable(varRef) } @@ -108,10 +108,10 @@ for (const imageRef of Array.from(new Set(imagesToMove))) { moveImage(imageRef) } -// 4. Move the content files. +// Move the content files. execFileSync('mv', [oldPath, migratePath]) -// 5. Update the parent product TOC with the new child path. +// Update the parent product TOC with the new child path. const parentProductTocPath: string = path.posix.join(path.dirname(newPath), 'index.md') const parentProductToc = frontmatter(fs.readFileSync(parentProductTocPath, 'utf-8')) if (parentProductToc.data && Array.isArray(parentProductToc.data.children)) { @@ -123,7 +123,7 @@ fs.writeFileSync( frontmatter.stringify(parentProductToc.content || '', parentProductToc.data || {}), ) -// 6. Optionally, update the new product TOC with the new title. +// Optionally, update the new product TOC with the new title. if (program.opts().newTitle) { const productTocPath: string = path.posix.join(newPath, 'index.md') const productToc = frontmatter(fs.readFileSync(productTocPath, 'utf-8')) @@ -137,7 +137,7 @@ if (program.opts().newTitle) { ) } -// 7. Update internal links now that the files have been moved. +// Update internal links now that the files have been moved. console.log('\nRunning script to update internal links...') execFileSync('tsx', ['src/links/scripts/update-internal-links.ts']) @@ -163,9 +163,8 @@ function moveVariable(dataRef: string): void { // If early access is part of the path, remove it (since the path below already includes it) .filter((n) => n !== 'early-access') || [] - // Given a string `variables.foo.bar` split into an array, we want the last segment 'bar', which is the variable key. - // Then pop 'bar' off the array because it's not really part of the filepath. - // The filepath we want is `variables/foo.yml`. + // In `variables.foo.bar` the last segment is the variable key. + // Pop it off, leaving the filepath `variables/foo.yml`. const variableKey: string = last(variablePathArray) as string variablePathArray.pop() @@ -243,7 +242,6 @@ function moveReusable(dataRef: string): void { console.log(`Problem migrating files for ${dataRef}`) return } - // return } // If the reusable file doesn't exist, move it. @@ -272,10 +270,9 @@ function moveImage(imageRef: string): void { console.log(`Problem migrating files for ${imageRef}`) return } - // return } - // If the reusable file doesn't exist, move it. + // If the image file doesn't exist, move it. if (!fs.existsSync(newImagePath)) { execFileSync('mkdir', ['-p', path.dirname(newImagePath)]) execFileSync('mv', [oldImagePath, newImagePath]) diff --git a/src/early-access/scripts/symlink-from-local-repo.ts b/src/early-access/scripts/symlink-from-local-repo.ts index fcae6950f165..fcb2a70f9e61 100644 --- a/src/early-access/scripts/symlink-from-local-repo.ts +++ b/src/early-access/scripts/symlink-from-local-repo.ts @@ -8,7 +8,6 @@ import fs from 'fs' import path from 'path' import { program } from 'commander' -// Early Access details const earlyAccessRepo = 'docs-early-access' const earlyAccessDirName = 'early-access' const earlyAccessRepoUrl = `https://github.com/github/${earlyAccessRepo}` @@ -35,7 +34,6 @@ if (!pathToEarlyAccessRepo && !unlink) { let earlyAccessLocalRepoDir: string | undefined -// If creating symlinks, run some extra validation if (!unlink && pathToEarlyAccessRepo) { earlyAccessLocalRepoDir = path.resolve(process.cwd(), pathToEarlyAccessRepo) @@ -74,32 +72,24 @@ for (const dirName of destinationDirNames) { console.log(`- Removed symlink for early access directory '${dirName}' from this repo`) } -// If removing symlinks, just stop here! if (unlink) { process.exit(0) } -// -// Otherwise, keep going... -// - -// Move the latest early access source directories into this repo +// Symlink the latest early access source directories into this repo for (const dirName of destinationDirNames) { if (!earlyAccessLocalRepoDir) continue const sourceDir = path.join(earlyAccessLocalRepoDir, dirName) const destDir = destinationDirsMap[dirName] - // If the source directory doesn't exist, skip it if (!fs.existsSync(sourceDir)) { console.warn(`Early access directory '${dirName}' does not exist. Skipping...`) continue } - // Create a symbolic link to the directory fs.symlinkSync(sourceDir, destDir, 'junction') - // Confirm the newly moved directory exist if (!fs.existsSync(destDir)) { throw new Error(`Failed to symlink early access directory '${dirName}'!`) } diff --git a/src/early-access/scripts/update-data-and-image-paths.ts b/src/early-access/scripts/update-data-and-image-paths.ts index 6b47eb315caf..dc74cc0a69d9 100644 --- a/src/early-access/scripts/update-data-and-image-paths.ts +++ b/src/early-access/scripts/update-data-and-image-paths.ts @@ -73,7 +73,6 @@ if (earlyAccessPath) { selectedFiles = contentFiles.concat(dataFiles) } -// Update the EA content and data files for (const file of selectedFiles) { const oldContents = fs.readFileSync(file, 'utf8') @@ -83,53 +82,41 @@ for (const file of selectedFiles) { const replacements: Record = {} if (add) { - // Since we're adding early-access to the path, filter for those that do not already include it const dataRefsToAdd = dataRefs.filter((ref) => !ref.includes(' early-access.')) for (const dataRef of dataRefsToAdd) { - // Add to the { oldRef: newRef } replacements object replacements[dataRef] = dataRef.replace( /({% (?:data|indented_data_reference) )(.*)/, '$1early-access.$2', ) } - // Since we're adding early-access to the path, filter for those that do not already include it const imageRefsToAdd = imageRefs.filter((ref) => !ref.split('/').includes('early-access')) for (const imageRef of imageRefsToAdd) { - // Add to the { oldRef: newRef } replacements object replacements[imageRef] = imageRef.replace('/assets/images/', '/assets/images/early-access/') } } if (remove) { - // Since we're removing early-access from the path, filter for those that include it const dataRefsToRemove = dataRefs.filter((ref) => ref.includes(' early-access.')) for (const dataRef of dataRefsToRemove) { - // Add to the { oldRef: newRef } replacements object replacements[dataRef] = dataRef.replace('early-access.', '').replace('-alt.', '.') - // replacements[dataRef] = dataRef.replace('early-access.', '') } - // Since we're removing early-access from the path, filter for those that include it const imageRefsToRemove = imageRefs.filter((ref) => ref.split('/').includes('early-access')) for (const imageRef of imageRefsToRemove) { - // Add to the { oldRef: newRef } replacements object replacements[imageRef] = imageRef.replace('/assets/images/early-access/', '/assets/images/') } } - // Return early if nothing to replace if (!Object.keys(replacements).length) { continue } - // Make the replacement in the content let newContents = oldContents for (const [oldRef, newRef] of Object.entries(replacements)) { newContents = newContents.replace(new RegExp(escapeRegExp(oldRef), 'g'), newRef) } - // Write the updated content fs.writeFileSync(file, newContents) } diff --git a/src/eslint-rules/index.js b/src/eslint-rules/index.js index ee69e3a74f00..99e65518b440 100644 --- a/src/eslint-rules/index.js +++ b/src/eslint-rules/index.js @@ -1,4 +1,3 @@ -// Export custom rules from here module.exports = { rules: { "use-custom-logger": require("./use-custom-logger/use-custom-logger"), diff --git a/src/eslint-rules/no-dangerously-set-inner-html/no-dangerously-set-inner-html.js b/src/eslint-rules/no-dangerously-set-inner-html/no-dangerously-set-inner-html.js index c3cd003733d1..75b90dbc8b14 100644 --- a/src/eslint-rules/no-dangerously-set-inner-html/no-dangerously-set-inner-html.js +++ b/src/eslint-rules/no-dangerously-set-inner-html/no-dangerously-set-inner-html.js @@ -7,7 +7,7 @@ module.exports = { category: "Possible Errors", recommended: false, }, - schema: [], // no options + schema: [], messages: { noDanger: "Do not use dangerouslySetInnerHTML. Render trusted HTML with the RenderedHTML component (or renderHTMLString), or pass a hast tree to MarkdownContent. See github/docs-engineering#6619.", diff --git a/src/eslint-rules/no-dangerously-set-inner-html/tests/no-dangerously-set-inner-html.ts b/src/eslint-rules/no-dangerously-set-inner-html/tests/no-dangerously-set-inner-html.ts index 8b267965eb68..399ef0225838 100644 --- a/src/eslint-rules/no-dangerously-set-inner-html/tests/no-dangerously-set-inner-html.ts +++ b/src/eslint-rules/no-dangerously-set-inner-html/tests/no-dangerously-set-inner-html.ts @@ -48,7 +48,6 @@ describe('no-dangerously-set-inner-html', () => { code: `const childProps = { dangerouslySetInnerHTML: { __html: children } }`, errors: [{ messageId: 'noDanger' }], }, - // String-literal key form must also be flagged. { code: `const childProps = { 'dangerouslySetInnerHTML': { __html: children } }`, errors: [{ messageId: 'noDanger' }], diff --git a/src/eslint-rules/use-custom-logger/tests/use-custom-logger.ts b/src/eslint-rules/use-custom-logger/tests/use-custom-logger.ts index a9eb68e4d7db..37aeb287c2d4 100644 --- a/src/eslint-rules/use-custom-logger/tests/use-custom-logger.ts +++ b/src/eslint-rules/use-custom-logger/tests/use-custom-logger.ts @@ -442,8 +442,7 @@ const logger = createLogger(import.meta.url); }) it('should handle logger variable with destructuring pattern', () => { - // This test ensures the rule recognizes logger variables from destructuring patterns - // and doesn't create a duplicate declaration + // A destructured logger already exists, so the fix must not redeclare it. ruleTester.run('use-custom-logger', rule, { valid: [], invalid: [ diff --git a/src/eslint-rules/use-custom-logger/use-custom-logger.js b/src/eslint-rules/use-custom-logger/use-custom-logger.js index e498cb20e7db..05d273fb7a5f 100644 --- a/src/eslint-rules/use-custom-logger/use-custom-logger.js +++ b/src/eslint-rules/use-custom-logger/use-custom-logger.js @@ -7,11 +7,10 @@ module.exports = { recommended: false, }, fixable: "code", - schema: [], // no options + schema: [], }, create(context) { const sourceCode = context.getSourceCode(); - // flag to ensure we add the logger setup only once per file let setupInserted = false; // Check if the logger import is already present. diff --git a/src/events/components/Survey.tsx b/src/events/components/Survey.tsx index 63ee60eb6664..8b90a01e1812 100644 --- a/src/events/components/Survey.tsx +++ b/src/events/components/Survey.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from 'react' -import cx from 'classnames' +import cx from 'clsx' import { useRouter } from 'next/router' import { useTranslation } from '@/languages/components/useTranslation' @@ -39,9 +39,8 @@ export const Survey = () => { const [token, setToken] = useState('') useEffect(() => { - // Always reset the form if navigating to a new page because what - // you might have said or started to say belongs exclusively to - // to the page you started on. + // Send the reader back to the vote prompt on every navigation, + // because a rating belongs to the page it was given on. setState(ViewState.START) setVoteState(null) }, [asPath]) @@ -247,7 +246,6 @@ export const Survey = () => { } function trackEvent(eventData: EventData) { - // Nota bene: convert empty strings to undefined return sendEvent({ type: EventType.survey, survey_token: eventData.token || undefined, // Honeypot diff --git a/src/events/components/dotcom-cookies.ts b/src/events/components/dotcom-cookies.ts index e31369080a04..201047a0cf49 100644 --- a/src/events/components/dotcom-cookies.ts +++ b/src/events/components/dotcom-cookies.ts @@ -22,12 +22,10 @@ const LOCAL_STORAGE_KEY = 'dotcomCookies' async function fetchCookies(): Promise { if (isHeadless()) return { isStaff: false } - // Return the cached object if we have it in memory. if (cachedCookies) { return cachedCookies } - // Try to load from local storage. const storedCookies = localStorage.getItem(LOCAL_STORAGE_KEY) if (storedCookies) { try { @@ -39,12 +37,10 @@ async function fetchCookies(): Promise { } } - // If a request is already in progress, reuse it. if (inFlightPromise) { return inFlightPromise } - // Make a single fetch request to the backend. inFlightPromise = (async () => { try { const response = await fetch(GET_COOKIES_ENDPOINT) @@ -53,7 +49,6 @@ async function fetchCookies(): Promise { } const data = (await response.json()) as DotcomCookies cachedCookies = data - // Store the fetched cookies in local storage for future use. try { localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data)) } catch (e) { @@ -62,14 +57,12 @@ async function fetchCookies(): Promise { return data } catch (err) { console.error('Error fetching cookies:', err) - // On failure, return default values. const defaultCookies: DotcomCookies = { isStaff: false, } cachedCookies = defaultCookies return defaultCookies } finally { - // Clear the in-flight promise regardless of success or failure. inFlightPromise = null } })() diff --git a/src/events/components/events.ts b/src/events/components/events.ts index 127de20ee0d1..96ac18352a71 100644 --- a/src/events/components/events.ts +++ b/src/events/components/events.ts @@ -162,7 +162,7 @@ export function sendEvent({ queueEvent(body) - // Send events to hydro-analytics-client for cross-subdomain tracking + // Hydro handles cross-subdomain tracking. sendHydroAnalyticsEvent(body) if (type === EventType.exit) { @@ -250,18 +250,14 @@ function trackScroll() { pauseScrolling = false }, 200) - // Calculate where we are on the page const scrollPixels = window.scrollY + window.innerHeight const newScrollPosition = scrollPixels / document.documentElement.scrollHeight - // Count scroll flips const newScrollDirection = Math.sign(newScrollPosition - scrollPosition) if (newScrollDirection !== scrollDirection) scrollFlipCount++ - // Update maximum scroll position reached if (newScrollPosition > maxScrollY) maxScrollY = newScrollPosition - // Update before the next event scrollDirection = newScrollDirection scrollPosition = newScrollPosition } @@ -291,7 +287,7 @@ function sendExit() { } function initPageAndExitEvent() { - sendPage() // Initial page hit + sendPage() // Regular page exits window.addEventListener('scroll', trackScroll) @@ -400,7 +396,7 @@ function initLinkEvent() { document.documentElement.addEventListener('click', (evt) => { const target = evt.target as HTMLElement if (!target.closest('.ghd-scroll-to-top')) return - const url = window.location.href.split('#')[0] // Remove hash + const url = window.location.href.split('#')[0] sendEvent({ type: EventType.link, link_url: `${url}#scroll-to-top`, diff --git a/src/events/components/experiments/experiment.ts b/src/events/components/experiments/experiment.ts index 9e21f575f22b..f6180daa60de 100644 --- a/src/events/components/experiments/experiment.ts +++ b/src/events/components/experiments/experiment.ts @@ -25,27 +25,23 @@ export function shouldShowExperiment( experimentKey = experimentKey.key } - // Determine if user is in treatment group. If they are, show the experiment const experiments = getActiveExperiments('all') for (const experiment of experiments) { if (experiment.key === experimentKey) { // Respect isActive so flipping it to false actually stops the experiment if (!experiment.isActive) return false - // If there is an override for the current session, use that if (controlGroupOverride[experiment.key]) { const controlGroup = getExperimentControlGroupFromSession( experimentKey, experiment.percentOfUsersToGetExperiment, ) return controlGroup === TREATMENT_VARIATION - // Otherwise determine if the user is in the treatment group } else if ( (experiment.limitToLanguages?.length ? experiment.limitToLanguages.includes(locale) : true) && (experiment.limitToVersions?.length ? experiment.limitToVersions.includes(version) : true) ) { - // If the user has staffonly cookie, and staff override is true, show the experiment if (experiment.alwaysShowForStaff) { if (isStaff) { userIsStaff = true @@ -83,7 +79,6 @@ if (typeof window !== 'undefined') { controlGroup: 'treatment' | 'control', ): string => { const activeExperiments = getActiveExperiments('all') - // Make sure key is valid if (activeExperiments.some((experiment) => experiment.key === experimentKey)) { controlGroupOverride[experimentKey] = controlGroup const event = new Event('controlGroupOverrideChanged') @@ -97,7 +92,6 @@ if (typeof window !== 'undefined') { } } -// Determine if the user is in the treatment or control group for a given experiment export function getExperimentControlGroupFromSession( experimentKey: ExperimentNames, percentToGetExperiment = 50, @@ -119,7 +113,8 @@ export function getExperimentVariationForContext(locale: string, version: string const experiments = getActiveExperiments(locale, version) for (const experiment of experiments) { if (experiment.includeVariationInContext) { - // If the user is using the URL param to view the experiment, include the variation in the context + // A query string containing `feature=`, or a staff + // reader when alwaysShowForStaff is set, forces the treatment variation. if ( (experiment.turnOnWithURLParam && window.location?.search @@ -176,7 +171,6 @@ export function initializeExperiments( let numberOfExperimentsUsingContext = 0 for (const experiment of experiments) { if (experiment.includeVariationInContext) { - // Validate the experiments object numberOfExperimentsUsingContext++ if (numberOfExperimentsUsingContext > 1) { throw new Error( @@ -209,7 +203,6 @@ export function initializeForwardFeatureUrlParam(router: NextRouter, currentVers try { const searchParams = new URLSearchParams(window.location.search) const featureValue = searchParams.get('feature') - // If the user's URL doesn't include `feature`, we don't need to forward it if (!featureValue) return const updateAnchorHref = (anchor: HTMLAnchorElement): void => { @@ -227,7 +220,6 @@ export function initializeForwardFeatureUrlParam(router: NextRouter, currentVers if (!(event.target instanceof Element)) return const anchor = event.target.closest('a') if (anchor) { - // If we found that the target is an anchor, we need to update and manually navigate to it event.preventDefault() updateAnchorHref(anchor) } @@ -238,7 +230,6 @@ export function initializeForwardFeatureUrlParam(router: NextRouter, currentVers if (!(event.target instanceof Element)) return const anchor = event.target.closest('a') if (anchor) { - // If we found that the target is an anchor, we need to update and manually navigate to it event.preventDefault() updateAnchorHref(anchor) } diff --git a/src/events/components/experiments/experiments.ts b/src/events/components/experiments/experiments.ts index f4fe2f607d5f..813b06bd82d7 100644 --- a/src/events/components/experiments/experiments.ts +++ b/src/events/components/experiments/experiments.ts @@ -21,13 +21,13 @@ export const EXPERIMENTS = { // Placeholder experiment to maintain type compatibility placeholder_experiment: { key: 'placeholder_experiment', - isActive: false, // Inactive placeholder + isActive: false, percentOfUsersToGetExperiment: 0, includeVariationInContext: false, limitToLanguages: [], limitToVersions: [], alwaysShowForStaff: false, - turnOnWithURLParam: 'placeholder', // Placeholder URL param + turnOnWithURLParam: 'placeholder', }, readability_copilot: { key: 'readability_copilot', @@ -68,12 +68,10 @@ export function getActiveExperiments(locale: string, version?: string): Experime include = false } - // Only include experiment if it's supported for the current language if (experiment.limitToLanguages?.length && !experiment.limitToLanguages.includes(locale)) { include = false } - // Only include experiment if it's supported for the current version if (experiment.limitToVersions?.length && !experiment.limitToVersions.includes(version || '')) { include = false } diff --git a/src/events/components/experiments/useShouldShowExperiment.ts b/src/events/components/experiments/useShouldShowExperiment.ts index 28211883f69b..3e6b6c39440d 100644 --- a/src/events/components/experiments/useShouldShowExperiment.ts +++ b/src/events/components/experiments/useShouldShowExperiment.ts @@ -61,7 +61,6 @@ export function useShouldShowExperiment(experimentKey: ExperimentNames | { key: updateShouldShow() - // Event listener to update when controlGroupOverride is called window.addEventListener('controlGroupOverrideChanged', updateShouldShow) return () => { diff --git a/src/events/components/hydro-analytics.ts b/src/events/components/hydro-analytics.ts index 1a1ae51cd3be..55ed58ad2d02 100644 --- a/src/events/components/hydro-analytics.ts +++ b/src/events/components/hydro-analytics.ts @@ -1,20 +1,14 @@ -/** - * Integration with @github/hydro-analytics-client for cross-subdomain tracking. - * - * This sends events to collector.githubapp.com alongside our existing analytics. - * The client auto-collects: page, title, client_id, referrer, user_agent, - * screen_resolution, browser_resolution, browser_languages, pixel_ratio, timestamp, tz_seconds - * - * We send all other docs-specific context fields, including: - * - path_language, path_version, path_product, path_article - * - page_document_type, page_type, content_type - * - color_mode_preference, is_logged_in, experiment_variation, is_headless - * - event_id, page_event_id, octo_client_id - * - Plus any event-specific properties (exit metrics, link_url, etc.) - * - * All functions are wrapped in try/catch to ensure that issues with the - * hydro-analytics-client or collector don't affect our primary analytics. - */ +// Integration with @github/hydro-analytics-client for cross-subdomain tracking. +// Events go to collector.githubapp.com alongside our existing analytics. +// +// The client auto-collects page, title, client_id, referrer, user_agent, +// screen_resolution, browser_resolution, browser_languages, pixel_ratio, +// timestamp, and tz_seconds. We send every other docs-specific context field. +// +// The two entry points, getOctoClientId and sendHydroAnalyticsEvent, are wrapped +// in try/catch so a problem with the client cannot affect our primary analytics. +// That only covers synchronous throws: the client fires its request without +// awaiting it, so a collector network failure never reaches us. import { AnalyticsClient, @@ -22,10 +16,7 @@ import { } from '@github/hydro-analytics-client' import { EventType } from '../types' -/** - * Safe wrapper around hydro-analytics-client's getOrCreateClientId. - * Returns undefined if the client fails for any reason. - */ +// Returns undefined if the client fails for any reason. export function getOctoClientId(): string | undefined { try { return hydroGetOrCreateClientId() @@ -55,11 +46,9 @@ const AUTO_COLLECTED_FIELDS = new Set([ 'title', ]) -/** - * Flatten a nested event body into a single-level context object, - * excluding fields that hydro-analytics-client already auto-collects, - * and adding fields required for analytics_v0_page_view compatibility. - */ +// Flattens a nested event body into a single-level context object, dropping +// fields the client already auto-collects and adding the ones +// analytics_v0_page_view needs. export function prepareData(body: Record): { type: string context: Record @@ -88,13 +77,10 @@ export function prepareData(body: Record): { return { type: typeof type === 'string' ? type : 'unknown', context } } -/** - * Send an event to hydro-analytics-client. - * For page events, sends as a page view. For all other events, sends as a custom event. - * - * This is wrapped in try/catch to ensure that if the hydro collector is down - * or errors, it doesn't affect our primary analytics pipeline. - */ +// Page events go out as a page view, everything else as a custom event. +// +// Wrapped in try/catch so a broken hydro client cannot affect our primary +// analytics pipeline. export function sendHydroAnalyticsEvent(body: Record): void { try { const { type, context } = prepareData(body) diff --git a/src/events/lib/analyze-comment.ts b/src/events/lib/analyze-comment.ts index e51cafbf0677..f76a151f1250 100644 --- a/src/events/lib/analyze-comment.ts +++ b/src/events/lib/analyze-comment.ts @@ -92,14 +92,6 @@ export async function getGuessedLanguage(comment: string) { const lang = await getLanguageInstance() const bestGuess = lang.guessBest(comment.trim(), []) if (!bestGuess) return // Can happen if the text is just whitespace - // // @horizon-rs/language-guesser is based on tri-grams and can lead - // // to false positives. For example, it thinks that 'Thamk you ❤️🙏' is - // // Haitian! And that 'I wanne robux 1000' is Polish! - // // But that's because they are short and there's not enough clues to - // // guess what language it is. You and I might know those are actually - // // attempts to be English, despite the spelling. - // // But are they useful comments? Given that this is just a signal, - // // and not a hard blocker, it's more of a clue than a fact. return bestGuess.alpha2 || undefined } diff --git a/src/events/lib/get-document-type.ts b/src/events/lib/get-document-type.ts index 1fca81649eba..628bfc210137 100644 --- a/src/events/lib/get-document-type.ts +++ b/src/events/lib/get-document-type.ts @@ -1,13 +1,8 @@ -/** - * Document types used by the system - */ type DocumentType = 'homepage' | 'product' | 'category' | 'subcategory' | 'article' | 'early-access' -/** - * This function derives the document type from the *relative path* segment length, - * where a relative path refers to the content path starting with the product dir. - * For example: actions/index.md or github/getting-started-with-github/quickstart.md. - */ +// Derives the document type from the number of segments in the relative path, +// meaning the content path starting at the product directory. +// For example: actions/index.md or github/getting-started-with-github/quickstart.md export default function getDocumentType(relativePath: string): DocumentType { // A non-index file is ALWAYS considered an article in this approach, // even if it's at the category level (like actions/quickstart.md) diff --git a/src/events/lib/hydro.ts b/src/events/lib/hydro.ts index 267fb22d1b5a..995d36b5aba8 100644 --- a/src/events/lib/hydro.ts +++ b/src/events/lib/hydro.ts @@ -85,7 +85,6 @@ async function _publish( body, requestBody, }) - // Report the error to Failbot report(error, { statusCode, body, requestBody }) } else { throw error diff --git a/src/events/lib/middleware-errors.ts b/src/events/lib/middleware-errors.ts index bcae7fd498ca..a711c05869ef 100644 --- a/src/events/lib/middleware-errors.ts +++ b/src/events/lib/middleware-errors.ts @@ -23,7 +23,6 @@ export function formatErrors(errors: ErrorObject[], body: unknown) { raw: makeString(body), // We convert to snake_case because dealing with case in SQL is unfortunate. - // Ensure the result is a string or undefined ...Object.fromEntries( Object.entries(pick(error, errorKeys)).map(([key, value]) => [ snakeCase(key), @@ -33,7 +32,6 @@ export function formatErrors(errors: ErrorObject[], body: unknown) { })) } -// Leave strings alone, otherwise convert to either string or undefined function makeString(value: unknown) { return typeof value === 'string' ? value : JSON.stringify(value) } diff --git a/src/events/lib/schema.ts b/src/events/lib/schema.ts index cde4a14077b9..e80a1fb153aa 100644 --- a/src/events/lib/schema.ts +++ b/src/events/lib/schema.ts @@ -230,7 +230,7 @@ const context = { description: 'The variation this user we bucketed in is in, such as control or treatment.', }, - // Event Grouping. The comination of key + id should be unique + // Event grouping. The combination of key + id should be unique. event_group_key: { type: 'string', description: 'A enum indentifier (e.g. "ask-ai") used to put events into a specific group.', diff --git a/src/events/middleware.ts b/src/events/middleware.ts index 53655be36f5c..be4823305315 100644 --- a/src/events/middleware.ts +++ b/src/events/middleware.ts @@ -64,7 +64,6 @@ router.post( for (const eventBody of eventsToProcess) { try { - // Skip event if it doesn't have a type or if the type is not in the allowed types if (!eventBody.type || !allowedTypes.has(eventBody.type)) { continue } @@ -79,13 +78,11 @@ router.post( } if (body.context) { - // Add dotcom_user to the context if it's available // JSON.stringify removes `undefined` values but not `null`, and we don't want to send `null` to Hydro body.context.dotcom_user = req.cookies?.[DOTCOM_USER_COOKIE_NAME] ? req.cookies[DOTCOM_USER_COOKIE_NAME] : undefined body.context.is_staff = Boolean(req.cookies?.[STAFFONLY_COOKIE_NAME]) - // Add IP address and user agent from request // Moda forwards the client's IP using the `fastly-client-ip` header body.context.ip = req.headers['fastly-client-ip'] as string | undefined body.context.user_agent ??= req.headers['user-agent'] diff --git a/src/events/scripts/analyze-comment-cli.ts b/src/events/scripts/analyze-comment-cli.ts index 389e5fe3a24f..e9cbf1b3b375 100644 --- a/src/events/scripts/analyze-comment-cli.ts +++ b/src/events/scripts/analyze-comment-cli.ts @@ -1,14 +1,10 @@ -/** - * This script can be used to debug and test our signals. - * Example use: - * - * npm run analyze-comment -- "I love this site\!" --verbose - * - * or, using stdin: - * - * cat naught-comment.txt | npm run analyze-comment - * - */ +// Debugs and tests our comment signals. +// +// npm run analyze-comment -- "I love this site\!" --verbose +// +// or, using stdin: +// +// cat naughty-comment.txt | npm run analyze-comment import fs from 'node:fs' import util from 'node:util' @@ -42,7 +38,7 @@ async function main(comment?: string, options?: Options) { } console.log(chalk.grey('Comment:'), chalk.bold(util.inspect(comment))) - console.log('') // whitespace + console.log() const language = options?.language || 'en' @@ -68,7 +64,8 @@ async function main(comment?: string, options?: Options) { console.log(chalk.green(`Not triggered on ${chalk.bold(name)}`)) } } - console.log('') // whitespace + + console.log() if (!broke) { console.log(chalk.whiteBright(`Final rating: ${chalk.bold(rating.toFixed(1))}`)) } diff --git a/src/events/scripts/analyze-comments-csv.ts b/src/events/scripts/analyze-comments-csv.ts index d40ed7a373ba..386113cce0de 100644 --- a/src/events/scripts/analyze-comments-csv.ts +++ b/src/events/scripts/analyze-comments-csv.ts @@ -1,10 +1,6 @@ -/** - * This script is used to analyze posted survey comments in a CSV file. - * The CSV file is expected to have come from the Azure Data Explorer - * after having queries the `docs_v0_survey_event` table. - * - * - */ +// Analyzes posted survey comments in a CSV file. +// The CSV is expected to come from the Azure Data Explorer, after querying the +// `docs_v0_survey_event` table. import fs from 'node:fs' import util from 'node:util' diff --git a/src/events/tests/analyze-comments.ts b/src/events/tests/analyze-comments.ts index a917435a9a16..b8f5a99adbae 100644 --- a/src/events/tests/analyze-comments.ts +++ b/src/events/tests/analyze-comments.ts @@ -4,14 +4,12 @@ import { analyzeComment, getGuessedLanguage } from '../lib/analyze-comment' describe('analyzeComment', () => { test('email only', async () => { - // Yes { const { signals, rating } = await analyzeComment(' foo@example.com \n') expect(signals.includes('email-only')).toBeTruthy() expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('one@example.com\ntwo@example.com') expect(signals.includes('email-only')).toBeFalsy() @@ -29,7 +27,6 @@ describe('analyzeComment', () => { }) test('url only', async () => { - // Yes { const { signals, rating } = await analyzeComment(' https://github.com ') expect(signals.includes('url-only')).toBeTruthy() @@ -43,7 +40,6 @@ describe('analyzeComment', () => { expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('https://example.com but more text') expect(signals.includes('url-only')).toBeFalsy() @@ -55,7 +51,6 @@ describe('analyzeComment', () => { }) test('numbers only', async () => { - // Yes { const { signals, rating } = await analyzeComment(' 1234 ') expect(signals.includes('numbers-only')).toBeTruthy() @@ -67,7 +62,6 @@ describe('analyzeComment', () => { expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('123 fake street') expect(signals.includes('numbers-only')).toBeFalsy() @@ -79,7 +73,6 @@ describe('analyzeComment', () => { }) test('all uppercase', async () => { - // Yes { const { signals, rating } = await analyzeComment(' SHOUTING ') expect(signals.includes('all-uppercase')).toBeTruthy() @@ -91,7 +84,6 @@ describe('analyzeComment', () => { expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('Not All Uppercase') expect(signals.includes('all-uppercase')).toBeFalsy() @@ -99,7 +91,6 @@ describe('analyzeComment', () => { }) test('too-short', async () => { - // Yes { const { signals, rating } = await analyzeComment('Oneword ') expect(signals.includes('too-short')).toBeTruthy() @@ -121,7 +112,6 @@ describe('analyzeComment', () => { expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('A\nB\nC\nD') expect(signals.includes('too-short')).toBeFalsy() @@ -133,7 +123,6 @@ describe('analyzeComment', () => { }) test('single-word', async () => { - // Yes { const { signals, rating } = await analyzeComment(' Word ') expect(signals.includes('single-word')).toBeTruthy() @@ -150,7 +139,6 @@ describe('analyzeComment', () => { expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('One two') expect(signals.includes('single-word')).toBeFalsy() @@ -162,7 +150,6 @@ describe('analyzeComment', () => { }) test('not-language', async () => { - // Yes { const { signals, rating } = await analyzeComment('Garçon') expect(signals.includes('not-language')).toBeTruthy() @@ -175,7 +162,6 @@ describe('analyzeComment', () => { expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('english words longer sentence this time') expect(signals.includes('not-language')).toBeFalsy() @@ -205,7 +191,6 @@ describe('analyzeComment', () => { }) test('mostly-emoji', async () => { - // Yes { const { signals, rating } = await analyzeComment('Hello World\n 😆 😆 😆 😆 😆 😆\n') expect(signals.includes('mostly-emoji')).toBeTruthy() @@ -217,7 +202,6 @@ describe('analyzeComment', () => { expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('Great 👍') expect(signals.includes('mostly-emoji')).toBeFalsy() @@ -225,7 +209,6 @@ describe('analyzeComment', () => { }) test('spammy-words', async () => { - // Yes { const { signals, rating } = await analyzeComment('Roblox free roblux') expect(signals.includes('spammy-words')).toBeTruthy() @@ -237,7 +220,6 @@ describe('analyzeComment', () => { expect(rating).toBeLessThan(1.0) } - // No { const { signals } = await analyzeComment('GitHub is great!') expect(signals.includes('spammy-words')).toBeFalsy() @@ -250,7 +232,6 @@ describe('analyzeComment', () => { }) test('guessed-language', async () => { - // Yes { const guessedLanguage = await getGuessedLanguage('Garçon des la voituré') expect(guessedLanguage).toBe('fr') diff --git a/src/events/tests/middleware.ts b/src/events/tests/middleware.ts index 0dc9363197b5..dcfc71e0536d 100644 --- a/src/events/tests/middleware.ts +++ b/src/events/tests/middleware.ts @@ -106,7 +106,7 @@ describe('POST /events', () => { test('should require a type', async () => { const { statusCode } = await checkEvent({ ...pageExample, type: undefined }) - // should skip events with no type + // Events with no type are skipped, not rejected, so the batch still succeeds. expect(statusCode).toBe(200) }) diff --git a/src/frame/components/CodeTabsGroup.tsx b/src/frame/components/CodeTabsGroup.tsx index d6b701399090..19cd15af0a0b 100644 --- a/src/frame/components/CodeTabsGroup.tsx +++ b/src/frame/components/CodeTabsGroup.tsx @@ -16,7 +16,7 @@ import { } from 'react' import { useRouter } from 'next/router' import { UnderlineNav } from '@primer/react' -import cx from 'classnames' +import cx from 'clsx' import Cookies from '@/frame/components/lib/cookies' import { CODE_SAMPLE_LANGUAGE_COOKIE_NAME } from '@/frame/lib/constants' diff --git a/src/frame/components/DefaultLayout.tsx b/src/frame/components/DefaultLayout.tsx index f61788f770e1..a07e50a126f5 100644 --- a/src/frame/components/DefaultLayout.tsx +++ b/src/frame/components/DefaultLayout.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import Head from 'next/head' import { useRouter } from 'next/router' -import cx from 'classnames' +import cx from 'clsx' import { SidebarNav } from '@/frame/components/sidebar/SidebarNav' import { Header } from '@/frame/components/page-header/Header' diff --git a/src/frame/components/HighlightedCode.tsx b/src/frame/components/HighlightedCode.tsx index db1d48207990..3d10336032ff 100644 --- a/src/frame/components/HighlightedCode.tsx +++ b/src/frame/components/HighlightedCode.tsx @@ -6,7 +6,7 @@ import { createLowlight } from 'lowlight' import json from 'highlight.js/lib/languages/json' import javascript from 'highlight.js/lib/languages/javascript' import hljsCurl from 'highlightjs-curl' -import cx from 'classnames' +import cx from 'clsx' // React-native replacement for the imperative ClientSideHighlightJS enhancer // (#6619). The old enhancer scanned the document for `[data-highlight] code` and diff --git a/src/frame/components/article/ArticleGridLayout.tsx b/src/frame/components/article/ArticleGridLayout.tsx index 93d4cb43db52..20f27b114b67 100644 --- a/src/frame/components/article/ArticleGridLayout.tsx +++ b/src/frame/components/article/ArticleGridLayout.tsx @@ -1,5 +1,5 @@ import React from 'react' -import cx from 'classnames' +import cx from 'clsx' import { SupportPortalVaIframe, SupportPortalVaIframeProps } from './SupportPortalVaIframe' import { useSidebarCollapsed } from '@/frame/components/sidebar/SidebarCollapseContext' diff --git a/src/frame/components/article/ArticleInlineLayout.tsx b/src/frame/components/article/ArticleInlineLayout.tsx index c7db7db096bd..70b88cc75168 100644 --- a/src/frame/components/article/ArticleInlineLayout.tsx +++ b/src/frame/components/article/ArticleInlineLayout.tsx @@ -1,5 +1,5 @@ import React from 'react' -import cx from 'classnames' +import cx from 'clsx' import { SupportPortalVaIframe, SupportPortalVaIframeProps } from './SupportPortalVaIframe' import styles from './ArticleInlineLayout.module.scss' diff --git a/src/frame/components/article/ArticleTitle.tsx b/src/frame/components/article/ArticleTitle.tsx index 6cf4f5f396ac..069172df74f1 100644 --- a/src/frame/components/article/ArticleTitle.tsx +++ b/src/frame/components/article/ArticleTitle.tsx @@ -1,5 +1,5 @@ import React from 'react' -import cx from 'classnames' +import cx from 'clsx' import styles from './ArticleTitle.module.scss' type Props = { diff --git a/src/frame/components/article/ViewMarkdownButton.tsx b/src/frame/components/article/ViewMarkdownButton.tsx index d85be6a60b87..9ec4ff6f905d 100644 --- a/src/frame/components/article/ViewMarkdownButton.tsx +++ b/src/frame/components/article/ViewMarkdownButton.tsx @@ -14,7 +14,7 @@ import { MARKDOWN_SOURCE_MENU_EVENT_GROUP } from '@/events/components/event-grou import { sendEvent } from '@/events/components/events' import { EventType } from '@/events/types' import { useTranslation } from '@/languages/components/useTranslation' -import cx from 'classnames' +import cx from 'clsx' import styles from './ViewMarkdownButton.module.scss' interface CopyMarkdownMenuProps { diff --git a/src/frame/components/context/MainContext.tsx b/src/frame/components/context/MainContext.tsx index de15901f3f9c..75b101a9c0d2 100644 --- a/src/frame/components/context/MainContext.tsx +++ b/src/frame/components/context/MainContext.tsx @@ -1,5 +1,5 @@ import { createContext, useContext } from 'react' -import pick from 'lodash/pick' +import { pick } from 'lodash-es' import type { Response } from 'express' import type { BreadcrumbT } from '@/frame/components/page-header/Breadcrumbs' diff --git a/src/frame/components/page-footer/DocsFooter.tsx b/src/frame/components/page-footer/DocsFooter.tsx index d2c29cdbd782..484c4c22ec7c 100644 --- a/src/frame/components/page-footer/DocsFooter.tsx +++ b/src/frame/components/page-footer/DocsFooter.tsx @@ -1,7 +1,7 @@ import { type MouseEvent } from 'react' import { useRouter } from 'next/router' import { MinimalFooter, Text } from '@primer/react-brand' -import cx from 'classnames' +import cx from 'clsx' import { FooterDivider } from '@/frame/components/page-footer/FooterDivider' import { SupportSection } from '@/frame/components/page-footer/SupportSection' diff --git a/src/frame/components/page-footer/SupportSection.tsx b/src/frame/components/page-footer/SupportSection.tsx index 282b1ee61972..7d2366d9794b 100644 --- a/src/frame/components/page-footer/SupportSection.tsx +++ b/src/frame/components/page-footer/SupportSection.tsx @@ -1,4 +1,4 @@ -import cx from 'classnames' +import cx from 'clsx' import { Survey } from '@/events/components/Survey' import { Contribution } from '@/frame/components/page-footer/Contribution' diff --git a/src/frame/components/page-header/Breadcrumbs.tsx b/src/frame/components/page-header/Breadcrumbs.tsx index f77fad5981b1..cf6ac585308d 100644 --- a/src/frame/components/page-header/Breadcrumbs.tsx +++ b/src/frame/components/page-header/Breadcrumbs.tsx @@ -1,6 +1,6 @@ import { type MouseEvent, useCallback } from 'react' import { useRouter } from 'next/router' -import cx from 'classnames' +import cx from 'clsx' import { Breadcrumbs as BrandBreadcrumbs } from '@primer/react-brand' import { useMainContext } from '../context/MainContext' diff --git a/src/frame/components/page-header/BreadcrumbsScroller.tsx b/src/frame/components/page-header/BreadcrumbsScroller.tsx index 83754e8cd05f..b8063a374f40 100644 --- a/src/frame/components/page-header/BreadcrumbsScroller.tsx +++ b/src/frame/components/page-header/BreadcrumbsScroller.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import type { FocusEvent } from 'react' -import cx from 'classnames' +import cx from 'clsx' import { IconButton } from '@primer/react' import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react' diff --git a/src/frame/components/page-header/DocsSecondaryBar.tsx b/src/frame/components/page-header/DocsSecondaryBar.tsx index 3ac83e83378e..eef5c84c6ed9 100644 --- a/src/frame/components/page-header/DocsSecondaryBar.tsx +++ b/src/frame/components/page-header/DocsSecondaryBar.tsx @@ -1,4 +1,4 @@ -import cx from 'classnames' +import cx from 'clsx' import { useRouter } from 'next/router' import { IconButton } from '@primer/react' import { SidebarCollapseIcon, SidebarExpandIcon } from '@primer/octicons-react' diff --git a/src/frame/components/page-header/HeaderNotifications.tsx b/src/frame/components/page-header/HeaderNotifications.tsx index d8fdc06ca6fd..ae7ebc94cbe0 100644 --- a/src/frame/components/page-header/HeaderNotifications.tsx +++ b/src/frame/components/page-header/HeaderNotifications.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/router' -import cx from 'classnames' +import cx from 'clsx' import { XIcon } from '@primer/octicons-react' import { useLanguages } from '@/languages/components/LanguagesContext' diff --git a/src/frame/components/sidebar/AllProductsLink.tsx b/src/frame/components/sidebar/AllProductsLink.tsx index c9056124aa37..446b92c661df 100644 --- a/src/frame/components/sidebar/AllProductsLink.tsx +++ b/src/frame/components/sidebar/AllProductsLink.tsx @@ -1,6 +1,6 @@ import { useRouter } from 'next/router' import { ArrowLeftIcon } from '@primer/octicons-react' -import cx from 'classnames' +import cx from 'clsx' import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion' import { Link } from '@/frame/components/Link' import { useTranslation } from '@/languages/components/useTranslation' diff --git a/src/frame/components/sidebar/SidebarNav.tsx b/src/frame/components/sidebar/SidebarNav.tsx index 8563cba7d52e..3b1830ea88ff 100644 --- a/src/frame/components/sidebar/SidebarNav.tsx +++ b/src/frame/components/sidebar/SidebarNav.tsx @@ -1,4 +1,4 @@ -import cx from 'classnames' +import cx from 'clsx' import { useRouter } from 'next/router' import { useMainContext } from '@/frame/components/context/MainContext' diff --git a/src/frame/components/ui/Alert/Alert.tsx b/src/frame/components/ui/Alert/Alert.tsx index 497da298e148..5af0da32ac97 100644 --- a/src/frame/components/ui/Alert/Alert.tsx +++ b/src/frame/components/ui/Alert/Alert.tsx @@ -1,5 +1,5 @@ import { createElement, ReactNode } from 'react' -import cx from 'classnames' +import cx from 'clsx' import styles from './Alert.module.scss' import { InfoIcon, ReportIcon, AlertIcon, LightBulbIcon, StopIcon } from '@primer/octicons-react' import { useTranslation } from '@/languages/components/useTranslation' diff --git a/src/frame/components/ui/Lead/Lead.tsx b/src/frame/components/ui/Lead/Lead.tsx index 18297b6a4dbb..236172a15544 100644 --- a/src/frame/components/ui/Lead/Lead.tsx +++ b/src/frame/components/ui/Lead/Lead.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react' import type { JSX } from 'react' -import cx from 'classnames' +import cx from 'clsx' import styles from './Lead.module.scss' import { RenderedHTML } from '@/frame/components/ui/RenderedHTML' diff --git a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx index 6ae4bd9dafe6..0b1f4d9dbf38 100644 --- a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx +++ b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx @@ -3,7 +3,7 @@ import type { JSX } from 'react' import { Fragment, jsx, jsxs } from 'react/jsx-runtime' import { toJsxRuntime } from 'hast-util-to-jsx-runtime' import type { Root as HastRoot } from 'hast' -import cx from 'classnames' +import cx from 'clsx' import { markdownComponents } from './markdownComponents' import { renderHTMLString } from '@/frame/components/ui/RenderedHTML/render-html-string' diff --git a/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx b/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx index 5b03961433e4..ada9872e77f3 100644 --- a/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx +++ b/src/frame/components/ui/MarkdownContent/UnrenderedMarkdownContent.tsx @@ -2,7 +2,7 @@ import ReactMarkdown from 'react-markdown' import type { Components } from 'react-markdown' import type { JSX } from 'react' import remarkGfm from 'remark-gfm' -import cx from 'classnames' +import cx from 'clsx' import { IconButton } from '@primer/react' import { CopyIcon, CheckIcon } from '@primer/octicons-react' import { announce } from '@primer/live-region-element' diff --git a/src/frame/components/ui/MiniTocs/MiniTocShared.tsx b/src/frame/components/ui/MiniTocs/MiniTocShared.tsx index 63fe1e1028a9..8157fc321e2b 100644 --- a/src/frame/components/ui/MiniTocs/MiniTocShared.tsx +++ b/src/frame/components/ui/MiniTocs/MiniTocShared.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react' import { NavList } from '@primer/react-brand' -import cx from 'classnames' +import cx from 'clsx' import type { MiniTocItem } from '@/frame/components/context/ArticleContext' import { ArticleContext } from '@/frame/components/context/ArticleContext' diff --git a/src/frame/components/ui/MiniTocs/MiniTocs.tsx b/src/frame/components/ui/MiniTocs/MiniTocs.tsx index 576afcca08a1..d5013835271b 100644 --- a/src/frame/components/ui/MiniTocs/MiniTocs.tsx +++ b/src/frame/components/ui/MiniTocs/MiniTocs.tsx @@ -1,5 +1,5 @@ import { Heading, NavList } from '@primer/react-brand' -import cx from 'classnames' +import cx from 'clsx' import type { MiniTocItem } from '@/frame/components/context/ArticleContext' import { useTranslation } from '@/languages/components/useTranslation' diff --git a/src/frame/components/ui/MiniTocs/OverviewMenu.tsx b/src/frame/components/ui/MiniTocs/OverviewMenu.tsx index 2b48c44b18b9..ffef11bfa2e5 100644 --- a/src/frame/components/ui/MiniTocs/OverviewMenu.tsx +++ b/src/frame/components/ui/MiniTocs/OverviewMenu.tsx @@ -1,6 +1,6 @@ import { useEffect, useId, useRef, useState } from 'react' import { NavList } from '@primer/react-brand' -import cx from 'classnames' +import cx from 'clsx' import { ChevronDownIcon } from '@primer/octicons-react' import type { MiniTocItem } from '@/frame/components/context/ArticleContext' diff --git a/src/frame/components/ui/MiniTocs/UpNext.tsx b/src/frame/components/ui/MiniTocs/UpNext.tsx index f45128cf6af1..9a554101b2c0 100644 --- a/src/frame/components/ui/MiniTocs/UpNext.tsx +++ b/src/frame/components/ui/MiniTocs/UpNext.tsx @@ -1,6 +1,6 @@ import { useEffect, useId, useRef, useState } from 'react' import { ArrowRightIcon } from '@primer/octicons-react' -import cx from 'classnames' +import cx from 'clsx' import { Link } from '@/frame/components/Link' import { RenderedHTML } from '@/frame/components/ui/RenderedHTML' diff --git a/src/frame/components/ui/ScrollButton/ScrollButton.tsx b/src/frame/components/ui/ScrollButton/ScrollButton.tsx index 8510eebc805d..526a511aee2f 100644 --- a/src/frame/components/ui/ScrollButton/ScrollButton.tsx +++ b/src/frame/components/ui/ScrollButton/ScrollButton.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import cx from 'classnames' +import cx from 'clsx' import { ChevronUpIcon } from '@primer/octicons-react' import styles from './ScrollButton.module.scss' diff --git a/src/frame/pages/app.tsx b/src/frame/pages/app.tsx index 6c0edb05f810..386c5c496705 100644 --- a/src/frame/pages/app.tsx +++ b/src/frame/pages/app.tsx @@ -51,7 +51,7 @@ function getFaviconHref(stagingName?: string) { // surrogate key so a production deploy does not purge it. // If you edit these images on disk, change the numbers. if (stagingName) { - return `/assets/cb-345/images/site/evergreens/${stagingName}.png` + return `/assets/cb-346/images/site/evergreens/${stagingName}.png` } return '/assets/cb-345/images/site/favicon.png' } diff --git a/src/frame/tests/secure-files.ts b/src/frame/tests/secure-files.ts index f741ee91df2d..9c760ecd3bc6 100644 --- a/src/frame/tests/secure-files.ts +++ b/src/frame/tests/secure-files.ts @@ -1,7 +1,6 @@ import fs from 'fs/promises' import { describe, expect, test } from 'vitest' -import { glob } from 'glob' /* * Verify that a list of file paths are present and optionally have a CODEOWNERS entry @@ -29,7 +28,7 @@ const codeOwners = codeOwnersFile.split(/\r?\n/) describe('Secure file paths are present and have code owners if required', () => { for (const file of secureFiles) { test(`secure file(s) check for: ${file.name}`, async () => { - const matchingFiles = await glob(file.path) + const matchingFiles = await Array.fromAsync(fs.glob(file.path)) expect(matchingFiles.length, `Expected to find content in "${file.path}"`).toBeGreaterThan(0) if (file.requiredCodeOwner) { diff --git a/src/frame/tests/server.ts b/src/frame/tests/server.ts index 8543c653d313..f97554d740ca 100644 --- a/src/frame/tests/server.ts +++ b/src/frame/tests/server.ts @@ -1,5 +1,3 @@ -// csp-parse doesn't have TypeScript types -import CspParse from 'csp-parse' import { beforeAll, describe, expect, test, vi } from 'vitest' import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases' @@ -17,6 +15,21 @@ interface Category { published_articles: string[] } +// Parses a Content-Security-Policy header into its directives. Mirrors the +// behavior of the unmaintained `csp-parse` package it replaces: the policy is +// lowercased, split on `;`, and each directive's values are returned as a +// space-joined string, or an empty string when the directive is absent. +function parseCsp(policy: string) { + const directives = new Map() + for (const part of (policy || '').toLowerCase().split(';')) { + const [name, ...values] = part.trim().split(/\s+/) + if (name) directives.set(name, values.join(' ')) + } + return { + get: (directive: string) => directives.get(directive) || '', + } +} + describe('server', () => { vi.setConfig({ testTimeout: 60 * 1000 }) @@ -50,7 +63,7 @@ describe('server', () => { expect(res.statusCode).toBe(200) expect('content-security-policy' in res.headers).toBe(true) - const csp = new CspParse(res.headers['content-security-policy']) + const csp = parseCsp(res.headers['content-security-policy']) expect(csp.get('default-src')).toBe("'none'") expect(csp.get('font-src').includes("'self'")).toBe(true) diff --git a/src/github-apps/components/PermissionsList.tsx b/src/github-apps/components/PermissionsList.tsx index 64fe8f92edb0..89f708cda041 100644 --- a/src/github-apps/components/PermissionsList.tsx +++ b/src/github-apps/components/PermissionsList.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import cx from 'classnames' +import cx from 'clsx' import { slug as githubSlug } from 'github-slugger' import { CheckIcon, XIcon } from '@primer/octicons-react' @@ -61,7 +61,6 @@ export function PermissionsList({ const rootPath = currentVersion === DEFAULT_VERSION ? `/${locale}` : `/${locale}/${currentVersion}` - // Translated strings const { t } = useTranslation('rest') const ENDPOINTS_TH = t('rest.overview.permissions.endpoints') const ACCESS_TH = t('rest.overview.permissions.access') diff --git a/src/github-apps/lib/index.ts b/src/github-apps/lib/index.ts index df71d862d168..9fef828acf55 100644 --- a/src/github-apps/lib/index.ts +++ b/src/github-apps/lib/index.ts @@ -12,14 +12,12 @@ interface AppsConfig { pages: Record } -// Per-page apps data shapes vary (enabled lists, permissions, etc.), so callers -// are expected to provide the concrete shape via the generic parameter. +// Apps data shapes vary by page, so callers narrow this through the generic +// parameter on getAppsData. type AppsData = Record // Deduplicated on-disk format types. -// A leaf entry in the shared pool — an operation or permission object. Kept -// loose because consumers expect different shapes, but typed enough to convey -// intent and replace bare `unknown`. +// A leaf entry in the shared pool: an operation or permission object. type SharedAppsEntry = Record // Per-page index for permission pages: permName → metadata + indices into the pool. @@ -45,7 +43,7 @@ const logger = createLogger(import.meta.url) const ENABLED_APPS_DIR = 'src/github-apps/data' const githubAppsData = new Map>() -// Shared dedup data — loaded once, shared across all versions +// Shared dedup data, loaded once and shared across all versions. let sharedEntries: SharedAppsEntry[] | null = null let sharedVersionIndex: AppsVersionIndex | null = null let sharedFormatAvailable: boolean | null = null @@ -75,10 +73,10 @@ function loadSharedAppsFormat(): boolean { sharedFormatAvailable = true } catch (err) { if (isFileNotFoundError(err)) { - // Shared files don't exist — fall back to per-version files silently. + // Shared files don't exist, so fall back to per-version files silently. sharedFormatAvailable = false } else { - // Corrupt JSON, schema mismatch, etc. — surface this instead of hiding it. + // Corrupt JSON, schema mismatch, and so on. Surface it instead of hiding it. logger.error('Failed to load shared GitHub Apps dedup format (corrupt data?)', { error: err instanceof Error ? err : new Error(String(err)), }) @@ -159,13 +157,12 @@ export async function getAppsData( const filename = `${pageType}.json` const openApiVersion = getOpenApiVersion(docsVersion) + (apiVersion ? `-${apiVersion}` : '') if (!pageTypeMap.has(openApiVersion)) { - // Try shared deduplicated format first const data = reconstructAppsFromSharedFormat(pageType, openApiVersion) if (data) { pageTypeMap.set(openApiVersion, data) } else { - // Fall back to per-version JSON file. The `readCompressedJsonFileFallback()` - // function will check for both a .br and .json extension. + // Fall back to per-version JSON. + // readCompressedJsonFileFallback checks for both a .br and a .json extension. const appDataPath = path.join(ENABLED_APPS_DIR, openApiVersion, filename) pageTypeMap.set(openApiVersion, readCompressedJsonFileFallback(appDataPath) as AppsData) } @@ -197,14 +194,13 @@ export async function getAppsServerSideProps( : allVersions[currentVersion].latestApiVersion const appsItems: AppsData = await getAppsData(pageType, currentVersion, apiVersion) - // Create minitoc const { miniTocItems } = getAutomatedPageContextFromRequest(req) const titles: string[] = useDisplayTitle ? Object.values(appsItems).map((item) => (item as AppsItemWithDisplayTitle).displayTitle!) : Object.keys(appsItems) - // Note: getAutomatedPageMiniTocItems expects a `Context`, but this code path - // historically passed Next.js's GetServerSidePropsContext at runtime. Keep - // that runtime behavior but document the divergence via the assertion below. + // getAutomatedPageMiniTocItems expects a `Context`, but this code path has + // always passed Next.js's GetServerSidePropsContext at runtime. + // Hence the double assertion. const appMiniToc = await getAutomatedPageMiniTocItems(titles, context as unknown as Context) if (appMiniToc) { miniTocItems.push(...appMiniToc) diff --git a/src/github-apps/scripts/sync.ts b/src/github-apps/scripts/sync.ts index 9ffcf052ba42..84fcfba10a66 100755 --- a/src/github-apps/scripts/sync.ts +++ b/src/github-apps/scripts/sync.ts @@ -131,7 +131,6 @@ export async function syncGitHubAppsData( ) as OpenApiData const appsDataConfig = JSON.parse(await readFile(CONFIG_FILE, 'utf8')) as AppsDataConfig - // Initialize the data structure with keys for each page type const githubAppsData: GitHubAppsData = {} for (const pageType of Object.keys(appsDataConfig.pages)) { githubAppsData[pageType] = {} @@ -140,7 +139,6 @@ export async function syncGitHubAppsData( // rendered content we can parse the dereferenced files directly for (const [requestPath, operationsAtPath] of Object.entries(schemaData.paths)) { for (const [verb, operation] of Object.entries(operationsAtPath)) { - // We only want to process operations that have programmatic access data if (!progAccessData[operation.operationId]) continue const isInstallationAccessToken = progAccessData[operation.operationId].serverToServer @@ -158,19 +156,15 @@ export async function syncGitHubAppsData( { category }, appDataOperation, ) - // server-to-server if (isInstallationAccessToken) { addAppData(githubAppsData['server-to-server-rest'], category, appDataOperation) } - // user-to-server if (isUserAccessToken) { addAppData(githubAppsData['user-to-server-rest'], category, appDataOperation) } - // fine-grained pat if (isFineGrainedPat) { - // Check if all permission sets for this operation are excluded for fine-grained PATs const allPermissionSetsExcluded = progAccessData[operation.operationId].permissions.every( (permissionSet) => Object.keys(permissionSet).every((permissionName) => @@ -187,7 +181,6 @@ export async function syncGitHubAppsData( } } - // permissions for (const permissionSet of progAccessData[operation.operationId].permissions) { for (const [permissionName, readOrWrite] of Object.entries(permissionSet)) { const { title, displayTitle } = getDisplayTitle(permissionName, progActorResources) @@ -214,7 +207,6 @@ export async function syncGitHubAppsData( continue } - // github app permissions if (!isActorExcluded(excludedActors, 'server_to_server', actorTypeMap)) { const serverToServerPermissions = githubAppsData['server-to-server-permissions'] if (!serverToServerPermissions[permissionName]) { @@ -247,7 +239,6 @@ export async function syncGitHubAppsData( ) } - // fine-grained pats const isExcluded = isActorExcluded(excludedActors, 'fine_grained_pat', actorTypeMap) if (isFineGrainedPat && !isExcluded) { @@ -309,7 +300,6 @@ export async function syncGitHubAppsData( } } - // Write deduplicated shared format await writeDeduplicatedAppsFormat() } @@ -385,7 +375,6 @@ async function writeDeduplicatedAppsFormat() { } } - // Write shared files const sharedDir = path.join(ENABLED_APPS_DIR, 'shared') if (!existsSync(sharedDir)) { await mkdirp(sharedDir) @@ -406,7 +395,6 @@ export async function getProgAccessData( isRest = false, ): Promise<{ progAccessData: ProgAccessData; progActorResources: ProgActorResources }> { const useRemoteGitHubFiles = progAccessSource === 'rest-api-description' - // check for required PAT if (useRemoteGitHubFiles && !process.env.GITHUB_TOKEN) { throw new Error( 'Error! You must have the GITHUB_TOKEN environment variable set to access the programmatic access and resource files via the GitHub REST API.', @@ -453,7 +441,6 @@ export async function getProgAccessData( disabledForPatV2: operation.disabled_for_patv2, } - // Handle comma-separated operation IDs const operationIds = operation.operation_ids.split(',').map((id) => id.trim()) for (const operationId of operationIds) { progAccessData[operationId] = operationData @@ -552,9 +539,6 @@ function sentenceCase(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1) } -/** - * Calculates whether an operation has additional permissions beyond a single permission. - */ export function calculateAdditionalPermissions( permissionSets: Array>, ): boolean { @@ -564,10 +548,7 @@ export function calculateAdditionalPermissions( ) } -/** - * Determines whether a metadata permission should be filtered out when it has additional permissions. - * Prevents misleading documentation where mutating operations appear to only need metadata access. - */ +// Without this, a mutating operation appears to need only metadata access. export function shouldFilterMetadataPermission( permissionName: string, permissionSets: Array>, @@ -588,21 +569,17 @@ export function isActorExcluded( return false } - // Map generic actor type to actual YAML value if mapping exists const mappedActorType = actorMapping[actorType] || actorType - // Check if the mapped actor type is excluded if (excludedActors.includes(mappedActorType)) { return true } - // Also check for the original actor type (before mapping) if (excludedActors.includes(actorType)) { return true } - // Check for known aliases - the source data might use different values - // than what we expect in our mapping + // The source data sometimes uses values our mapping does not expect. if (actorType === 'fine_grained_pat' && excludedActors.includes('UserProgrammaticAccess')) { return true } @@ -665,7 +642,6 @@ async function getProgActorResourceContent({ path: resourcePath, gitHubSourceDirectory = null, }: ProgActorResourceContentOptions): Promise { - // Get files either locally from disk or from the GitHub remote repo let files: string[] if (gitHubSourceDirectory) { files = await getProgActorContentFromDisk(gitHubSourceDirectory) @@ -675,13 +651,10 @@ async function getProgActorResourceContent({ ) } - // We need to format the file content into a single object. Each file - // contains a single key and a single value that needs to be added - // to the object. + // Each file holds a single key and value, so merge them into one object. const progActorResources: ProgActorResources = {} for (const file of files) { const fileContent = load(file) as Record - // Each file should only contain a single key and value. if (Object.keys(fileContent).length !== 1) { throw new Error(`Error: The file ${JSON.stringify(fileContent)} must only have one key.`) } diff --git a/src/github-apps/tests/deduplication.ts b/src/github-apps/tests/deduplication.ts index 20c1a066068e..3a2503bec523 100644 --- a/src/github-apps/tests/deduplication.ts +++ b/src/github-apps/tests/deduplication.ts @@ -47,20 +47,17 @@ describe('GitHub Apps deduplication', () => { fs.readFileSync(path.join(ENABLED_APPS_DIR, 'version-index.json'), 'utf8'), ) - // Test a non-permissions page type for (const [version, pageTypes] of Object.entries( index as Record>, )) { for (const [pageType, pageData] of Object.entries(pageTypes as Record)) { if (pageType.includes('permissions')) continue - // Reconstruct const reconstructed: Record = {} for (const [category, indices] of Object.entries(pageData as Record)) { reconstructed[category] = indices.map((idx: number) => entries[idx]) } - // Compare with original const originalPath = path.join(ENABLED_APPS_DIR, version, `${pageType}.json`) if (!fs.existsSync(originalPath)) continue const original = JSON.parse(fs.readFileSync(originalPath, 'utf8')) @@ -89,7 +86,6 @@ describe('GitHub Apps deduplication', () => { for (const [pageType, pageData] of Object.entries(pageTypes as Record)) { if (!pageType.includes('permissions')) continue - // Reconstruct const reconstructed: Record< string, { title: string; displayTitle: string; permissions: unknown[] } @@ -104,7 +100,6 @@ describe('GitHub Apps deduplication', () => { } } - // Compare with original const originalPath = path.join(ENABLED_APPS_DIR, version, `${pageType}.json`) if (!fs.existsSync(originalPath)) continue const original = JSON.parse(fs.readFileSync(originalPath, 'utf8')) diff --git a/src/github-apps/tests/excluded-actors.ts b/src/github-apps/tests/excluded-actors.ts index b9e02c5c111d..7cbaa790f24e 100644 --- a/src/github-apps/tests/excluded-actors.ts +++ b/src/github-apps/tests/excluded-actors.ts @@ -41,7 +41,6 @@ describe('excluded_actors filtering', () => { }) test('handles actor type mapping from generic to YAML values', () => { - // Test with actual YAML values that would come from the config files expect(isActorExcluded(['UserProgrammaticAccess'], 'fine_grained_pat', actorTypeMap)).toBe(true) expect(isActorExcluded(['github_app'], 'server_to_server', actorTypeMap)).toBe(true) expect(isActorExcluded(['user_access_token'], 'user_to_server', actorTypeMap)).toBe(true) @@ -54,39 +53,32 @@ describe('excluded_actors filtering', () => { test('handles mixed generic and YAML actor type values', () => { const mixedExcludedActors = ['UserProgrammaticAccess', 'github_app', 'user_access_token'] - // Should match mapped values expect(isActorExcluded(mixedExcludedActors, 'fine_grained_pat', actorTypeMap)).toBe(true) expect(isActorExcluded(mixedExcludedActors, 'server_to_server', actorTypeMap)).toBe(true) expect(isActorExcluded(mixedExcludedActors, 'user_to_server', actorTypeMap)).toBe(true) - // Should not match unmapped values expect(isActorExcluded(mixedExcludedActors, 'unmapped_actor', actorTypeMap)).toBe(false) }) test('verifies independent filtering of server_to_server and user_to_server', () => { - // Only server_to_server excluded const onlyServerExcluded = ['server_to_server'] expect(isActorExcluded(onlyServerExcluded, 'server_to_server')).toBe(true) expect(isActorExcluded(onlyServerExcluded, 'user_to_server')).toBe(false) - // Only user_to_server excluded const onlyUserExcluded = ['user_to_server'] expect(isActorExcluded(onlyUserExcluded, 'server_to_server')).toBe(false) expect(isActorExcluded(onlyUserExcluded, 'user_to_server')).toBe(true) - // Both excluded const bothExcluded = ['server_to_server', 'user_to_server'] expect(isActorExcluded(bothExcluded, 'server_to_server')).toBe(true) expect(isActorExcluded(bothExcluded, 'user_to_server')).toBe(true) - // Neither excluded const neitherExcluded = ['fine_grained_pat'] expect(isActorExcluded(neitherExcluded, 'server_to_server')).toBe(false) expect(isActorExcluded(neitherExcluded, 'user_to_server')).toBe(false) }) test('handles actor type mapping from generic to YAML values', () => { - // Test with actual YAML values that would come from the config files expect( isActorExcluded(['fine_grained_personal_access_token'], 'fine_grained_pat', actorTypeMap), ).toBe(true) @@ -105,28 +97,22 @@ describe('excluded_actors filtering', () => { 'user_access_token', ] - // Should match mapped values expect(isActorExcluded(mixedExcludedActors, 'fine_grained_pat', actorTypeMap)).toBe(true) expect(isActorExcluded(mixedExcludedActors, 'server_to_server', actorTypeMap)).toBe(true) - // Should match mapped values expect(isActorExcluded(mixedExcludedActors, 'user_to_server', actorTypeMap)).toBe(true) - // Should not match unmapped values expect(isActorExcluded(mixedExcludedActors, 'unmapped_actor', actorTypeMap)).toBe(false) }) test('handles UserProgrammaticAccess alias for fine_grained_pat', () => { - // Test that UserProgrammaticAccess (actual source data value) is recognized as fine_grained_pat expect(isActorExcluded(['UserProgrammaticAccess'], 'fine_grained_pat')).toBe(true) - // Test mixed scenarios with UserProgrammaticAccess const mixedWithUserProgrammatic = ['UserProgrammaticAccess', 'github_app'] expect(isActorExcluded(mixedWithUserProgrammatic, 'fine_grained_pat')).toBe(true) expect(isActorExcluded(mixedWithUserProgrammatic, 'server_to_server', actorTypeMap)).toBe(true) expect(isActorExcluded(mixedWithUserProgrammatic, 'user_to_server')).toBe(false) - // Test that both mapped value and alias work const bothValues = ['fine_grained_personal_access_token', 'UserProgrammaticAccess'] expect(isActorExcluded(bothValues, 'fine_grained_pat', actorTypeMap)).toBe(true) }) diff --git a/src/github-apps/tests/metadata-permissions.ts b/src/github-apps/tests/metadata-permissions.ts index 4dc7e088e213..3bdc2e025b9a 100644 --- a/src/github-apps/tests/metadata-permissions.ts +++ b/src/github-apps/tests/metadata-permissions.ts @@ -31,7 +31,6 @@ interface MetadataPermission { } describe('metadata permissions filtering', () => { - // Mock data structure representing operations with metadata permissions const mockOperationsWithMetadata: Operation[] = [ { operationId: 'repos/enable-automated-security-fixes', @@ -51,7 +50,6 @@ describe('metadata permissions filtering', () => { }, ] - // Mock programmatic access data const mockProgAccessData: Record = { 'repos/enable-automated-security-fixes': { userToServerRest: true, @@ -75,7 +73,6 @@ describe('metadata permissions filtering', () => { }, } - // Mock actor resources const mockProgActorResources: Record = { metadata: { title: 'Metadata', @@ -96,29 +93,22 @@ describe('metadata permissions filtering', () => { } test('calculateAdditionalPermissions works correctly', () => { - // Single permission set with multiple permissions expect(calculateAdditionalPermissions([{ metadata: 'read', admin: 'write' }])).toBe(true) - // Single permission set with single permission expect(calculateAdditionalPermissions([{ metadata: 'read' }])).toBe(false) - // Multiple permission sets expect(calculateAdditionalPermissions([{ metadata: 'read' }, { admin: 'write' }])).toBe(true) - // Empty permission sets expect(calculateAdditionalPermissions([])).toBe(false) }) test('identifies metadata with additional permissions correctly', () => { - // Case 1: metadata + administration (should be filtered) const metadataWithAdmin = [{ metadata: 'read', administration: 'write' }] expect(shouldFilterMetadataPermission('metadata', metadataWithAdmin)).toBe(true) - // Case 2: metadata only (should NOT be filtered) const metadataOnly = [{ metadata: 'read' }] expect(shouldFilterMetadataPermission('metadata', metadataOnly)).toBe(false) - // Case 3: non-metadata permission (should NOT be filtered) const nonMetadata = [{ contents: 'read' }] expect(shouldFilterMetadataPermission('contents', nonMetadata)).toBe(false) }) @@ -136,9 +126,7 @@ describe('metadata permissions filtering', () => { const additionalPermissions = calculateAdditionalPermissions(progData.permissions) - // Apply metadata filtering logic if (shouldFilterMetadataPermission(permissionName, progData.permissions)) { - // Skip this metadata permission as it has additional permissions continue } @@ -163,7 +151,6 @@ describe('metadata permissions filtering', () => { expect(metadataPermissions[0].operationId).toBe('repos/get-readme') expect(metadataPermissions[0].additionalPermissions).toBe(false) - // Should have other permissions from operations with additional permissions const adminPermission = filteredOperations.find((op) => op.permission === 'administration') expect(adminPermission).toBeDefined() expect(adminPermission!.operationId).toBe('repos/enable-automated-security-fixes') @@ -186,23 +173,22 @@ describe('metadata permissions filtering', () => { expect(nonMetadataOperations).toHaveLength(1) expect(nonMetadataOperations[0].operationId).toBe('repos/get-content') - // Verify contents permission would be preserved const contentsPermissionSet = mockProgAccessData['repos/get-content'].permissions[0] expect('contents' in contentsPermissionSet).toBe(true) expect('metadata' in contentsPermissionSet).toBe(false) }) test('handles edge cases in permission sets', () => { - // Empty permission set expect(shouldFilterMetadataPermission('metadata', [])).toBe(false) - // Permission set with empty object (edge case) - const edgeCase1: Record[] = [{ metadata: 'read' }, {}] - expect(shouldFilterMetadataPermission('metadata', edgeCase1)).toBe(true) + const metadataWithEmptySet: Record[] = [{ metadata: 'read' }, {}] + expect(shouldFilterMetadataPermission('metadata', metadataWithEmptySet)).toBe(true) - // Multiple permission sets with metadata in different sets - const edgeCase2: Record[] = [{ metadata: 'read' }, { admin: 'write' }] - expect(shouldFilterMetadataPermission('metadata', edgeCase2)).toBe(true) + const metadataInSeparateSet: Record[] = [ + { metadata: 'read' }, + { admin: 'write' }, + ] + expect(shouldFilterMetadataPermission('metadata', metadataInSeparateSet)).toBe(true) }) test('filters metadata permissions that match the GitHub issue examples', () => { @@ -213,11 +199,9 @@ describe('metadata permissions filtering', () => { // DELETE /orgs/{org}/actions/permissions/repositories/{repository_id} const deleteActionsPermissions = [{ metadata: 'read', organization_administration: 'write' }] - // These should be filtered out because they have metadata + additional permissions expect(shouldFilterMetadataPermission('metadata', putActionsPermissions)).toBe(true) expect(shouldFilterMetadataPermission('metadata', deleteActionsPermissions)).toBe(true) - // But the organization_administration permissions should NOT be filtered expect( shouldFilterMetadataPermission('organization_administration', putActionsPermissions), ).toBe(false) @@ -227,28 +211,23 @@ describe('metadata permissions filtering', () => { }) test('preserves metadata permissions that are standalone', () => { - // Example of a metadata-only permission that should be preserved const metadataOnlyPermissions = [{ metadata: 'read' }] - // This should NOT be filtered out expect(shouldFilterMetadataPermission('metadata', metadataOnlyPermissions)).toBe(false) }) test('handles complex permission structures from real data', () => { - // Multiple permission sets (should filter metadata) const multiplePermissionSets: Record[] = [ { metadata: 'read' }, { administration: 'write' }, ] expect(shouldFilterMetadataPermission('metadata', multiplePermissionSets)).toBe(true) - // Single permission set with multiple permissions (should filter metadata) const multiplePermissionsInSet: Record[] = [ { metadata: 'read', contents: 'write', pull_requests: 'write' }, ] expect(shouldFilterMetadataPermission('metadata', multiplePermissionsInSet)).toBe(true) - // Multiple permission sets where metadata is not in the first set const metadataInSecondSet: Record[] = [ { administration: 'write' }, { metadata: 'read' }, @@ -257,24 +236,16 @@ describe('metadata permissions filtering', () => { }) test('validates filtering logic against known problematic endpoints', () => { - // Based on the issue description, these types of operations should have - // their metadata permissions filtered out: - - // Runner group operations const runnerGroupPermissions = [{ metadata: 'read', organization_administration: 'write' }] - // Organization secrets operations const orgSecretsPermissions = [{ metadata: 'read', organization_secrets: 'write' }] - // Repository operations with admin permissions const repoAdminPermissions = [{ metadata: 'read', administration: 'write' }] - // All of these should filter out metadata expect(shouldFilterMetadataPermission('metadata', runnerGroupPermissions)).toBe(true) expect(shouldFilterMetadataPermission('metadata', orgSecretsPermissions)).toBe(true) expect(shouldFilterMetadataPermission('metadata', repoAdminPermissions)).toBe(true) - // But should preserve the actual required permissions expect( shouldFilterMetadataPermission('organization_administration', runnerGroupPermissions), ).toBe(false) @@ -286,16 +257,12 @@ describe('metadata permissions filtering', () => { test('verifies consistency with additional-permissions flag calculation', () => { const testCases: Array<{ permissionSets: Record[]; expected: boolean }> = [ - // Single permission, single set - no additional permissions { permissionSets: [{ metadata: 'read' }], expected: false }, - // Multiple permissions, single set - has additional permissions { permissionSets: [{ metadata: 'read', admin: 'write' }], expected: true }, - // Single permission, multiple sets - has additional permissions { permissionSets: [{ metadata: 'read' }, { admin: 'write' }], expected: true }, - // Multiple permissions, multiple sets - has additional permissions { permissionSets: [{ metadata: 'read', contents: 'read' }, { admin: 'write' }], expected: true, @@ -306,7 +273,6 @@ describe('metadata permissions filtering', () => { const additionalPermissions = calculateAdditionalPermissions(testCase.permissionSets) const shouldFilter = shouldFilterMetadataPermission('metadata', testCase.permissionSets) - // The filtering logic should match the additional permissions calculation expect(shouldFilter).toBe(additionalPermissions) expect(additionalPermissions).toBe(testCase.expected) } @@ -324,15 +290,12 @@ describe('metadata permissions filtering', () => { permissions: [{ metadata: 'read', organization_administration: 'write' }], } - // This should be filtered out from metadata permissions expect(shouldFilterMetadataPermission('metadata', progData.permissions)).toBe(true) - // But organization_administration permission should still be included expect( shouldFilterMetadataPermission('organization_administration', progData.permissions), ).toBe(false) - // Verify additional permissions flag is set correctly expect(calculateAdditionalPermissions(progData.permissions)).toBe(true) }) }) diff --git a/src/github-apps/tests/sync.ts b/src/github-apps/tests/sync.ts index efc7ef86d0ff..ec8dad3f1810 100644 --- a/src/github-apps/tests/sync.ts +++ b/src/github-apps/tests/sync.ts @@ -80,11 +80,9 @@ describe('getProgAccessData', () => { const result = await processProgAccessDataMock(mockProgAccessDataRaw, mockProgActorResources) - // Both operation IDs should exist expect(result.progAccessData).toHaveProperty('teams/remove-repo-in-org') expect(result.progAccessData).toHaveProperty('teams/remove-repo-legacy') - // Both should have identical data const expectedData = { userToServerRest: true, serverToServer: true, @@ -123,12 +121,10 @@ describe('getProgAccessData', () => { const result = await processProgAccessDataMock(mockProgAccessDataRaw, mockProgActorResources) - // All three operation IDs should exist expect(result.progAccessData).toHaveProperty('operation1') expect(result.progAccessData).toHaveProperty('operation2') expect(result.progAccessData).toHaveProperty('operation3') - // All should have identical data const expectedData = { userToServerRest: false, serverToServer: true, @@ -210,13 +206,11 @@ describe('getProgAccessData', () => { const result = await processProgAccessDataMock(mockProgAccessDataRaw, mockProgActorResources) - // Should have 3 total entries expect(Object.keys(result.progAccessData)).toHaveLength(3) expect(result.progAccessData).toHaveProperty('single-operation') expect(result.progAccessData).toHaveProperty('comma-op1') expect(result.progAccessData).toHaveProperty('comma-op2') - // Single operation should have its own data expect(result.progAccessData['single-operation']).toEqual({ userToServerRest: true, serverToServer: true, @@ -227,7 +221,6 @@ describe('getProgAccessData', () => { basicAuth: false, }) - // Comma-separated operations should have identical data const expectedCommaData = { userToServerRest: true, serverToServer: false, diff --git a/src/graphql/components/BreakingChanges.tsx b/src/graphql/components/BreakingChanges.tsx index 657847082128..c0072c7b5ee4 100644 --- a/src/graphql/components/BreakingChanges.tsx +++ b/src/graphql/components/BreakingChanges.tsx @@ -1,4 +1,4 @@ -import cx from 'classnames' +import cx from 'clsx' import { HeadingLink } from '@/frame/components/article/HeadingLink' import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML' diff --git a/src/graphql/components/Changelog.tsx b/src/graphql/components/Changelog.tsx index ca57a9735d3b..8f83384a2a6b 100644 --- a/src/graphql/components/Changelog.tsx +++ b/src/graphql/components/Changelog.tsx @@ -1,5 +1,5 @@ import React from 'react' -import cx from 'classnames' +import cx from 'clsx' import GithubSlugger from 'github-slugger' import { HeadingLink } from '@/frame/components/article/HeadingLink' diff --git a/src/graphql/components/GraphqlCategoryPage.tsx b/src/graphql/components/GraphqlCategoryPage.tsx index acb91a7e7739..f5408a316c06 100644 --- a/src/graphql/components/GraphqlCategoryPage.tsx +++ b/src/graphql/components/GraphqlCategoryPage.tsx @@ -1,5 +1,5 @@ import { type JSX } from 'react' -import cx from 'classnames' +import cx from 'clsx' import { Enum } from './Enum' import { InputObject } from './InputObject' diff --git a/src/graphql/components/GraphqlPage.tsx b/src/graphql/components/GraphqlPage.tsx index 6af3dc924792..053109f0cf5f 100644 --- a/src/graphql/components/GraphqlPage.tsx +++ b/src/graphql/components/GraphqlPage.tsx @@ -1,5 +1,5 @@ import type { JSX } from 'react' -import cx from 'classnames' +import cx from 'clsx' import { Enum } from './Enum' import { InputObject } from './InputObject' diff --git a/src/graphql/components/Previews.tsx b/src/graphql/components/Previews.tsx index 6bea7d5690ed..91de57cb0313 100644 --- a/src/graphql/components/Previews.tsx +++ b/src/graphql/components/Previews.tsx @@ -1,5 +1,5 @@ import GithubSlugger from 'github-slugger' -import cx from 'classnames' +import cx from 'clsx' import { HeadingLink } from '@/frame/components/article/HeadingLink' import { useTranslation } from '@/languages/components/useTranslation' diff --git a/src/journeys/components/JourneyTrackNav.tsx b/src/journeys/components/JourneyTrackNav.tsx index 337a4c9aada7..0a8923691aa8 100644 --- a/src/journeys/components/JourneyTrackNav.tsx +++ b/src/journeys/components/JourneyTrackNav.tsx @@ -16,8 +16,8 @@ export function JourneyTrackNav({ context }: Props) { const upNext = nextGuide ?? nextTrackFirstGuide if (!upNext) return null - // In-track: show the next article's title. Crossing into a new track: show the - // track's name so the reader knows they're moving on to a new track. + // In-track, show the next article's title. + // Crossing tracks, show the track name so the reader knows they're moving on. const label = nextGuide ? nextGuide.title : nextTrackFirstGuide!.trackTitle const progress = t('up_next_progress') diff --git a/src/journeys/lib/journey-path-resolver.ts b/src/journeys/lib/journey-path-resolver.ts index 80b7f8f24456..566199827fbd 100644 --- a/src/journeys/lib/journey-path-resolver.ts +++ b/src/journeys/lib/journey-path-resolver.ts @@ -59,9 +59,10 @@ type JourneyPage = { }> } -// Cache for journey pages so we only filter all pages once +// All computed once, on first use. +// Guide hrefs containing Liquid can't be resolved ahead of time, +// so they're absent from cachedGuidePaths and set hasDynamicGuides instead. let cachedJourneyPages: JourneyPage[] | null = null -// Cache for guide paths to quickly check if a page is part of any journey let cachedGuidePaths: Set | null = null let hasDynamicGuides = false @@ -100,22 +101,16 @@ function getGuidePaths(pages: Record): Set { } function normalizeGuidePath(path: string): string { - // First ensure we have a leading slash for consistent processing const pathWithSlash = path.startsWith('/') ? path : `/${path}` - // Use the same normalization pattern as other middleware const withoutVersion = getPathWithoutVersion(pathWithSlash) const withoutLanguage = getPathWithoutLanguage(withoutVersion) - // Ensure we always return a path with leading slash for consistent comparison return withoutLanguage && withoutLanguage.startsWith('/') ? withoutLanguage : `/${withoutLanguage || path}` } -/** - * Helper function to fetch guide data (href and title) for a given path - */ async function fetchGuideData( guidePath: string, context: Context, @@ -140,11 +135,7 @@ async function fetchGuideData( } /** - * Resolves the journey context for a given article path. - * - * The journey context includes information about the journey track, the current - * guide's position within that track, and links to the previous and next - * guides if they exist. + * Returns null if the article isn't a guide in any journey track. */ export async function resolveJourneyContext( articlePath: string, @@ -154,32 +145,26 @@ export async function resolveJourneyContext( ): Promise { const normalizedPath = normalizeGuidePath(articlePath) - // Optimization: Fast path check - // If we are not forcing a specific journey page, check our global cache if (!currentJourneyPage) { const guidePaths = getGuidePaths(pages) - // If we have no dynamic guides and this path isn't in our known guides, return null early. if (!hasDynamicGuides && !guidePaths.has(normalizedPath)) { return null } } - // Use the current journey page if provided, otherwise find all journey pages const journeyPages = currentJourneyPage ? [currentJourneyPage] : getJourneyPages(pages) let result: JourneyContext | null = null - // Search through all journey pages for (const journeyPage of journeyPages) { if (!journeyPage.journeyTracks) continue - // Check version compatibility - only show journey navigation if the current version - // is compatible with the journey landing page's versions (journey track articles - // currently inherit the journey landing page's versions) + // Track articles inherit the landing page's versions, + // so a journey that doesn't apply to the current version has no navigation to show. if (journeyPage.versions) { const journeyVersions = getApplicableVersions(journeyPage.versions) if (!journeyVersions.includes(context.currentVersion || '')) { - continue // Skip this journey if current version is not supported + continue } } @@ -188,14 +173,12 @@ export async function resolveJourneyContext( for (const track of journeyPage.journeyTracks) { if (!track.guides || !Array.isArray(track.guides)) continue - // Find if current article is in this track let guideIndex = -1 for (let i = 0; i < track.guides.length; i++) { const guidePath = track.guides[i].href let renderedGuidePath = guidePath - // Handle Liquid conditionals in guide paths if (needsRendering(guidePath)) { try { renderedGuidePath = await executeWithFallback( @@ -204,7 +187,8 @@ export async function resolveJourneyContext( () => guidePath, ) } catch { - // If rendering fails, use the original path rather than erroring + // executeWithFallback rethrows errors it can't fall back from, + // such as any error in English. renderedGuidePath = guidePath } } @@ -221,7 +205,7 @@ export async function resolveJourneyContext( const alternativeNextStep = track.guides[guideIndex].alternativeNextStep || '' let renderedAlternativeNextStep = alternativeNextStep - // Handle Liquid conditionals in branching text which likely has links + // Rendered with links intact, unlike the hrefs above which use textOnly. if (needsRendering(alternativeNextStep)) { try { renderedAlternativeNextStep = await executeWithFallback( @@ -230,14 +214,12 @@ export async function resolveJourneyContext( () => alternativeNextStep, ) } catch { - // If rendering fails, use the original branching text rather than erroring renderedAlternativeNextStep = alternativeNextStep } } - // Build the list of guides available for the current version. - // fetchGuideData returns null for guides that don't exist in the current version, - // so this filters out unavailable guides for correct counts and navigation. + // fetchGuideData returns null for guides missing in the current version. + // Dropping them keeps the counts and prev/next links correct. const availableGuides = ( await Promise.all( track.guides.map(async (guide, i) => { @@ -262,19 +244,16 @@ export async function resolveJourneyContext( alternativeNextStep: renderedAlternativeNextStep, } - // Set up previous guide using the version-filtered list if (filteredIndex > 0) { const prev = availableGuides[filteredIndex - 1] result.prevGuide = { href: prev.href, title: prev.title } } - // Set up next guide using the version-filtered list if (filteredIndex >= 0 && filteredIndex < filteredCount - 1) { const next = availableGuides[filteredIndex + 1] result.nextGuide = { href: next.href, title: next.title } } - // Only populate nextTrackFirstGuide when on the last guide of the filtered track if (filteredIndex === filteredCount - 1) { foundTrackIndex = trackIndex @@ -294,22 +273,20 @@ export async function resolveJourneyContext( } } - break // Found the track, stop searching + break } trackIndex++ } - if (result) break // Found the journey, stop searching + if (result) break } return result } /** - * Resolves journey tracks data from frontmatter, including rendering any Liquid. - * - * Returns an array of JourneyTrack objects with titles, descriptions, and guide links. + * Reads journey tracks from frontmatter, rendering any Liquid they contain. */ export async function resolveJourneyTracks( journeyTracks: JourneyPage['journeyTracks'], @@ -321,7 +298,6 @@ export async function resolveJourneyTracks( const result = await Promise.all( journeyTracks.map(async (track) => { - // Render Liquid templates in title and description const renderedTitle = needsRendering(track.title) ? await renderContent(track.title, context, { textOnly: true }) : track.title diff --git a/src/journeys/middleware/journey-track.ts b/src/journeys/middleware/journey-track.ts index e84a4f085c8c..dd06002faa59 100644 --- a/src/journeys/middleware/journey-track.ts +++ b/src/journeys/middleware/journey-track.ts @@ -35,15 +35,15 @@ export default async function journeyTrack( try { const page = req.context.page as PageWithJourneys - // If this page has journey tracks defined, resolve them for the landing page if (page.journeyTracks) { const resolvedTracks = await resolveJourneyTracks(page.journeyTracks, req.context) - // Store resolved tracks on the page context for later use in getServerSideProps + // Read later by getServerSideProps. page.resolvedJourneyTracks = resolvedTracks } - // Always try to resolve journey context (for navigation on guide articles) + // Unconditional, because guide articles need this + // even though they carry no journeyTracks of their own. const journeyContext = await resolveJourneyContext( req.pagePath || '', req.context.pages || {}, diff --git a/src/journeys/tests/journey-path-resolver.ts b/src/journeys/tests/journey-path-resolver.ts index 461ceae4b05b..e037a54309b0 100644 --- a/src/journeys/tests/journey-path-resolver.ts +++ b/src/journeys/tests/journey-path-resolver.ts @@ -185,8 +185,6 @@ describe('journey-path-resolver', () => { }) test('normalizes article paths without leading slash', async () => { - // The resolver should handle paths without leading slashes - // by normalizing them to match the guide paths in the data const result = await resolveJourneyContext( 'enterprise-onboarding/config', mockPages, @@ -250,7 +248,7 @@ describe('journey-path-resolver', () => { mockRenderContent.mockClear() const result = await resolveJourneyTracks(mockJourneyTracks, mockContext) - // Liquid value passes through the (passthrough) renderer; plain string is unchanged + // The mock renderer passes content through, so the Liquid is unchanged. expect(result[0].timeCommitment).toBe('{% data variables.product.company_short %} 2-4 hours') expect(result[1].timeCommitment).toBe('4-6 hours') @@ -311,7 +309,6 @@ describe('journey-path-resolver', () => { describe('resolveJourneyContext with version-filtered guides', () => { afterEach(() => { - // Restore the default implementation after each test in this block mockGetLinkData.mockImplementation(async (rawLinks: string | string[] | undefined) => { const path = Array.isArray(rawLinks) ? rawLinks[0] : rawLinks if (!path) return undefined diff --git a/src/landings/components/ArticleList.tsx b/src/landings/components/ArticleList.tsx index b0af1ac03a8b..61ac7172925d 100644 --- a/src/landings/components/ArticleList.tsx +++ b/src/landings/components/ArticleList.tsx @@ -21,9 +21,6 @@ export const ArticleList = ({ articles, }: ArticleListPropsT) => { const { t } = useTranslation('product_landing') - // Use TypeScript's "not null assertion" because `mainContext.page` should - // will present in mainContext if it's gotten to the stage of React - // rendering. return ( <> diff --git a/src/landings/components/CategoryLanding.tsx b/src/landings/components/CategoryLanding.tsx index 8378131f8574..995261e8edd7 100644 --- a/src/landings/components/CategoryLanding.tsx +++ b/src/landings/components/CategoryLanding.tsx @@ -87,7 +87,6 @@ export const CategoryLanding = () => { setSelectedComplexity('All') } - // Helper function to find article data from tocItems const findArticleData = (articlePath: string) => { const cleanPath = articlePath.startsWith('/') ? articlePath.slice(1) : articlePath return onlyFlatItems.find( @@ -97,7 +96,6 @@ export const CategoryLanding = () => { ) } - // Process spotlight items to get complete data const processedSpotlight = spotlight?.map((spotlightItem) => { const articleData = findArticleData(spotlightItem.article) @@ -114,7 +112,7 @@ export const CategoryLanding = () => { {router.route === '/[versionId]/rest/[category]' && } - {/* Doesn't matter *where* this is included because it will + {/* Position does not matter, because it will never render anything. It always just return null. */} diff --git a/src/landings/components/SidebarProduct.tsx b/src/landings/components/SidebarProduct.tsx index b8ef0f8a633e..5e66cf0f056d 100644 --- a/src/landings/components/SidebarProduct.tsx +++ b/src/landings/components/SidebarProduct.tsx @@ -71,11 +71,11 @@ function handleNavClick(router: Router, event: MouseEvent, href: st // navigate/prefetch callbacks, instead of every item calling useRouter itself. type SidebarNavValue = { // The real loaded route. Drives aria-current (the semantic "current page") and the - // auto-expanded active ancestor chain — both must reflect the page actually loaded. + // auto-expanded active ancestor chain. Both must reflect the page actually loaded. routePath: string // The in-flight click target, or null. Drives a VISUAL-ONLY optimistic accent bar // (via data-pending) so the click feels acknowledged before the slow - // getServerSideProps page loads — without lying to assistive tech about the current + // getServerSideProps page loads, without lying to assistive tech about the current // page. Once navigation completes, the keyed remount clears it and routePath catches up. pendingHref: string | null navigate: (event: MouseEvent, href: string) => void @@ -146,7 +146,7 @@ export const SidebarProduct = () => { // page is still loading. This SidebarProduct instance persists during the pending // fetch (SidebarNav keys it on asPath, which only changes once navigation completes), // so the state survives the wait and is discarded by the keyed remount when the new - // route lands. aria-current is NOT derived from this — it stays on the loaded route. + // route lands. aria-current is NOT derived from this: it stays on the loaded route. const [pendingHref, setPendingHref] = useState(null) const prefetchHref = usePrefetchOnInteraction() @@ -169,7 +169,7 @@ export const SidebarProduct = () => { useEffect(() => { // Clear the optimistic highlight if a navigation genuinely fails, so it doesn't - // stick on a page that never loaded. Skip cancellations (err.cancelled) — those + // stick on a page that never loaded. Skip cancellations (err.cancelled): those // fire when a second click supersedes the first, and pendingHref already points at // that newer target, which we want to keep highlighted. const clearPending = (err: { cancelled?: boolean }) => { @@ -303,7 +303,7 @@ function ExpandableItem({ // NavList.Item. Brand's NavList.Item forwards style/aria-hidden to its inner