diff --git a/.eslintrc.js b/.eslintrc.js index a28010feb3c0..b8d4a1ead03e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,51 +3,40 @@ module.exports = { browser: true, commonjs: true, es2020: true, - node: true + node: true, }, parser: '@babel/eslint-parser', - extends: [ - 'eslint:recommended', - 'standard', - 'prettier' - ], + extends: ['eslint:recommended', 'standard', 'prettier'], parserOptions: { ecmaVersion: 11, requireConfigFile: 'false', - babelOptions: { configFile: './.babelrc' } + babelOptions: { configFile: './.babelrc' }, }, rules: { 'import/no-extraneous-dependencies': ['error', { packageDir: '.' }], 'node/global-require': ['error'], - 'import/no-dynamic-require': ['error'] + 'import/no-dynamic-require': ['error'], }, overrides: [ { - files: [ - '**/tests/**/*.js' - ], + files: ['**/tests/**/*.js'], env: { - jest: true - } + jest: true, + }, }, { - files: [ - '**/*.tsx', '**/*.ts' - ], - plugins: [ - '@typescript-eslint', - 'jsx-a11y' - ], + files: ['**/*.tsx', '**/*.ts'], + plugins: ['@typescript-eslint', 'jsx-a11y'], extends: ['plugin:jsx-a11y/recommended'], parser: '@typescript-eslint/parser', rules: { - 'camelcase': 'off', + camelcase: 'off', 'no-unused-vars': 'off', 'no-undef': 'off', 'no-use-before-define': 'off', '@typescript-eslint/no-unused-vars': ['error'], 'jsx-a11y/no-onchange': 'off', - } + }, }, - ] + ], } diff --git a/.github/actions-scripts/check-for-enterprise-issues-by-label.js b/.github/actions-scripts/check-for-enterprise-issues-by-label.js index cb8e6baf55e0..ccc1bc35682e 100755 --- a/.github/actions-scripts/check-for-enterprise-issues-by-label.js +++ b/.github/actions-scripts/check-for-enterprise-issues-by-label.js @@ -3,13 +3,17 @@ import { getOctokit } from '@actions/github' import { setOutput } from '@actions/core' -async function run () { +async function run() { const token = process.env.GITHUB_TOKEN const octokit = getOctokit(token) const query = encodeURIComponent('is:open repo:github/docs-internal is:issue') - const deprecationIssues = await octokit.request(`GET /search/issues?q=${query}+label:"enterprise%20deprecation"`) - const releaseIssues = await octokit.request(`GET /search/issues?q=${query}+label:"enterprise%20release"`) + const deprecationIssues = await octokit.request( + `GET /search/issues?q=${query}+label:"enterprise%20deprecation"` + ) + const releaseIssues = await octokit.request( + `GET /search/issues?q=${query}+label:"enterprise%20release"` + ) const isDeprecationIssue = deprecationIssues.data.items.length === 0 ? 'false' : 'true' const isReleaseIssue = releaseIssues.data.items.length === 0 ? 'false' : 'true' setOutput('deprecationIssue', isDeprecationIssue) @@ -17,11 +21,12 @@ async function run () { return `Set outputs deprecationIssue: ${isDeprecationIssue}, releaseIssue: ${isReleaseIssue}` } -run() - .then( - (response) => { console.log(`Finished running: ${response}`) }, - (error) => { - console.log(`#ERROR# ${error}`) - process.exit(1) - } - ) +run().then( + (response) => { + console.log(`Finished running: ${response}`) + }, + (error) => { + console.log(`#ERROR# ${error}`) + process.exit(1) + } +) diff --git a/.github/actions-scripts/create-enterprise-issue.js b/.github/actions-scripts/create-enterprise-issue.js index ddadd20cbddb..f709ee743229 100755 --- a/.github/actions-scripts/create-enterprise-issue.js +++ b/.github/actions-scripts/create-enterprise-issue.js @@ -20,44 +20,46 @@ const numberOfdaysBeforeDeprecationToOpenIssue = 15 // number of days. // // When a milestone is within the specified number of days, a new issue is -// created using the templates in -// .github/actions-scripts/enterprise-server-issue-templates. +// created using the templates in +// .github/actions-scripts/enterprise-server-issue-templates. // // Release issues are then added to the docs content squad board for triage. -// Deprecations issues are owned by docs engineering and are added to the +// Deprecations issues are owned by docs engineering and are added to the // docs engineering squad board automatically when the engineering label is added. // // [end-readme] run() -async function run () { - +async function run() { const milestone = process.argv[2] if (!acceptedMilestones.includes(milestone)) { - console.log('Please specify either \'release\' or \'deprecation\'\n') + console.log("Please specify either 'release' or 'deprecation'\n") console.log('Example: script/open-enterprise-issue.js release') process.exit(1) } // Milestone-dependent values. - const numberOfdaysBeforeMilestoneToOpenIssue = milestone === 'release' - ? numberOfdaysBeforeReleaseToOpenIssue - : numberOfdaysBeforeDeprecationToOpenIssue + const numberOfdaysBeforeMilestoneToOpenIssue = + milestone === 'release' + ? numberOfdaysBeforeReleaseToOpenIssue + : numberOfdaysBeforeDeprecationToOpenIssue - const versionNumber = milestone === 'release' - ? getNextVersionNumber() - : oldestSupported + const versionNumber = milestone === 'release' ? getNextVersionNumber() : oldestSupported if (!versionNumber) { - console.log(`Could not find the next version number after ${latest} in enterprise-dates.json. Try running script/udpate-enterprise-dates.js, then rerun this script.`) + console.log( + `Could not find the next version number after ${latest} in enterprise-dates.json. Try running script/udpate-enterprise-dates.js, then rerun this script.` + ) process.exit(0) } const datesForVersion = enterpriseDates[versionNumber] if (!datesForVersion) { - console.log(`Could not find ${versionNumber} in enterprise-dates.json. Try running script/udpate-enterprise-dates.js, then rerun this script.`) + console.log( + `Could not find ${versionNumber} in enterprise-dates.json. Try running script/udpate-enterprise-dates.js, then rerun this script.` + ) process.exit(0) } @@ -66,11 +68,19 @@ async function run () { // If the milestone is more than the specific days away, exit now. if (daysUntilMilestone > numberOfdaysBeforeMilestoneToOpenIssue) { - console.log(`The ${versionNumber} ${milestone} is not until ${nextMilestoneDate}! An issue will be opened when it is ${numberOfdaysBeforeMilestoneToOpenIssue} days away.`) + console.log( + `The ${versionNumber} ${milestone} is not until ${nextMilestoneDate}! An issue will be opened when it is ${numberOfdaysBeforeMilestoneToOpenIssue} days away.` + ) process.exit(0) } - const milestoneSteps = fs.readFileSync(path.join(process.cwd(), `.github/actions-scripts/enterprise-server-issue-templates/${milestone}-issue.md`), 'utf8') + const milestoneSteps = fs.readFileSync( + path.join( + process.cwd(), + `.github/actions-scripts/enterprise-server-issue-templates/${milestone}-issue.md` + ), + 'utf8' + ) const issueLabels = [`enterprise ${milestone}`, `engineering`] const issueTitle = `[${nextMilestoneDate}] Enterprise Server ${versionNumber} ${milestone} (technical steps)` @@ -88,11 +98,13 @@ async function run () { repo: 'docs-internal', title: issueTitle, body: issueBody, - labels: issueLabels + labels: issueLabels, }) if (issue.status === 201) { // Write the values to disk for use in the workflow. - console.log(`Issue #${issue.data.number} for the ${versionNumber} ${milestone} was opened: ${issue.data.html_url}`) + console.log( + `Issue #${issue.data.number} for the ${versionNumber} ${milestone} was opened: ${issue.data.html_url}` + ) } } catch (error) { console.error(`#ERROR# ${error}`) @@ -100,7 +112,7 @@ async function run () { process.exit(1) } - // Add the release issue to the 'Needs triage' column on the + // Add the release issue to the 'Needs triage' column on the // docs content squad project board: // https://github.com/orgs/github/projects/1773#column-12198119 // Deprecation issues are owned by docs engineering only and will @@ -108,21 +120,21 @@ async function run () { if (milestone === 'release') { try { const addCard = await octokit.request('POST /projects/columns/{column_id}/cards', { - column_id: 12198119, + column_id: 12198119, content_id: issue.data.id, content_type: 'Issue', mediaType: { - previews: [ - 'inertia' - ] - } + previews: ['inertia'], + }, }) if (addCard.status === 201) { // Write the values to disk for use in the workflow. - console.log(`The issue #${issue.data.number} was added to https://github.com/orgs/github/projects/1773#column-12198119.`) - } - } catch(error) { + console.log( + `The issue #${issue.data.number} was added to https://github.com/orgs/github/projects/1773#column-12198119.` + ) + } + } catch (error) { console.error(`#ERROR# ${error}`) console.log(`๐Ÿ›‘ There was an error adding the issue to the project board.`) process.exit(1) @@ -130,19 +142,19 @@ async function run () { } } -function getNextVersionNumber () { +function getNextVersionNumber() { const indexOfLatest = Object.keys(enterpriseDates).indexOf(latest) const indexOfNext = indexOfLatest + 1 return Object.keys(enterpriseDates)[indexOfNext] } -function calculateDaysUntilMilestone (nextMilestoneDate) { +function calculateDaysUntilMilestone(nextMilestoneDate) { const today = new Date().toISOString().slice(0, 10) const differenceInMilliseconds = getTime(nextMilestoneDate) - getTime(today) // Return the difference in days - return Math.floor((differenceInMilliseconds) / (1000 * 60 * 60 * 24)) + return Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24)) } -function getTime (date) { +function getTime(date) { return new Date(date).getTime() } diff --git a/.github/actions-scripts/enterprise-algolia-label.js b/.github/actions-scripts/enterprise-algolia-label.js index 8bf927d936e4..cf5f344df737 100755 --- a/.github/actions-scripts/enterprise-algolia-label.js +++ b/.github/actions-scripts/enterprise-algolia-label.js @@ -20,8 +20,8 @@ if (!(labelsArray && labelsArray.length)) { // Find the relevant label const algoliaLabel = labelsArray - .map(label => label.name) - .find(label => label.startsWith(labelText)) + .map((label) => label.name) + .find((label) => label.startsWith(labelText)) // Exit early if no relevant label is found if (!algoliaLabel) { diff --git a/.github/actions-scripts/openapi-schema-branch.js b/.github/actions-scripts/openapi-schema-branch.js index 07fdd3c1a39d..593b9003c464 100755 --- a/.github/actions-scripts/openapi-schema-branch.js +++ b/.github/actions-scripts/openapi-schema-branch.js @@ -7,23 +7,25 @@ import semver from 'semver' /* * This script performs two checks to prevent shipping development mode OpenAPI schemas: -* - Ensures the `info.version` property is a semantic version. -* In development mode, the `info.version` property is a string -* containing the `github/github` branch name. -* - Ensures the decorated schema matches the dereferenced schema. -* The workflow that calls this script runs `script/rest/update-files.js` -* with the `--decorate-only` switch then checks to see if files changed. -* -*/ + * - Ensures the `info.version` property is a semantic version. + * In development mode, the `info.version` property is a string + * containing the `github/github` branch name. + * - Ensures the decorated schema matches the dereferenced schema. + * The workflow that calls this script runs `script/rest/update-files.js` + * with the `--decorate-only` switch then checks to see if files changed. + * + */ // Check that the `info.version` property is a semantic version const dereferencedDir = path.join(process.cwd(), 'lib/rest/static/dereferenced') const schemas = fs.readdirSync(dereferencedDir) -schemas.forEach(filename => { +schemas.forEach((filename) => { const schema = JSON.parse(fs.readFileSync(path.join(dereferencedDir, filename))) if (!semver.valid(schema.info.version)) { - console.log(`๐Ÿšงโš ๏ธ Your branch contains a development mode OpenAPI schema: ${schema.info.version}. This check is a reminder to not ๐Ÿšข OpenAPI files in development mode. ๐Ÿ›‘`) + console.log( + `๐Ÿšงโš ๏ธ Your branch contains a development mode OpenAPI schema: ${schema.info.version}. This check is a reminder to not ๐Ÿšข OpenAPI files in development mode. ๐Ÿ›‘` + ) process.exit(1) } }) @@ -31,10 +33,12 @@ schemas.forEach(filename => { // Check that the decorated schema matches the dereferenced schema const changedFiles = execSync('git diff --name-only HEAD').toString() -if(changedFiles !== '') { +if (changedFiles !== '') { console.log(`These files were changed:\n${changedFiles}`) - console.log(`๐Ÿšงโš ๏ธ Your decorated and dereferenced schema files don't match. Ensure you're using decorated and dereferenced schemas from the automatically created pull requests by the 'github-openapi-bot' user. For more information, see 'script/rest/README.md'. ๐Ÿ›‘`) - process.exit(1) + console.log( + `๐Ÿšงโš ๏ธ Your decorated and dereferenced schema files don't match. Ensure you're using decorated and dereferenced schemas from the automatically created pull requests by the 'github-openapi-bot' user. For more information, see 'script/rest/README.md'. ๐Ÿ›‘` + ) + process.exit(1) } // All checks pass, ready to ship diff --git a/.github/allowed-actions.js b/.github/allowed-actions.js index 339a3f90e4e9..207122a88553 100644 --- a/.github/allowed-actions.js +++ b/.github/allowed-actions.js @@ -4,36 +4,36 @@ // can be added it this list. export default [ - "actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f", // v2.3.4 - "actions/github-script@2b34a689ec86a68d8ab9478298f91d5401337b7d", // v4.0.2 - "actions/labeler@5f867a63be70efff62b767459b009290364495eb", // v2.2.0 - "actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f", // v2.2.0 - "actions/setup-python@dc73133d4da04e56a135ae2246682783cc7c7cb6", // v2.2.2 - "actions/stale@9d6f46564a515a9ea11e7762ab3957ee58ca50da", // v3.0.16 - "alex-page/github-project-automation-plus@fdb7991b72040d611e1123d2b75ff10eda9372c9", - "andymckay/labeler@22d5392de2b725cea4b284df5824125054049d84", - "crowdin/github-action@fd9429dd63d6c0f8a8cb4b93ad8076990bd6e688", - "crykn/copy_folder_to_another_repo_action@0282e8b9fef06de92ddcae9fe6cb44df6226646c", - "cschleiden/actions-linter@0ff16d6ac5103cca6c92e6cbc922b646baaea5be", - "dawidd6/action-delete-branch@47743101a121ad657031e6704086271ca81b1911", - "docker://chinthakagodawita/autoupdate-action:v1", - "dorny/paths-filter@eb75a1edc117d3756a18ef89958ee59f9500ba58", - "github/codeql-action/analyze@v1", - "github/codeql-action/init@v1", - "juliangruber/approve-pull-request-action@c530832d4d346c597332e20e03605aa94fa150a8", - "juliangruber/find-pull-request-action@db875662766249c049b2dcd85293892d61cb0b51", // v1.5.0 - "juliangruber/read-file-action@e0a316da496006ffd19142f0fd594a1783f3b512", - "lee-dohm/close-matching-issues@22002609b2555fe18f52b8e2e7c07cbf5529e8a8", - "lee-dohm/no-response@9bb0a4b5e6a45046f00353d5de7d90fb8bd773bb", - "pascalgn/automerge-action@c9bd1823770819dc8fb8a5db2d11a3a95fbe9b07", // v0.12.0 - "peter-evans/create-issue-from-file@b4f9ee0a9d4abbfc6986601d9b1a4f8f8e74c77e", - "peter-evans/create-or-update-comment@5221bf4aa615e5c6e95bb142f9673a9c791be2cd", - "peter-evans/create-pull-request@8c603dbb04b917a9fc2dd991dc54fef54b640b43", - "rachmari/actions-add-new-issue-to-column@1a459ef92308ba7c9c9dc2fcdd72f232495574a9", - "rachmari/labeler@832d42ec5523f3c6d46e8168de71cd54363e3e2e", - "repo-sync/github-sync@3832fe8e2be32372e1b3970bbae8e7079edeec88", - "repo-sync/pull-request@33777245b1aace1a58c87a29c90321aa7a74bd7d", - "someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd", - "tjenkinson/gh-action-auto-merge-dependency-updates@4d7756c04d9d999c5968697a621b81c47f533d61", - "EndBug/add-and-commit@b3c7c1e078a023d75fb0bd326e02962575ce0519" + 'actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f', // v2.3.4 + 'actions/github-script@2b34a689ec86a68d8ab9478298f91d5401337b7d', // v4.0.2 + 'actions/labeler@5f867a63be70efff62b767459b009290364495eb', // v2.2.0 + 'actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f', // v2.2.0 + 'actions/setup-python@dc73133d4da04e56a135ae2246682783cc7c7cb6', // v2.2.2 + 'actions/stale@9d6f46564a515a9ea11e7762ab3957ee58ca50da', // v3.0.16 + 'alex-page/github-project-automation-plus@fdb7991b72040d611e1123d2b75ff10eda9372c9', + 'andymckay/labeler@22d5392de2b725cea4b284df5824125054049d84', + 'crowdin/github-action@fd9429dd63d6c0f8a8cb4b93ad8076990bd6e688', + 'crykn/copy_folder_to_another_repo_action@0282e8b9fef06de92ddcae9fe6cb44df6226646c', + 'cschleiden/actions-linter@0ff16d6ac5103cca6c92e6cbc922b646baaea5be', + 'dawidd6/action-delete-branch@47743101a121ad657031e6704086271ca81b1911', + 'docker://chinthakagodawita/autoupdate-action:v1', + 'dorny/paths-filter@eb75a1edc117d3756a18ef89958ee59f9500ba58', + 'github/codeql-action/analyze@v1', + 'github/codeql-action/init@v1', + 'juliangruber/approve-pull-request-action@c530832d4d346c597332e20e03605aa94fa150a8', + 'juliangruber/find-pull-request-action@db875662766249c049b2dcd85293892d61cb0b51', // v1.5.0 + 'juliangruber/read-file-action@e0a316da496006ffd19142f0fd594a1783f3b512', + 'lee-dohm/close-matching-issues@22002609b2555fe18f52b8e2e7c07cbf5529e8a8', + 'lee-dohm/no-response@9bb0a4b5e6a45046f00353d5de7d90fb8bd773bb', + 'pascalgn/automerge-action@c9bd1823770819dc8fb8a5db2d11a3a95fbe9b07', // v0.12.0 + 'peter-evans/create-issue-from-file@b4f9ee0a9d4abbfc6986601d9b1a4f8f8e74c77e', + 'peter-evans/create-or-update-comment@5221bf4aa615e5c6e95bb142f9673a9c791be2cd', + 'peter-evans/create-pull-request@8c603dbb04b917a9fc2dd991dc54fef54b640b43', + 'rachmari/actions-add-new-issue-to-column@1a459ef92308ba7c9c9dc2fcdd72f232495574a9', + 'rachmari/labeler@832d42ec5523f3c6d46e8168de71cd54363e3e2e', + 'repo-sync/github-sync@3832fe8e2be32372e1b3970bbae8e7079edeec88', + 'repo-sync/pull-request@33777245b1aace1a58c87a29c90321aa7a74bd7d', + 'someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd', + 'tjenkinson/gh-action-auto-merge-dependency-updates@4d7756c04d9d999c5968697a621b81c47f533d61', + 'EndBug/add-and-commit@b3c7c1e078a023d75fb0bd326e02962575ce0519', ] diff --git a/.prettierrc.json b/.prettierrc.json index 929880de5402..a76d08f0287d 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -7,7 +7,7 @@ } }, { - "files": ["**/*.{ts,tsx}"], + "files": ["**/*.{ts,tsx,js,mjs}"], "options": { "semi": false, "singleQuote": true, diff --git a/components/landing/TocLanding.tsx b/components/landing/TocLanding.tsx index 861cd92e6219..bbe20b22582d 100644 --- a/components/landing/TocLanding.tsx +++ b/components/landing/TocLanding.tsx @@ -8,8 +8,16 @@ import { ArticleList } from 'components/landing/ArticleList' import { useTranslation } from 'components/hooks/useTranslation' export const TocLanding = () => { - const { title, introPlainText, tocItems, productCallout, variant, featuredLinks, isEarlyAccess, renderedEarlyAccessPage } = - useTocLandingContext() + const { + title, + introPlainText, + tocItems, + productCallout, + variant, + featuredLinks, + isEarlyAccess, + renderedEarlyAccessPage, + } = useTocLandingContext() const { t } = useTranslation('toc') return ( @@ -68,14 +76,15 @@ export const TocLanding = () => { )} - {isEarlyAccess && + {isEarlyAccess && (
-
} +
+ )} diff --git a/data/allowed-topics.js b/data/allowed-topics.js index 92aa31cc04b8..19e27d625a0b 100644 --- a/data/allowed-topics.js +++ b/data/allowed-topics.js @@ -155,5 +155,5 @@ export default [ 'Xamarin.Android', 'Xamarin.iOS', 'Xamarin', - 'Xcode' + 'Xcode', ] diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index 08e3c8703ae3..a66133dd35ea 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -1,5 +1,3 @@ module.exports = { - launch: process.env.GITHUB_ACTIONS - ? { executablePath: 'google-chrome-stable' } - : {} + launch: process.env.GITHUB_ACTIONS ? { executablePath: 'google-chrome-stable' } : {}, } diff --git a/jest.config.js b/jest.config.js index 743fbd5f21bd..f78badd08236 100644 --- a/jest.config.js +++ b/jest.config.js @@ -19,28 +19,22 @@ module.exports = { branches: 95, functions: 95, lines: 95, - statements: -5 - } + statements: -5, + }, }, - preset: isBrowser - ? 'jest-puppeteer' - : undefined, + preset: isBrowser ? 'jest-puppeteer' : undefined, reporters, - modulePathIgnorePatterns: [ - 'assets/' - ], + modulePathIgnorePatterns: ['assets/'], setupFilesAfterEnv: ['jest-expect-message'], - ...isBrowser ? {} : { testEnvironment: 'node' }, + ...(isBrowser ? {} : { testEnvironment: 'node' }), testPathIgnorePatterns: [ 'node_modules/', 'vendor/', 'tests/fixtures/', 'tests/helpers/', 'tests/javascripts/', - ...isBrowser ? [] : ['tests/browser/browser.js'] - ], - testMatch: [ - '**/tests/**/*.js' + ...(isBrowser ? [] : ['tests/browser/browser.js']), ], - testLocationInResults: isActions + testMatch: ['**/tests/**/*.js'], + testLocationInResults: isActions, } diff --git a/lib/all-products.js b/lib/all-products.js index 2a790bf56508..5357932874f9 100644 --- a/lib/all-products.js +++ b/lib/all-products.js @@ -12,7 +12,7 @@ const externalProducts = data.externalProducts const internalProducts = {} -productIds.forEach(productId => { +productIds.forEach((productId) => { const relPath = productId const dir = path.posix.join('content', relPath) @@ -31,7 +31,7 @@ productIds.forEach(productId => { dir, toc, wip: data.wip || false, - hidden: data.hidden || false + hidden: data.hidden || false, } internalProducts[productId].versions = applicableVersions @@ -41,5 +41,5 @@ export const productMap = Object.assign({}, internalProducts, externalProducts) export default { productIds, - productMap + productMap, } diff --git a/lib/all-versions.js b/lib/all-versions.js index ddf38500a1d5..1c0259ee4a81 100644 --- a/lib/all-versions.js +++ b/lib/all-versions.js @@ -7,7 +7,8 @@ const versionDelimiter = '@' const latestNonNumberedRelease = 'latest' const plans = [ - { // free-pro-team is **not** a user-facing version and is stripped from URLs. + { + // free-pro-team is **not** a user-facing version and is stripped from URLs. // See lib/remove-fpt-from-path.js for details. plan: 'free-pro-team', planTitle: 'GitHub.com', @@ -16,7 +17,7 @@ const plans = [ latestRelease: latestNonNumberedRelease, nonEnterpriseDefault: true, // permanent way to refer to this plan if the name changes openApiBaseName: 'api.github.com', // used for REST - miscBaseName: 'dotcom' // used for GraphQL and webhooks + miscBaseName: 'dotcom', // used for GraphQL and webhooks }, { plan: 'enterprise-server', @@ -26,7 +27,7 @@ const plans = [ latestRelease: enterpriseServerReleases.latest, hasNumberedReleases: true, openApiBaseName: 'ghes-', - miscBaseName: 'ghes-' + miscBaseName: 'ghes-', }, { plan: 'github-ae', @@ -35,25 +36,31 @@ const plans = [ releases: [latestNonNumberedRelease], latestRelease: latestNonNumberedRelease, openApiBaseName: 'github.ae', - miscBaseName: 'ghae' - } + miscBaseName: 'ghae', + }, ] const allVersions = {} // combine the plans and releases to get allVersions object // e.g. free-pro-team@latest, enterprise-server@2.21, enterprise-server@2.20, etc. -plans.forEach(planObj => { - planObj.releases.forEach(release => { +plans.forEach((planObj) => { + planObj.releases.forEach((release) => { const version = `${planObj.plan}${versionDelimiter}${release}` const versionObj = { version, - versionTitle: planObj.hasNumberedReleases ? `${planObj.planTitle} ${release}` : planObj.planTitle, + versionTitle: planObj.hasNumberedReleases + ? `${planObj.planTitle} ${release}` + : planObj.planTitle, latestVersion: `${planObj.plan}${versionDelimiter}${planObj.latestRelease}`, currentRelease: release, - openApiVersionName: planObj.hasNumberedReleases ? `${planObj.openApiBaseName}${release}` : planObj.openApiBaseName, - miscVersionName: planObj.hasNumberedReleases ? `${planObj.miscBaseName}${release}` : planObj.miscBaseName + openApiVersionName: planObj.hasNumberedReleases + ? `${planObj.openApiBaseName}${release}` + : planObj.openApiBaseName, + miscVersionName: planObj.hasNumberedReleases + ? `${planObj.miscBaseName}${release}` + : planObj.miscBaseName, } allVersions[version] = Object.assign(versionObj, planObj) diff --git a/lib/app.js b/lib/app.js index 0d4adb2f2cf3..b8049eba8e3d 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,7 +1,7 @@ import express from 'express' import middleware from '../middleware/index.js' -function createApp () { +function createApp() { const app = express() middleware(app) return app diff --git a/lib/built-asset-urls.js b/lib/built-asset-urls.js index 4596946f8487..f1990f1c91b4 100644 --- a/lib/built-asset-urls.js +++ b/lib/built-asset-urls.js @@ -3,14 +3,14 @@ import path from 'path' import crypto from 'crypto' // Get an MD4 Digest Hex content hash, loosely based on Webpack `[contenthash]` -function getContentHash (absFilePath) { +function getContentHash(absFilePath) { const buffer = fs.readFileSync(absFilePath) const hash = crypto.createHash('md4') hash.update(buffer) return hash.digest('hex') } -function getUrl (relFilePath) { +function getUrl(relFilePath) { const absFilePath = path.join(process.cwd(), relFilePath) return `/${relFilePath}?hash=${getContentHash(absFilePath)}` } @@ -18,6 +18,6 @@ function getUrl (relFilePath) { export default { main: { js: getUrl('dist/index.js'), - css: getUrl('dist/index.css') - } + css: getUrl('dist/index.css'), + }, } diff --git a/lib/changelog.js b/lib/changelog.js index 91d124041b0c..a3f85bf5e4cf 100644 --- a/lib/changelog.js +++ b/lib/changelog.js @@ -1,6 +1,6 @@ import Parser from 'rss-parser' -export async function getRssFeed (url) { +export async function getRssFeed(url) { const parser = new Parser({ timeout: 5000 }) const feedUrl = `${url}/feed` let feed @@ -15,7 +15,7 @@ export async function getRssFeed (url) { return feed } -export async function getChangelogItems (prefix, feed) { +export async function getChangelogItems(prefix, feed) { if (!feed || !feed.items) { console.log(feed) console.error('feed is not valid or has no items') @@ -23,18 +23,16 @@ export async function getChangelogItems (prefix, feed) { } // only show the first 3 posts - const changelog = feed.items - .slice(0, 3) - .map(item => { - // remove the prefix if it exists (Ex: 'GitHub Actions: '), where the colon and expected whitespace should be hardcoded. - const title = prefix ? item.title.replace(new RegExp(`^${prefix}`), '') : item.title - return { - // capitalize the first letter of the title - title: title.trim().charAt(0).toUpperCase() + title.slice(1), - date: item.isoDate, - href: item.link - } - }) + const changelog = feed.items.slice(0, 3).map((item) => { + // remove the prefix if it exists (Ex: 'GitHub Actions: '), where the colon and expected whitespace should be hardcoded. + const title = prefix ? item.title.replace(new RegExp(`^${prefix}`), '') : item.title + return { + // capitalize the first letter of the title + title: title.trim().charAt(0).toUpperCase() + title.slice(1), + date: item.isoDate, + href: item.link, + } + }) return changelog } diff --git a/lib/check-if-next-version-only.js b/lib/check-if-next-version-only.js index f35c01c54a42..8c20e4013e3a 100644 --- a/lib/check-if-next-version-only.js +++ b/lib/check-if-next-version-only.js @@ -4,10 +4,11 @@ import versionSatisfiesRange from './version-satisfies-range.js' // Special handling for frontmatter that evalues to the next GHES release number or a hardcoded `next`: // we don't want to return it as an applicable version or it will become a permalink, // but we also don't want to throw an error if no other versions are found. -export default function checkIfNextVersionOnly (value) { +export default function checkIfNextVersionOnly(value) { if (value === '*') return false - const ghesNextVersionOnly = versionSatisfiesRange(next, value) && !versionSatisfiesRange(latest, value) + const ghesNextVersionOnly = + versionSatisfiesRange(next, value) && !versionSatisfiesRange(latest, value) - return (ghesNextVersionOnly || value === 'next') + return ghesNextVersionOnly || value === 'next' } diff --git a/lib/cookie-settings.js b/lib/cookie-settings.js index f12cc4d635eb..0169ee29570d 100644 --- a/lib/cookie-settings.js +++ b/lib/cookie-settings.js @@ -4,7 +4,7 @@ export default { // requires https protocol // `secure` doesn't work with supertest at all // http://localhost fails on chrome with secure - sameSite: 'lax' + sameSite: 'lax', // most browsers are "lax" these days, // but older browsers used to default to "none" } diff --git a/lib/create-tree.js b/lib/create-tree.js index 954c9b71dd8a..681bab66c540 100644 --- a/lib/create-tree.js +++ b/lib/create-tree.js @@ -5,7 +5,7 @@ import Page from './page.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const fs = xFs.promises -export default async function createTree (originalPath, langObj) { +export default async function createTree(originalPath, langObj) { // This basePath definition is needed both here and in lib/page-data.js because this // function runs recursively, and the value for originalPath changes on recursive runs. const basePath = path.posix.join(__dirname, '..', langObj.dir, 'content') @@ -27,7 +27,7 @@ export default async function createTree (originalPath, langObj) { const page = await Page.init({ basePath, relativePath, - languageCode: langObj.code + languageCode: langObj.code, }) if (!page) { @@ -41,14 +41,18 @@ export default async function createTree (originalPath, langObj) { // Create the root tree object on the first run, and create children recursively. const item = { - page + page, } // Process frontmatter children recursively. if (item.page.children) { - item.childPages = (await Promise.all(item.page.children - .map(async (child) => await createTree(path.posix.join(originalPath, child), langObj)))) - .filter(Boolean) + item.childPages = ( + await Promise.all( + item.page.children.map( + async (child) => await createTree(path.posix.join(originalPath, child), langObj) + ) + ) + ).filter(Boolean) } return item diff --git a/lib/data-directory.js b/lib/data-directory.js index 81630dc41920..fa7aca79a8aa 100644 --- a/lib/data-directory.js +++ b/lib/data-directory.js @@ -6,17 +6,13 @@ import yaml from 'js-yaml' import { isRegExp, set } from 'lodash-es' import filenameToKey from './filename-to-key.js' -export default function dataDirectory (dir, opts = {}) { +export default function dataDirectory(dir, opts = {}) { const defaultOpts = { - preprocess: (content) => { return content }, + preprocess: (content) => { + return content + }, ignorePatterns: [/README\.md$/i], - extensions: [ - '.json', - '.md', - '.markdown', - '.yaml', - '.yml' - ] + extensions: ['.json', '.md', '.markdown', '.yaml', '.yml'], } opts = Object.assign({}, defaultOpts, opts) @@ -31,18 +27,15 @@ export default function dataDirectory (dir, opts = {}) { const data = {} // find YAML and Markdown files in the given directory, recursively - const filenames = walk(dir, { includeBasePath: true }) - .filter(filename => { - // ignore files that match any of ignorePatterns regexes - if (opts.ignorePatterns.some(pattern => pattern.test(filename))) return false + const filenames = walk(dir, { includeBasePath: true }).filter((filename) => { + // ignore files that match any of ignorePatterns regexes + if (opts.ignorePatterns.some((pattern) => pattern.test(filename))) return false - // ignore files that don't have a whitelisted file extension - return opts.extensions.includes(path.extname(filename).toLowerCase()) - }) + // ignore files that don't have a whitelisted file extension + return opts.extensions.includes(path.extname(filename).toLowerCase()) + }) - const files = filenames.map( - filename => [filename, fs.readFileSync(filename, 'utf8')] - ) + const files = filenames.map((filename) => [filename, fs.readFileSync(filename, 'utf8')]) files.forEach(([filename, fileContent]) => { // derive `foo.bar.baz` object key from `foo/bar/baz.yml` filename const key = filenameToKey(path.relative(dir, filename)) @@ -64,8 +57,7 @@ export default function dataDirectory (dir, opts = {}) { set(data, key, fileContent) break } - } - ) + }) return data } diff --git a/lib/encode-bracketed-parentheses.js b/lib/encode-bracketed-parentheses.js index b0cdb2f0e974..e3e4b3040375 100644 --- a/lib/encode-bracketed-parentheses.js +++ b/lib/encode-bracketed-parentheses.js @@ -1,6 +1,6 @@ // prevent `[foo] (bar)` strings with a space between from being interpreted as markdown links // by encoding the space character -export default function encodeBracketedParentheses (input) { +export default function encodeBracketedParentheses(input) { return input.replace(/] \(/g, '] (') } diff --git a/lib/enterprise-server-releases.js b/lib/enterprise-server-releases.js index 82089cb4840f..c1df2f67659a 100644 --- a/lib/enterprise-server-releases.js +++ b/lib/enterprise-server-releases.js @@ -2,7 +2,9 @@ import versionSatisfiesRange from './version-satisfies-range.js' import fs from 'fs' import path from 'path' -export const dates = JSON.parse(fs.readFileSync(path.join(process.cwd(), './lib/enterprise-dates.json'))) +export const dates = JSON.parse( + fs.readFileSync(path.join(process.cwd(), './lib/enterprise-dates.json')) +) // GHES Release Lifecycle Dates: // enterprise-releases/docs/supported-versions.md#release-lifecycle-dates @@ -10,12 +12,7 @@ export const dates = JSON.parse(fs.readFileSync(path.join(process.cwd(), './lib/ // Some frontmatter may contain the upcoming GHES release number export const next = '3.2' -export const supported = [ - '3.1', - '3.0', - '2.22', - '2.21' -] +export const supported = ['3.1', '3.0', '2.22', '2.21'] export const deprecated = [ '2.20', '2.19', @@ -38,35 +35,45 @@ export const deprecated = [ '2.2', '2.1', '2.0', - '11.10.340' -] -export const legacyAssetVersions = [ - '3.0', - '2.22', - '2.21' + '11.10.340', ] +export const legacyAssetVersions = ['3.0', '2.22', '2.21'] export const all = supported.concat(deprecated) export const latest = supported[0] export const oldestSupported = supported[supported.length - 1] export const nextDeprecationDate = dates[oldestSupported].deprecationDate export const isOldestReleaseDeprecated = new Date() > new Date(nextDeprecationDate) -export const deprecatedOnNewSite = deprecated.filter(version => versionSatisfiesRange(version, '>=2.13')) +export const deprecatedOnNewSite = deprecated.filter((version) => + versionSatisfiesRange(version, '>=2.13') +) export const firstVersionDeprecatedOnNewSite = '2.13' // starting from 2.18, we updated the archival script to create a redirects.json top-level file in the archived repo export const lastVersionWithoutArchivedRedirectsFile = '2.17' // last version using paths like /enterprise/////
// instead of /enterprise-server@///
export const lastReleaseWithLegacyFormat = '2.18' -export const deprecatedReleasesWithLegacyFormat = deprecated.filter(version => versionSatisfiesRange(version, '<=2.18')) -export const deprecatedReleasesWithNewFormat = deprecated.filter(version => versionSatisfiesRange(version, '>2.18')) -export const deprecatedReleasesOnDeveloperSite = deprecated.filter(version => versionSatisfiesRange(version, '<=2.16')) +export const deprecatedReleasesWithLegacyFormat = deprecated.filter((version) => + versionSatisfiesRange(version, '<=2.18') +) +export const deprecatedReleasesWithNewFormat = deprecated.filter((version) => + versionSatisfiesRange(version, '>2.18') +) +export const deprecatedReleasesOnDeveloperSite = deprecated.filter((version) => + versionSatisfiesRange(version, '<=2.16') +) export const firstReleaseNote = '2.20' export const firstRestoredAdminGuides = '2.21' -export const findReleaseNumberIndex = (releaseNum) => { return all.findIndex(i => i === releaseNum) } -export const getNextReleaseNumber = (releaseNum) => { return all[findReleaseNumberIndex(releaseNum) - 1] } -export const getPreviousReleaseNumber = (releaseNum) => { return all[findReleaseNumberIndex(releaseNum) + 1] } +export const findReleaseNumberIndex = (releaseNum) => { + return all.findIndex((i) => i === releaseNum) +} +export const getNextReleaseNumber = (releaseNum) => { + return all[findReleaseNumberIndex(releaseNum) - 1] +} +export const getPreviousReleaseNumber = (releaseNum) => { + return all[findReleaseNumberIndex(releaseNum) + 1] +} export default { next, @@ -89,5 +96,5 @@ export default { firstReleaseNote, firstRestoredAdminGuides, getNextReleaseNumber, - getPreviousReleaseNumber + getPreviousReleaseNumber, } diff --git a/lib/excluded-links.js b/lib/excluded-links.js index 715ceffb9b1d..f9fb697f4ab2 100644 --- a/lib/excluded-links.js +++ b/lib/excluded-links.js @@ -18,5 +18,5 @@ export default [ 'https://ko-fi.com/', 'https://en.liberapay.com/', 'https://nbviewer.jupyter.org/github/bokeh/bokeh-notebooks/blob/main/tutorial/06%20-%20Linking%20and%20Interactions.ipynb', - 'https://www.vmware.com/products/esxi-and-esx.html' + 'https://www.vmware.com/products/esxi-and-esx.html', ] diff --git a/lib/failbot.js b/lib/failbot.js index 4ae5e0b48735..4e8f53ab5cdb 100644 --- a/lib/failbot.js +++ b/lib/failbot.js @@ -1,7 +1,7 @@ import fetch from 'node-fetch' export default class FailBot { - constructor ({ app, haystackURL, headers }) { + constructor({ app, haystackURL, headers }) { this.app = app this.haystackURL = haystackURL this.headers = headers @@ -13,14 +13,14 @@ export default class FailBot { * @param {any} metadata * @param {any} [headers] */ - static async report (error, metadata, headers = {}) { + static async report(error, metadata, headers = {}) { // If there's no HAYSTACK_URL set, bail early if (!process.env.HAYSTACK_URL) return const failbot = new FailBot({ app: 'docs', haystackURL: process.env.HAYSTACK_URL, - headers + headers, }) return failbot.sendException(error, metadata) @@ -30,7 +30,7 @@ export default class FailBot { * Create a rollup of this error by generating a base64 representation * @param {Error} error */ - createRollup (error) { + createRollup(error) { const stackLine = error.stack && error.stack.split('\n')[1] const str = `${error.name}:${stackLine}`.replace(/=/g, '') return Buffer.from(str).toString('base64') @@ -41,7 +41,7 @@ export default class FailBot { * @param {Error} error * @param {any} metadata */ - formatJSON (error, metadata) { + formatJSON(error, metadata) { return Object.assign({}, metadata, { /* eslint-disable camelcase */ created_at: new Date().toISOString(), @@ -49,7 +49,7 @@ export default class FailBot { class: error.name, message: error.message, backtrace: error.stack || '', - js_environment: `Node.js ${process.version}` + js_environment: `Node.js ${process.version}`, /* eslint-enable camelcase */ }) } @@ -58,7 +58,7 @@ export default class FailBot { * Populate default context from settings. Since settings commonly comes from * ENV, this allows setting defaults for the context via the environment. */ - getFailbotContext () { + getFailbotContext() { const failbotKeys = {} for (const key in process.env) { @@ -76,7 +76,7 @@ export default class FailBot { * @param {Error} error * @param {any} metadata */ - async sendException (error, metadata = {}) { + async sendException(error, metadata = {}) { const data = Object.assign({ app: this.app }, this.getFailbotContext(), metadata) const body = this.formatJSON(error, Object.assign({ app: this.app }, data)) @@ -85,8 +85,8 @@ export default class FailBot { body: JSON.stringify(body), headers: { ...this.headers, - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }) } } diff --git a/lib/filename-to-key.js b/lib/filename-to-key.js index fa2ec6fd7695..8fc07f05c804 100644 --- a/lib/filename-to-key.js +++ b/lib/filename-to-key.js @@ -14,7 +14,7 @@ const windowsPathSeparator = new RegExp('/', 'g') const windowsDoubleSlashSeparator = new RegExp('\\\\', 'g') // derive `foo.bar.baz` object key from `foo/bar/baz.yml` filename -export default function filenameToKey (filename) { +export default function filenameToKey(filename) { const extension = new RegExp(`${path.extname(filename)}$`) const key = filename .replace(extension, '') diff --git a/lib/find-page-in-site-tree.js b/lib/find-page-in-site-tree.js index 8396c07e838d..52f4261d1ff0 100644 --- a/lib/find-page-in-site-tree.js +++ b/lib/find-page-in-site-tree.js @@ -2,7 +2,7 @@ import { getLanguageCode } from './patterns.js' // This module recursively searches a given part of the site tree by iterating through child // pages and finding a path that matches the original path provided. -export default function findPageInSiteTree (treePage, englishTree, originalPath, modifiedPath) { +export default function findPageInSiteTree(treePage, englishTree, originalPath, modifiedPath) { if (Array.isArray(treePage)) throw new Error('received array instead of object') // If the tree page already matches the path, or if it has no child pages, return the page itself. @@ -24,10 +24,10 @@ export default function findPageInSiteTree (treePage, englishTree, originalPath, // If we found a page... if (foundPage) { return modifiedPath === originalPath - // Check if it matches the _original_ path, and return it if so. - ? foundPage - // If we found a page with the modified path, keep going down the tree until we find the original path. - : findPageInSiteTree(foundPage, englishTree, originalPath) + ? // Check if it matches the _original_ path, and return it if so. + foundPage + : // If we found a page with the modified path, keep going down the tree until we find the original path. + findPageInSiteTree(foundPage, englishTree, originalPath) } // If no page was found at the path we tried, try again by removing the last segment of the path. diff --git a/lib/find-page.js b/lib/find-page.js index b0b8e1e5c4fa..071882e2c9f3 100644 --- a/lib/find-page.js +++ b/lib/find-page.js @@ -1,6 +1,6 @@ import { getLanguageCode } from './patterns.js' -export default function findPage (href, pageMap, redirects) { +export default function findPage(href, pageMap, redirects) { // remove any fragments href = href.replace(/#.*$/, '') diff --git a/lib/frontmatter.js b/lib/frontmatter.js index daaaa3e43ff0..af01fe6f7df0 100644 --- a/lib/frontmatter.js +++ b/lib/frontmatter.js @@ -10,158 +10,159 @@ const layoutNames = Object.keys(layouts).concat([false]) const semverRange = { type: 'string', conform: semverValidRange, - message: 'Must be a valid SemVer range' + message: 'Must be a valid SemVer range', } const versionObjs = Object.values(xAllVersions) const guideTypes = ['overview', 'quick_start', 'tutorial', 'how_to', 'reference'] -const featureVersions = fs.readdirSync(path.posix.join(process.cwd(), 'data/features')) - .map(file => path.basename(file, '.yml')) +const featureVersions = fs + .readdirSync(path.posix.join(process.cwd(), 'data/features')) + .map((file) => path.basename(file, '.yml')) export const schema = { properties: { title: { type: 'string', required: true, - translatable: true + translatable: true, }, shortTitle: { type: 'string', - translatable: true + translatable: true, }, intro: { type: 'string', - translatable: true + translatable: true, }, product: { type: 'string', - translatable: true + translatable: true, }, permissions: { - type: 'string' + type: 'string', }, // true by default on articles, false on all other content showMiniToc: { - type: 'boolean' + type: 'boolean', }, miniTocMaxHeadingLevel: { type: 'number', default: 2, minimum: 2, - maximum: 4 + maximum: 4, }, mapTopic: { - type: 'boolean' + type: 'boolean', }, // allow hidden articles under `early-access` hidden: { - type: 'boolean' + type: 'boolean', }, layout: { type: ['string', 'boolean'], enum: layoutNames, - message: 'must be the filename of an existing layout file, or `false` for no layout' + message: 'must be the filename of an existing layout file, or `false` for no layout', }, redirect_from: { - type: ['array', 'string'] + type: ['array', 'string'], }, allowTitleToDifferFromFilename: { - type: 'boolean' + type: 'boolean', }, introLinks: { type: 'object', properties: { quickstart: { type: 'string' }, reference: { type: 'string' }, - overview: { type: 'string' } - } + overview: { type: 'string' }, + }, }, authors: { type: 'array', items: { - type: 'string' - } + type: 'string', + }, }, examples_source: { - type: 'string' + type: 'string', }, featuredLinks: { type: 'object', properties: { gettingStarted: { type: 'array', - items: { type: 'string' } + items: { type: 'string' }, }, guides: { type: 'array', - items: { type: 'string' } + items: { type: 'string' }, }, guideCards: { type: 'array', - items: { type: 'string' } + items: { type: 'string' }, }, popular: { type: 'array', - items: { type: 'string' } + items: { type: 'string' }, }, // allows you to use an alternate heading for the popular column popularHeading: { - type: 'string' - } - } + type: 'string', + }, + }, }, // Shown in `product-landing.html` "What's new" section changelog: { type: 'object', properties: { label: { type: 'string' }, - prefix: { type: 'string' } - } + prefix: { type: 'string' }, + }, }, type: { type: 'string', - enum: guideTypes + enum: guideTypes, }, topics: { - type: 'array' + type: 'array', }, includeGuides: { - type: 'array' + type: 'array', }, learningTracks: { - type: 'array' + type: 'array', }, // Used in `product-landing.html` beta_product: { - type: 'boolean' + type: 'boolean', }, // Show in `product-landing.html` product_video: { - type: 'string' + type: 'string', }, interactive: { - type: 'boolean' + type: 'boolean', }, // Platform-specific content preference defaultPlatform: { type: 'string', - enum: ['mac', 'windows', 'linux'] + enum: ['mac', 'windows', 'linux'], }, // Tool-specific content preference defaultTool: { type: 'string', - enum: ['webui', 'cli', 'desktop', 'curl'] + enum: ['webui', 'cli', 'desktop', 'curl'], }, // Documentation contributed by a third party, such as a GitHub Partner contributor: { type: 'object', properties: { name: { type: 'string' }, - URL: { type: 'string' } - } + URL: { type: 'string' }, + }, }, // Child links specified on any TOC page children: { - type: 'array' + type: 'array', }, // External products specified on the homepage externalProducts: { @@ -174,8 +175,8 @@ export const schema = { id: { type: 'string', required: true }, name: { type: 'string', required: true }, href: { type: 'string', format: 'url', required: true }, - external: { type: 'boolean', required: true } - } + external: { type: 'boolean', required: true }, + }, }, atom: { type: 'object', @@ -184,8 +185,8 @@ export const schema = { id: { type: 'string', required: true }, name: { type: 'string', required: true }, href: { type: 'string', format: 'url', required: true }, - external: { type: 'boolean', required: true } - } + external: { type: 'boolean', required: true }, + }, }, electron: { type: 'object', @@ -194,12 +195,12 @@ export const schema = { id: { type: 'string', required: true }, name: { type: 'string', required: true }, href: { type: 'string', format: 'url', required: true }, - external: { type: 'boolean', required: true } - } - } - } - } - } + external: { type: 'boolean', required: true }, + }, + }, + }, + }, + }, } const featureVersionsProp = { @@ -207,10 +208,11 @@ const featureVersionsProp = { type: ['string', 'array'], enum: featureVersions, items: { - type: 'string' + type: 'string', }, - message: 'must be the name (or names) of a feature that matches "filename" in data/features/_filename_.yml' - } + message: + 'must be the name (or names) of a feature that matches "filename" in data/features/_filename_.yml', + }, } schema.properties.versions = { @@ -220,18 +222,18 @@ schema.properties.versions = { acc[versionObj.plan] = semverRange acc[versionObj.shortName] = semverRange return acc - }, featureVersionsProp) + }, featureVersionsProp), } // Support 'github-ae': next schema.properties.versions.properties['github-ae'] = 'next' schema.properties.versions.properties.ghae = 'next' -function frontmatter (markdown, opts = {}) { +function frontmatter(markdown, opts = {}) { const defaults = { schema, validateKeyNames: true, - validateKeyOrder: false // TODO: enable this once we've sorted all the keys. See issue 9658 + validateKeyOrder: false, // TODO: enable this once we've sorted all the keys. See issue 9658 } return parse(markdown, Object.assign({}, defaults, opts)) diff --git a/lib/get-applicable-versions.js b/lib/get-applicable-versions.js index 49c57bfe5376..510a937366f5 100644 --- a/lib/get-applicable-versions.js +++ b/lib/get-applicable-versions.js @@ -10,13 +10,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const featuresDir = path.posix.join(__dirname, '../data/features') const featureData = dataDirectory(featuresDir, { - preprocess: dataString => - encodeBracketedParentheses(dataString.trimEnd()), - ignorePatterns: [/README\.md$/] + preprocess: (dataString) => encodeBracketedParentheses(dataString.trimEnd()), + ignorePatterns: [/README\.md$/], }) // return an array of versions that an article's product versions encompasses -function getApplicableVersions (frontmatterVersions, filepath) { +function getApplicableVersions(frontmatterVersions, filepath) { if (typeof frontmatterVersions === 'undefined') { throw new Error(`No \`versions\` frontmatter found in ${filepath}`) } @@ -37,19 +36,23 @@ function getApplicableVersions (frontmatterVersions, filepath) { // ghes: '>=2.23' // ghae: '*' // where the feature is bringing the ghes and ghae versions into the mix. - const featureVersions = reduce(frontmatterVersions, (result, value, key) => { - if (key === 'feature') { - if (typeof value === 'string') { - Object.assign(result, { ...featureData[value].versions }) - } else if (Array.isArray(value)) { - value.forEach(str => { - Object.assign(result, { ...featureData[str].versions }) - }) + const featureVersions = reduce( + frontmatterVersions, + (result, value, key) => { + if (key === 'feature') { + if (typeof value === 'string') { + Object.assign(result, { ...featureData[value].versions }) + } else if (Array.isArray(value)) { + value.forEach((str) => { + Object.assign(result, { ...featureData[str].versions }) + }) + } + delete result[key] } - delete result[key] - } - return result - }, {}) + return result + }, + {} + ) // We will be evaluating feature versions separately, so we can remove this. delete frontmatterVersions.feature @@ -59,19 +62,29 @@ function getApplicableVersions (frontmatterVersions, filepath) { const foundFrontmatterVersions = evaluateVersions(frontmatterVersions) // Combine them! - const applicableVersions = [...new Set(foundFrontmatterVersions.versions.concat(foundFeatureVersions.versions))] - - if (!applicableVersions.length && !foundFrontmatterVersions.isNextVersionOnly && !foundFeatureVersions.isNextVersionOnly) { - throw new Error(`No applicable versions found for ${filepath}. Please double-check the page's \`versions\` frontmatter.`) + const applicableVersions = [ + ...new Set(foundFrontmatterVersions.versions.concat(foundFeatureVersions.versions)), + ] + + if ( + !applicableVersions.length && + !foundFrontmatterVersions.isNextVersionOnly && + !foundFeatureVersions.isNextVersionOnly + ) { + throw new Error( + `No applicable versions found for ${filepath}. Please double-check the page's \`versions\` frontmatter.` + ) } // Sort them by the order in lib/all-versions. - const sortedVersions = sortBy(applicableVersions, (v) => { return Object.keys(allVersions).indexOf(v) }) + const sortedVersions = sortBy(applicableVersions, (v) => { + return Object.keys(allVersions).indexOf(v) + }) return sortedVersions } -function evaluateVersions (versionsObj) { +function evaluateVersions(versionsObj) { let isNextVersionOnly = false // get an array like: [ 'free-pro-team@latest', 'enterprise-server@2.21', 'enterprise-cloud@latest' ] @@ -82,24 +95,27 @@ function evaluateVersions (versionsObj) { // ghes: '>=2.19' // ghae: '*' // ^ where each key corresponds to a plan's short name (defined in lib/all-versions.js) - Object.entries(versionsObj) - .forEach(([plan, planValue]) => { - // Special handling for frontmatter that evalues to the next GHES release number or a hardcoded `next`. - isNextVersionOnly = checkIfNextVersionOnly(planValue) - - // For each plan (e.g., enterprise-server), get matching versions from allVersions object - Object.values(allVersions) - .filter(relevantVersion => relevantVersion.plan === plan || relevantVersion.shortName === plan) - .forEach(relevantVersion => { - // Use a dummy value of '1.0' for non-numbered versions like free-pro-team and github-ae - // This will evaluate to true against '*' but false against 'next', which is what we want. - const versionToCompare = relevantVersion.hasNumberedReleases ? relevantVersion.currentRelease : '1.0' - - if (versionSatisfiesRange(versionToCompare, planValue)) { - versions.push(relevantVersion.version) - } - }) - }) + Object.entries(versionsObj).forEach(([plan, planValue]) => { + // Special handling for frontmatter that evalues to the next GHES release number or a hardcoded `next`. + isNextVersionOnly = checkIfNextVersionOnly(planValue) + + // For each plan (e.g., enterprise-server), get matching versions from allVersions object + Object.values(allVersions) + .filter( + (relevantVersion) => relevantVersion.plan === plan || relevantVersion.shortName === plan + ) + .forEach((relevantVersion) => { + // Use a dummy value of '1.0' for non-numbered versions like free-pro-team and github-ae + // This will evaluate to true against '*' but false against 'next', which is what we want. + const versionToCompare = relevantVersion.hasNumberedReleases + ? relevantVersion.currentRelease + : '1.0' + + if (versionSatisfiesRange(versionToCompare, planValue)) { + versions.push(relevantVersion.version) + } + }) + }) return { versions, isNextVersionOnly } } diff --git a/lib/get-document-type.js b/lib/get-document-type.js index f816259b75a5..1af6f083b2bf 100644 --- a/lib/get-document-type.js +++ b/lib/get-document-type.js @@ -1,7 +1,7 @@ // 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. -export default function getDocumentType (relativePath) { +export default function getDocumentType(relativePath) { // A non-index file is ALWAYS considered an article in this approach, // even if it's at the category level (like actions/quickstart.md) if (!relativePath.endsWith('index.md')) { @@ -17,7 +17,7 @@ export default function getDocumentType (relativePath) { 1: 'homepage', 2: 'product', 3: 'category', - 4: 'mapTopic' + 4: 'mapTopic', } const earlyAccessDocs = { @@ -25,10 +25,8 @@ export default function getDocumentType (relativePath) { 2: 'early-access', 3: 'product', 4: 'category', - 5: 'mapTopic' + 5: 'mapTopic', } - return isEarlyAccess - ? earlyAccessDocs[segmentLength] - : publicDocs[segmentLength] + return isEarlyAccess ? earlyAccessDocs[segmentLength] : publicDocs[segmentLength] } diff --git a/lib/get-english-headings.js b/lib/get-english-headings.js index 3f40b04b068b..73f2a939e69d 100644 --- a/lib/get-english-headings.js +++ b/lib/get-english-headings.js @@ -6,7 +6,7 @@ import findPage from './find-page.js' // for any translated page, first get corresponding English markdown // then get the headings on both the translated and English pageMap // finally, create a map of translation:English for all headings on the page -export default function getEnglishHeadings (page, context) { +export default function getEnglishHeadings(page, context) { // Special handling for glossaries, because their headings are // generated programatically. if (page.relativePath.endsWith('/github-glossary.md')) { @@ -21,7 +21,11 @@ export default function getEnglishHeadings (page, context) { const translatedHeadings = getHeadings(page.markdown) if (!translatedHeadings.length) return - const englishPage = findPage(`/en/${page.relativePath.replace(/.md$/, '')}`, context.pages, context.redirects) + const englishPage = findPage( + `/en/${page.relativePath.replace(/.md$/, '')}`, + context.pages, + context.redirects + ) if (!englishPage) return // FIX there may be bugs if English headings are updated before Crowdin syncs up :/ @@ -29,16 +33,18 @@ export default function getEnglishHeadings (page, context) { if (!englishHeadings.length) return // return a map from translation:English - return Object.assign(...translatedHeadings.map((k, i) => ({ - [k]: englishHeadings[i] - }))) + return Object.assign( + ...translatedHeadings.map((k, i) => ({ + [k]: englishHeadings[i], + })) + ) } -function getHeadings (markdown) { +function getHeadings(markdown) { const ast = astFromMarkdown(markdown) const headings = [] - visit(ast, node => { + visit(ast, (node) => { if (node.type !== 'heading') return if (![2, 3, 4].includes(node.depth)) return headings.push(toString(node)) diff --git a/lib/get-link-data.js b/lib/get-link-data.js index 1ea6fd35e89f..7975af68aceb 100644 --- a/lib/get-link-data.js +++ b/lib/get-link-data.js @@ -30,10 +30,11 @@ export default async (rawLinks, context, option = { title: true, intro: true }) const processLink = async (link, context, option) => { const opts = { textOnly: true, encodeEntities: true } // Parse the link in case it includes Liquid conditionals - const linkPath = await renderContent((link.href || link), context, opts) + const linkPath = await renderContent(link.href || link, context, opts) if (!linkPath) return null - const version = context.currentVersion === 'homepage' ? nonEnterpriseDefaultVersion : context.currentVersion + const version = + context.currentVersion === 'homepage' ? nonEnterpriseDefaultVersion : context.currentVersion const href = removeFPTFromPath(path.join('/', context.currentLanguage, version, linkPath)) const linkedPage = findPage(href, context.pages, context.redirects) diff --git a/lib/get-liquid-data-references.js b/lib/get-liquid-data-references.js index fe2e0754c7fd..5c965de7efd5 100644 --- a/lib/get-liquid-data-references.js +++ b/lib/get-liquid-data-references.js @@ -2,14 +2,14 @@ import patterns from './patterns.js' // This module searches a string for references to data objects // It finds all references matching {{site.data.*}} and return an array of them -export default function getLiquidDataReferences (text) { - return (text.match(patterns.dataReference) || []) - .map(ref => { - const cleaned = ref.replace(/\.\.\//g, '') - .replace('{% data', '') - .replace('%}', '') - .trim() +export default function getLiquidDataReferences(text) { + return (text.match(patterns.dataReference) || []).map((ref) => { + const cleaned = ref + .replace(/\.\.\//g, '') + .replace('{% data', '') + .replace('%}', '') + .trim() - return `site.data.${cleaned}` - }) + return `site.data.${cleaned}` + }) } diff --git a/lib/get-mini-toc-items.js b/lib/get-mini-toc-items.js index ef5a0aec60bc..09f93bc1f28b 100644 --- a/lib/get-mini-toc-items.js +++ b/lib/get-mini-toc-items.js @@ -1,11 +1,13 @@ import cheerio from 'cheerio' import { range } from 'lodash-es' -export default function getMiniTocItems (html, maxHeadingLevel = 2, headingScope = '') { +export default function getMiniTocItems(html, maxHeadingLevel = 2, headingScope = '') { const $ = cheerio.load(html, { xmlMode: true }) // eg `h2, h3` or `h2, h3, h4` depending on maxHeadingLevel - const selector = range(2, maxHeadingLevel + 1).map(num => `${headingScope} h${num}`).join(', ') + const selector = range(2, maxHeadingLevel + 1) + .map((num) => `${headingScope} h${num}`) + .join(', ') const headings = $(selector) // return an array of objects containing each heading's contents, level, and optional platform. @@ -15,13 +17,13 @@ export default function getMiniTocItems (html, maxHeadingLevel = 2, headingScope // - `platform` to show or hide platform-specific headings via client JS const items = headings .get() - .filter(item => { + .filter((item) => { if (!item.parent || !item.parent.attribs) return true // Hide any items that belong to a hidden div const { attribs } = item.parent return !('hidden' in attribs) }) - .map(item => { + .map((item) => { // remove any tags including their content $('span').remove() @@ -36,8 +38,8 @@ export default function getMiniTocItems (html, maxHeadingLevel = 2, headingScope // determine indentation level for each item based on the largest // heading level in the current article - const largestHeadingLevel = items.map(item => item.headingLevel).sort()[0] - items.forEach(item => { + const largestHeadingLevel = items.map((item) => item.headingLevel).sort()[0] + items.forEach((item) => { item.indentationLevel = item.headingLevel - largestHeadingLevel }) diff --git a/lib/get-toc-items.js b/lib/get-toc-items.js index 0f8facef7fca..8fbbe1ee7212 100644 --- a/lib/get-toc-items.js +++ b/lib/get-toc-items.js @@ -1,13 +1,13 @@ import { productMap } from './all-products.js' const productTOCs = Object.values(productMap) - .filter(product => !product.external) - .map(product => product.toc.replace('content/', '')) + .filter((product) => !product.external) + .map((product) => product.toc.replace('content/', '')) const linkString = /{% [^}]*?link.*? \/(.*?) ?%}/m const linksArray = new RegExp(linkString.source, 'gm') // return an array of objects like { type: 'category|maptopic|article', href: 'path' } -export default function getTocItems (page) { +export default function getTocItems(page) { // only process product and category tocs if (!page.relativePath.endsWith('index.md')) return if (page.relativePath === 'index.md') return @@ -23,14 +23,16 @@ export default function getTocItems (page) { return [] } - return rawItems.map(item => { + return rawItems.map((item) => { const tocItem = {} // a product's toc items are always categories // whereas a category's toc items can be either maptopics or articles tocItem.type = productTOCs.includes(page.relativePath) ? 'category' - : item.includes('topic_') ? 'maptopic' : 'article' + : item.includes('topic_') + ? 'maptopic' + : 'article' tocItem.href = item.match(linkString)[1] diff --git a/lib/graphql/validator.js b/lib/graphql/validator.js index cc776b3b515b..04504e9bce28 100644 --- a/lib/graphql/validator.js +++ b/lib/graphql/validator.js @@ -6,33 +6,33 @@ export const previewsValidator = { properties: { title: { type: 'string', - required: true + required: true, }, description: { type: 'string', - required: true + required: true, }, toggled_by: { type: 'string', - required: true + required: true, }, toggled_on: { type: 'array', - required: true + required: true, }, owning_teams: { type: 'array', - required: true + required: true, }, accept_header: { type: 'string', - required: true + required: true, }, href: { type: 'string', - required: true - } - } + required: true, + }, + }, } // UPCOMING CHANGES @@ -40,32 +40,32 @@ export const upcomingChangesValidator = { properties: { location: { type: 'string', - required: true + required: true, }, description: { type: 'string', - required: true + required: true, }, reason: { type: 'string', - required: true + required: true, }, date: { type: 'string', required: true, - pattern: /^\d{4}-\d{2}-\d{2}$/ + pattern: /^\d{4}-\d{2}-\d{2}$/, }, criticality: { type: 'string', required: true, - pattern: '(breaking|dangerous)' + pattern: '(breaking|dangerous)', }, owner: { type: 'string', required: true, - pattern: /^[\S]*$/ - } - } + pattern: /^[\S]*$/, + }, + }, } // SCHEMAS @@ -74,38 +74,38 @@ const coreProps = { properties: { name: { type: 'string', - required: true + required: true, }, type: { type: 'string', - required: true + required: true, }, kind: { type: 'string', - required: true + required: true, }, id: { type: 'string', - required: true + required: true, }, href: { type: 'string', - required: true + required: true, }, description: { type: 'string', - required: true + required: true, }, isDeprecated: { type: 'boolean', - required: false + required: false, }, preview: { type: 'object', required: false, - properties: previewsValidator.properties - } - } + properties: previewsValidator.properties, + }, + }, } // some GraphQL schema members have the core properties plus an 'args' object @@ -114,13 +114,13 @@ const corePropsPlusArgs = dup(coreProps) corePropsPlusArgs.properties.args = { type: 'array', required: false, - properties: coreProps.properties + properties: coreProps.properties, } // the args object can have defaultValue prop corePropsPlusArgs.properties.args.properties.defaultValue = { type: 'boolean', - required: false + required: false, } const corePropsNoType = dup(coreProps) @@ -139,13 +139,13 @@ const mutations = dup(corePropsNoType) mutations.properties.inputFields = { type: 'array', required: true, - properties: corePropsNoDescription.properties + properties: corePropsNoDescription.properties, } mutations.properties.returnFields = { type: 'array', required: true, - properties: coreProps.properties + properties: coreProps.properties, } // OBJECTS @@ -154,7 +154,7 @@ const objects = dup(corePropsNoType) objects.properties.fields = { type: 'array', required: true, - properties: corePropsPlusArgs.properties + properties: corePropsPlusArgs.properties, } objects.properties.implements = { @@ -163,17 +163,17 @@ objects.properties.implements = { properties: { name: { type: 'string', - required: true + required: true, }, id: { type: 'string', - required: true + required: true, }, href: { type: 'string', - required: true - } - } + required: true, + }, + }, } // INTERFACES @@ -182,7 +182,7 @@ const interfaces = dup(corePropsNoType) interfaces.properties.fields = { type: 'array', required: true, - properties: corePropsPlusArgs.properties + properties: corePropsPlusArgs.properties, } // ENUMS @@ -194,13 +194,13 @@ enums.properties.values = { properties: { name: { type: 'string', - required: true + required: true, }, description: { type: 'string', - required: true - } - } + required: true, + }, + }, } // UNIONS @@ -212,17 +212,17 @@ unions.properties.possibleTypes = { properties: { name: { type: 'string', - required: true + required: true, }, id: { type: 'string', - required: true + required: true, }, href: { type: 'string', - required: true - } - } + required: true, + }, + }, } // INPUT OBJECTS @@ -231,14 +231,14 @@ const inputObjects = dup(corePropsNoType) inputObjects.properties.inputFields = { type: 'array', required: true, - properties: coreProps.properties + properties: coreProps.properties, } // SCALARS const scalars = dup(corePropsNoType) scalars.properties.kind.required = false -function dup (obj) { +function dup(obj) { return JSON.parse(JSON.stringify(obj)) } @@ -251,5 +251,5 @@ export const schemaValidator = { enums, unions, inputObjects, - scalars + scalars, } diff --git a/lib/handle-exceptions.js b/lib/handle-exceptions.js index 12c161f11074..0c9c74c4ef8f 100644 --- a/lib/handle-exceptions.js +++ b/lib/handle-exceptions.js @@ -1,16 +1,18 @@ import FailBot from './failbot.js' -process.on('uncaughtException', async err => { +process.on('uncaughtException', async (err) => { if (err.code === 'MODULE_NOT_FOUND') { console.error('\n\n๐Ÿ”ฅ Uh oh! It looks you are missing a required npm module.') - console.error('Please run `npm install` to make sure you have all the required dependencies.\n\n') + console.error( + 'Please run `npm install` to make sure you have all the required dependencies.\n\n' + ) } console.error(err) await FailBot.report(err) }) -process.on('unhandledRejection', async err => { +process.on('unhandledRejection', async (err) => { console.error(err) await FailBot.report(err) }) diff --git a/lib/hydro.js b/lib/hydro.js index e55a61adfd0b..51060e93bcb5 100644 --- a/lib/hydro.js +++ b/lib/hydro.js @@ -14,11 +14,11 @@ const SCHEMAS = { redirect: 'docs.v0.RedirectEvent', clipboard: 'docs.v0.ClipboardEvent', print: 'docs.v0.PrintEvent', - preference: 'docs.v0.PreferenceEvent' + preference: 'docs.v0.PreferenceEvent', } export default class Hydro { - constructor ({ secret, endpoint } = {}) { + constructor({ secret, endpoint } = {}) { this.secret = secret || process.env.HYDRO_SECRET this.endpoint = endpoint || process.env.HYDRO_ENDPOINT this.schemas = SCHEMAS @@ -27,7 +27,7 @@ export default class Hydro { /** * Can check if it can actually send to Hydro */ - maySend () { + maySend() { return Boolean(this.secret && this.endpoint) } @@ -36,10 +36,8 @@ export default class Hydro { * to authenticate with Hydro * @param {string} body */ - generatePayloadHmac (body) { - return crypto.createHmac('sha256', this.secret) - .update(body) - .digest('hex') + generatePayloadHmac(body) { + return crypto.createHmac('sha256', this.secret).update(body).digest('hex') } /** @@ -47,7 +45,7 @@ export default class Hydro { * @param {string} schema * @param {any} value */ - async publish (schema, value) { + async publish(schema, value) { return this.publishMany([{ schema, value }]) } @@ -55,25 +53,26 @@ export default class Hydro { * Publish multiple events to Hydro * @param {[{ schema: string, value: any }]} events */ - async publishMany (events) { + async publishMany(events) { const body = JSON.stringify({ events: events.map(({ schema, value }) => ({ schema, value: JSON.stringify(value), // We must double-encode the value property - cluster: 'potomac' // We only have ability to publish externally to potomac cluster - })) + cluster: 'potomac', // We only have ability to publish externally to potomac cluster + })), }) const token = this.generatePayloadHmac(body) - const doFetch = () => fetch(this.endpoint, { - method: 'POST', - body, - headers: { - Authorization: `Hydro ${token}`, - 'Content-Type': 'application/json', - 'X-Hydro-App': 'docs-production' - } - }) + const doFetch = () => + fetch(this.endpoint, { + method: 'POST', + body, + headers: { + Authorization: `Hydro ${token}`, + 'Content-Type': 'application/json', + 'X-Hydro-App': 'docs-production', + }, + }) const res = await statsd.asyncTimer(doFetch, 'hydro.response_time')() @@ -88,13 +87,17 @@ export default class Hydro { FailBot.report(err, { hydroStatus: res.status, - hydroText: res.statusText + hydroText: res.statusText, }) // If the Hydro request failed as an "Unprocessable Entity", log it for diagnostics if (res.status === 422) { const failures = await res.json() - console.error(`Hydro schema validation failed:\n - Request: ${body}\n - Failures: ${JSON.stringify(failures)}`) + console.error( + `Hydro schema validation failed:\n - Request: ${body}\n - Failures: ${JSON.stringify( + failures + )}` + ) } throw err diff --git a/lib/instrument-middleware.js b/lib/instrument-middleware.js index f9bfdb92fd42..c48404ea9009 100644 --- a/lib/instrument-middleware.js +++ b/lib/instrument-middleware.js @@ -1,7 +1,7 @@ import path from 'path' import statsd from './statsd.js' -export default function instrumentMiddleware (middleware, relativePath) { +export default function instrumentMiddleware(middleware, relativePath) { // Requires the file as if it were being required from '../middleware/index.js'. // This is a little wonky, but let's us write `app.use(instrument(path))` and // maintain the name of the file, instead of hard-coding it for each middleware. diff --git a/lib/is-archived-version.js b/lib/is-archived-version.js index 22c120340edd..7b9e358464ab 100644 --- a/lib/is-archived-version.js +++ b/lib/is-archived-version.js @@ -1,15 +1,18 @@ import patterns from '../lib/patterns.js' import { deprecated } from '../lib/enterprise-server-releases.js' -export default function isArchivedVersion (req) { +export default function isArchivedVersion(req) { // if this is an assets path, use the referrer // if this is a docs path, use the req.path - const pathToCheck = patterns.assetPaths.test(req.path) - ? req.get('referrer') - : req.path + const pathToCheck = patterns.assetPaths.test(req.path) ? req.get('referrer') : req.path // ignore paths that don't have an enterprise version number - if (!(patterns.getEnterpriseVersionNumber.test(pathToCheck) || patterns.getEnterpriseServerNumber.test(pathToCheck))) { + if ( + !( + patterns.getEnterpriseVersionNumber.test(pathToCheck) || + patterns.getEnterpriseServerNumber.test(pathToCheck) + ) + ) { return {} } diff --git a/lib/languages.js b/lib/languages.js index 90c2ef230d0a..0305ce973d92 100644 --- a/lib/languages.js +++ b/lib/languages.js @@ -6,7 +6,7 @@ const languages = { code: 'en', hreflang: 'en', dir: '', - wip: false + wip: false, }, cn: { name: 'Simplified Chinese', @@ -15,7 +15,7 @@ const languages = { hreflang: 'zh-Hans', redirectPatterns: [/^\/zh-\w{2}/, /^\/zh/], dir: 'translations/zh-CN', - wip: false + wip: false, }, ja: { name: 'Japanese', @@ -24,7 +24,7 @@ const languages = { hreflang: 'ja', redirectPatterns: [/^\/jp/], dir: 'translations/ja-JP', - wip: false + wip: false, }, es: { name: 'Spanish', @@ -32,7 +32,7 @@ const languages = { code: 'es', hreflang: 'es', dir: 'translations/es-XL', - wip: false + wip: false, }, pt: { name: 'Portuguese', @@ -40,7 +40,7 @@ const languages = { code: 'pt', hreflang: 'pt', dir: 'translations/pt-BR', - wip: false + wip: false, }, de: { name: 'German', @@ -48,12 +48,12 @@ const languages = { code: 'de', hreflang: 'de', dir: 'translations/de-DE', - wip: true - } + wip: true, + }, } if (process.env.ENABLED_LANGUAGES) { - Object.keys(languages).forEach(code => { + Object.keys(languages).forEach((code) => { if (!process.env.ENABLED_LANGUAGES.includes(code)) delete languages[code] }) console.log(`ENABLED_LANGUAGES: ${process.env.ENABLED_LANGUAGES}`) diff --git a/lib/layouts.js b/lib/layouts.js index bd2ed824b7dd..2403ad0d6db4 100644 --- a/lib/layouts.js +++ b/lib/layouts.js @@ -9,9 +9,9 @@ const layoutsDirectory = path.join(__dirname, '../layouts') const layouts = {} walk(layoutsDirectory, { directories: false }) - .filter(entry => validLayoutExtensions.includes(path.extname(entry.relativePath))) - .filter(entry => !entry.relativePath.includes('README')) - .forEach(entry => { + .filter((entry) => validLayoutExtensions.includes(path.extname(entry.relativePath))) + .filter((entry) => !entry.relativePath.includes('README')) + .forEach((entry) => { const key = path.basename(entry.relativePath).split('.').slice(0, -1).join('.') const fullPath = path.join(entry.basePath, entry.relativePath) const content = fs.readFileSync(fullPath, 'utf8') diff --git a/lib/liquid-tags/data.js b/lib/liquid-tags/data.js index d3a68b8eaf7d..d52c48dde1ae 100644 --- a/lib/liquid-tags/data.js +++ b/lib/liquid-tags/data.js @@ -4,7 +4,7 @@ const Syntax = /([a-z0-9/\\_.\-[\]]+)/i const SyntaxHelp = "Syntax Error in 'data' - Valid syntax: data [path]" export default { - parse (tagToken) { + parse(tagToken) { if (!tagToken || !Syntax.test(tagToken.args)) { throw new TokenizationError(SyntaxHelp, tagToken) } @@ -12,9 +12,9 @@ export default { this.path = tagToken.args }, - async render (scope) { + async render(scope) { const value = await this.liquid.evalValue(`site.data.${this.path}`, scope) if (typeof value !== 'string') return value return this.liquid.parseAndRender(value, scope.environments) - } + }, } diff --git a/lib/liquid-tags/extended-markdown.js b/lib/liquid-tags/extended-markdown.js index 5acac7b71a2a..5cfcd130fb13 100644 --- a/lib/liquid-tags/extended-markdown.js +++ b/lib/liquid-tags/extended-markdown.js @@ -10,39 +10,40 @@ export const tags = { tip: 'border rounded-1 mb-4 p-3 color-border-info color-bg-info f5', note: 'border rounded-1 mb-4 p-3 color-border-info color-bg-info f5', warning: 'border rounded-1 mb-4 p-3 color-border-danger color-bg-danger f5', - danger: 'border rounded-1 mb-4 p-3 color-border-danger color-bg-danger f5' + danger: 'border rounded-1 mb-4 p-3 color-border-danger color-bg-danger f5', } -export const template = '
{{ output }}
' +export const template = + '
{{ output }}
' export const ExtendedMarkdown = { type: 'block', - parse (tagToken, remainTokens) { + parse(tagToken, remainTokens) { this.tagName = tagToken.name this.templates = [] const stream = this.liquid.parser.parseStream(remainTokens) stream .on(`tag:end${this.tagName}`, () => stream.stop()) - .on('template', tpl => this.templates.push(tpl)) + .on('template', (tpl) => this.templates.push(tpl)) .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) }) stream.start() }, - render: function * (scope) { + render: function* (scope) { const output = yield this.liquid.renderer.renderTemplates(this.templates, scope) return yield this.liquid.parseAndRender(template, { tagName: this.tagName, classes: tags[this.tagName], - output + output, }) - } + }, } export default { tags, - ExtendedMarkdown + ExtendedMarkdown, } diff --git a/lib/liquid-tags/ifversion-supported-operators.js b/lib/liquid-tags/ifversion-supported-operators.js index 3ce23582c7e2..03234759e50e 100644 --- a/lib/liquid-tags/ifversion-supported-operators.js +++ b/lib/liquid-tags/ifversion-supported-operators.js @@ -1,6 +1 @@ -export default [ - '=', - '<', - '>', - '!=' -] +export default ['=', '<', '>', '!='] diff --git a/lib/liquid-tags/ifversion.js b/lib/liquid-tags/ifversion.js index b061804e0035..9df648bfa728 100644 --- a/lib/liquid-tags/ifversion.js +++ b/lib/liquid-tags/ifversion.js @@ -2,7 +2,8 @@ import { isTruthy, Expression, TokenizationError } from 'liquidjs' import versionSatisfiesRange from '../version-satisfies-range.js' import supportedOperators from './ifversion-supported-operators.js' -const SyntaxHelp = "Syntax Error in 'ifversion' with range - Valid syntax: ifversion [operator] [releaseNumber]" +const SyntaxHelp = + "Syntax Error in 'ifversion' with range - Valid syntax: ifversion [operator] [releaseNumber]" const supportedOperatorsRegex = new RegExp(`[${supportedOperators.join('')}]`) const releaseRegex = /\d\d?\.\d\d?/ @@ -14,21 +15,24 @@ const notRegex = /(?:^|\s)not\s/ // don't work the way we want because they evaluate 3.2 > 3.10 = true. export default { // The following is verbatim from https://github.com/harttle/liquidjs/blob/v9.22.1/src/builtin/tags/if.ts - parse (tagToken, remainTokens) { + parse(tagToken, remainTokens) { this.tagToken = tagToken this.branches = [] this.elseTemplates = [] let p - const stream = this.liquid.parser.parseStream(remainTokens) - .on('start', () => this.branches.push({ - cond: tagToken.args, - templates: (p = []) - })) + const stream = this.liquid.parser + .parseStream(remainTokens) + .on('start', () => + this.branches.push({ + cond: tagToken.args, + templates: (p = []), + }) + ) .on('tag:elsif', (token) => { this.branches.push({ cond: token.args, - templates: p = [] + templates: (p = []), }) }) .on('tag:else', () => (p = this.elseTemplates)) @@ -43,7 +47,7 @@ export default { // The following is _mostly_ verbatim from https://github.com/harttle/liquidjs/blob/v9.22.1/src/builtin/tags/if.ts // The additions here are the handleNots() and handleOperators() calls. - render: function * (ctx, emitter) { + render: function* (ctx, emitter) { const r = this.liquid.renderer const { operators, operatorsTrie } = this.liquid.options @@ -61,7 +65,12 @@ export default { resolvedBranchCond = this.handleOperators(resolvedBranchCond) // Use Liquid's native function for the final evaluation. - const cond = yield new Expression(resolvedBranchCond, operators, operatorsTrie, ctx.opts.lenientIf).value(ctx) + const cond = yield new Expression( + resolvedBranchCond, + operators, + operatorsTrie, + ctx.opts.lenientIf + ).value(ctx) if (isTruthy(cond, ctx)) { yield r.renderTemplates(branch.templates, ctx, emitter) @@ -71,13 +80,13 @@ export default { yield r.renderTemplates(this.elseTemplates, ctx, emitter) }, - handleNots (resolvedBranchCond) { + handleNots(resolvedBranchCond) { if (!notRegex.test(resolvedBranchCond)) return resolvedBranchCond const condArray = resolvedBranchCond.split(' ') // Find the first index in the array that contains "not". - const notIndex = condArray.findIndex(el => el === 'not') + const notIndex = condArray.findIndex((el) => el === 'not') // E.g., ['not', 'fpt'] const condParts = condArray.slice(notIndex, notIndex + 2) @@ -86,7 +95,7 @@ export default { const versionToEvaluate = condParts[1] // If the current version is the version being evaluated in the conditional, - // that is negated and resolved to false. If it's NOT the version being + // that is negated and resolved to false. If it's NOT the version being // evaluated, that resolves to true. const resolvedBoolean = !(versionToEvaluate === this.currentVersionShortName) @@ -101,14 +110,14 @@ export default { return resolvedBranchCond }, - handleOperators (resolvedBranchCond) { + handleOperators(resolvedBranchCond) { if (!supportedOperatorsRegex.test(resolvedBranchCond)) return resolvedBranchCond // If this conditional contains multiple parts using `or` or `and`, get only the conditional with operators. const condArray = resolvedBranchCond.split(' ') // Find the first index in the array that contains an operator. - const operatorIndex = condArray.findIndex(el => supportedOperators.find(op => el === op)) + const operatorIndex = condArray.findIndex((el) => supportedOperators.find((op) => el === op)) // E.g., ['ghae', '<', '3.1'] const condParts = condArray.slice(operatorIndex - 1, operatorIndex + 2) @@ -127,15 +136,17 @@ export default { if (operator === '!=') { // If this is the current version, compare the release numbers. (Our semver package doesn't handle !=.) // If it's not the current version, it's always true. - resolvedBoolean = versionShortName === this.currentVersionShortName - ? releaseToEvaluate !== this.currentRelease - : true + resolvedBoolean = + versionShortName === this.currentVersionShortName + ? releaseToEvaluate !== this.currentRelease + : true } else { // If this is the current version, evaluate the operator using semver. // If it's not the current version, it's always false. - resolvedBoolean = versionShortName === this.currentVersionShortName - ? versionSatisfiesRange(this.currentRelease, `${operator}${releaseToEvaluate}`) - : false + resolvedBoolean = + versionShortName === this.currentVersionShortName + ? versionSatisfiesRange(this.currentRelease, `${operator}${releaseToEvaluate}`) + : false } // Replace syntax like `fpt or ghes < 3.0` with `fpt or true` or `fpt or false`. @@ -147,5 +158,5 @@ export default { } return resolvedBranchCond - } + }, } diff --git a/lib/liquid-tags/indented-data-reference.js b/lib/liquid-tags/indented-data-reference.js index 7373265f0d27..babc5823260f 100644 --- a/lib/liquid-tags/indented-data-reference.js +++ b/lib/liquid-tags/indented-data-reference.js @@ -11,11 +11,11 @@ import assert from 'assert' // affecting the formatting when the reference is used elsewhere via {{ site.data.foo.bar }}. export default { - parse (tagToken) { + parse(tagToken) { this.markup = tagToken.args.trim() }, - async render (scope) { + async render(scope) { // obfuscate first legit space, remove all other spaces, then restore legit space // this way we can support spaces=NUMBER as well as spaces = NUMBER const input = this.markup @@ -38,8 +38,8 @@ export default { if (!value) return // add spaces to each line - const renderedReferenceWithIndent = value.replace(/^/mg, ' '.repeat(numSpaces)) + const renderedReferenceWithIndent = value.replace(/^/gm, ' '.repeat(numSpaces)) return this.liquid.parseAndRender(renderedReferenceWithIndent, scope.environments) - } + }, } diff --git a/lib/liquid-tags/link-as-article-card.js b/lib/liquid-tags/link-as-article-card.js index 1c6ca586201c..cfb9a1ab8e39 100644 --- a/lib/liquid-tags/link-as-article-card.js +++ b/lib/liquid-tags/link-as-article-card.js @@ -2,14 +2,14 @@ import link from './link.js' const linkAsArticleCard = link('link-as-article-card') // For details, see class method in lib/liquid-tags/link.js -linkAsArticleCard.renderPageProps = async function renderPageProps (page, ctx, props) { +linkAsArticleCard.renderPageProps = async function renderPageProps(page, ctx, props) { const renderedProps = await link().renderPageProps(page, ctx, props) const { type: typeKey, topics = [] } = page const typeVal = typeKey ? ctx.site.data.ui.product_sublanding.guide_types[typeKey] : null return { ...renderedProps, type: { key: typeKey, value: typeVal }, - topics + topics, } } diff --git a/lib/liquid-tags/link.js b/lib/liquid-tags/link.js index 16c1551bd8a0..e61e1db41095 100644 --- a/lib/liquid-tags/link.js +++ b/lib/liquid-tags/link.js @@ -26,17 +26,17 @@ const liquidVariableSyntax = /^{{\s*(.*)\s*}}/ // Liquid Docs: https://github.com/liquid-lang/liquid-node#registering-new-tags export default (name) => ({ - parse (tagToken) { + parse(tagToken) { this.param = tagToken.args.trim() }, - async getTemplate () { + async getTemplate() { const pathToTemplate = path.join(__dirname, '../../includes/liquid-tags', `${name}.html`) const template = await readFileAsync(pathToTemplate, 'utf8') return template.replace(/\r/g, '') }, - async renderPageProps (page, ctx, props) { + async renderPageProps(page, ctx, props) { const renderedProps = {} for (const propName in props) { @@ -47,7 +47,7 @@ export default (name) => ({ return renderedProps }, - async render (scope) { + async render(scope) { const template = await this.getTemplate() const ctx = scope.environments @@ -79,7 +79,14 @@ export default (name) => ({ } // add language code and version - fullPath = removeFPTFromPath(path.posix.join('/', ctx.currentLanguage, ctx.currentVersion, getPathWithoutLanguage(getPathWithoutVersion(fullPath)))) + fullPath = removeFPTFromPath( + path.posix.join( + '/', + ctx.currentLanguage, + ctx.currentVersion, + getPathWithoutLanguage(getPathWithoutVersion(fullPath)) + ) + ) // find the page based on the full path const page = findPage(fullPath, ctx.pages, ctx.redirects) @@ -96,11 +103,11 @@ export default (name) => ({ // find and render the props const renderedProps = await this.renderPageProps(page, ctx, { title: { opt: { textOnly: true, encodeEntities: true } }, - intro: { opt: { unwrap: true } } + intro: { opt: { unwrap: true } }, }) const parsed = await this.liquid.parseAndRender(template, { fullPath, ...renderedProps }) return parsed.trim() - } + }, }) diff --git a/lib/liquid-tags/liquid-tag.js b/lib/liquid-tags/liquid-tag.js index 49b3cdf9f253..cb4071ad6b6c 100644 --- a/lib/liquid-tags/liquid-tag.js +++ b/lib/liquid-tags/liquid-tag.js @@ -6,20 +6,23 @@ import { paramCase } from 'change-case' const __dirname = path.dirname(fileURLToPath(import.meta.url)) export default class LiquidTag extends Liquid.Tag { - constructor (template, tagName, param) { + constructor(template, tagName, param) { super() this.param = param this.tagName = tagName - this.templatePath = path.join(__dirname, `../../includes/liquid-tags/${paramCase(this.constructor.name)}.html`) + this.templatePath = path.join( + __dirname, + `../../includes/liquid-tags/${paramCase(this.constructor.name)}.html` + ) this.template = null return this } - async render (context) { + async render(context) { return this.parseTemplate(context) } - async getTemplate () { + async getTemplate() { if (!this.template) { this.template = await readFileAsync(this.templatePath, 'utf8') this.template = this.template.replace(/\r/g, '') diff --git a/lib/liquid-tags/octicon.js b/lib/liquid-tags/octicon.js index d68ed89721e0..e3c72827b402 100644 --- a/lib/liquid-tags/octicon.js +++ b/lib/liquid-tags/octicon.js @@ -13,7 +13,7 @@ const SyntaxHelp = 'Syntax Error in tag \'octicon\' - Valid syntax: octicon " version.nonEnterpriseDefault).version +const nonEnterpriseDefaultVersion = Object.values(xAllVersions).find( + (version) => version.nonEnterpriseDefault +).version export default nonEnterpriseDefaultVersion diff --git a/lib/old-versions-utils.js b/lib/old-versions-utils.js index ff20e4f13ea3..2ae0e9ef0b01 100644 --- a/lib/old-versions-utils.js +++ b/lib/old-versions-utils.js @@ -18,24 +18,24 @@ const newVersions = Object.keys(xAllVersions) // return an old version like 2.21. // Fall back to latest GHES version if one can't be found, // for example, if the new version is private-instances@latest. -export function getOldVersionFromNewVersion (newVersion) { +export function getOldVersionFromNewVersion(newVersion) { return newVersion === nonEnterpriseDefaultVersion ? 'dotcom' - : oldVersions.find(oldVersion => newVersion.includes(oldVersion)) || latest + : oldVersions.find((oldVersion) => newVersion.includes(oldVersion)) || latest } // Given an old version like 2.21, // return a new version like enterprise-server@2.21. // Fall back to latest GHES version if one can't be found. -export function getNewVersionFromOldVersion (oldVersion) { +export function getNewVersionFromOldVersion(oldVersion) { return oldVersion === 'dotcom' ? nonEnterpriseDefaultVersion - : newVersions.find(newVersion => newVersion.includes(oldVersion)) || latestNewVersion + : newVersions.find((newVersion) => newVersion.includes(oldVersion)) || latestNewVersion } // Given an old path like /enterprise/2.21/user/github/category/article, // return an old version like 2.21. -export function getOldVersionFromOldPath (oldPath, languageCode) { +export function getOldVersionFromOldPath(oldPath, languageCode) { // We should never be calling this function on a path that starts with a new version, // so we can assume the path either uses the old /enterprise format or it's dotcom. if (!patterns.enterprise.test(oldPath)) return 'dotcom' @@ -46,7 +46,7 @@ export function getOldVersionFromOldPath (oldPath, languageCode) { // Given an old path like /en/enterprise/2.21/user/github/category/article, // return a new path like /en/enterprise-server@2.21/github/category/article. -export function getNewVersionedPath (oldPath, languageCode = '') { +export function getNewVersionedPath(oldPath, languageCode = '') { // It's possible a new version has been injected into an old path // via syntax like: /en/enterprise/{{ currentVersion }}/admin/category/article // which could resolve to /en/enterprise/private-instances@latest/admin/category/article, @@ -76,5 +76,5 @@ export default { getOldVersionFromOldPath, getOldVersionFromNewVersion, getNewVersionFromOldVersion, - getNewVersionedPath + getNewVersionedPath, } diff --git a/lib/page-data.js b/lib/page-data.js index 3ef473ef7dbd..17a68cc2f36f 100644 --- a/lib/page-data.js +++ b/lib/page-data.js @@ -8,22 +8,23 @@ import loadSiteData from './site-data.js' import nonEnterpriseDefaultVersion from './non-enterprise-default-version.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const versions = Object.keys(xAllVersions) -const enterpriseServerVersions = versions.filter(v => v.startsWith('enterprise-server@')) +const enterpriseServerVersions = versions.filter((v) => v.startsWith('enterprise-server@')) const renderOpts = { textOnly: true, encodeEntities: true } /** * We only need to initialize pages _once per language_ since pages don't change per version. So we do that * first since it's the most expensive work. This gets us a nested object with pages attached that we can use * as the basis for the siteTree after we do some versioning. We can also use it to derive the pageList. -*/ -export async function loadUnversionedTree () { + */ +export async function loadUnversionedTree() { const unversionedTree = {} - await Promise.all(Object.values(languages) - .map(async (langObj) => { + await Promise.all( + Object.values(languages).map(async (langObj) => { const localizedContentPath = path.posix.join(__dirname, '..', langObj.dir, 'content') unversionedTree[langObj.code] = await createTree(localizedContentPath, langObj) - })) + }) + ) return unversionedTree } @@ -40,37 +41,48 @@ export async function loadUnversionedTree () { * * Order of languages and versions doesn't matter, but order of child page arrays DOES matter (for navigation). */ -export async function loadSiteTree (unversionedTree, siteData) { +export async function loadSiteTree(unversionedTree, siteData) { const site = siteData || loadSiteData() - const rawTree = Object.assign({}, (unversionedTree || await loadUnversionedTree())) + const rawTree = Object.assign({}, unversionedTree || (await loadUnversionedTree())) const siteTree = {} // For every language... - await Promise.all(Object.keys(languages).map(async (langCode) => { - const treePerVersion = {} - // in every version... - await Promise.all(versions.map(async (version) => { - // "version" the pages. - treePerVersion[version] = await versionPages(Object.assign({}, rawTree[langCode]), version, langCode, site) - })) - - siteTree[langCode] = treePerVersion - })) + await Promise.all( + Object.keys(languages).map(async (langCode) => { + const treePerVersion = {} + // in every version... + await Promise.all( + versions.map(async (version) => { + // "version" the pages. + treePerVersion[version] = await versionPages( + Object.assign({}, rawTree[langCode]), + version, + langCode, + site + ) + }) + ) + + siteTree[langCode] = treePerVersion + }) + ) return siteTree } -export async function versionPages (obj, version, langCode, site) { +export async function versionPages(obj, version, langCode, site) { // Add a versioned href as a convenience for use in layouts. - obj.href = obj.page.permalinks - .find(pl => pl.pageVersion === version || (pl.pageVersion === 'homepage' && version === nonEnterpriseDefaultVersion)) - .href + obj.href = obj.page.permalinks.find( + (pl) => + pl.pageVersion === version || + (pl.pageVersion === 'homepage' && version === nonEnterpriseDefaultVersion) + ).href const context = { currentLanguage: langCode, currentVersion: version, enterpriseServerVersions, - site: site[langCode].site + site: site[langCode].site, } // The Liquid parseAndRender method is MUCH faster than renderContent or renderProp. @@ -88,11 +100,13 @@ export async function versionPages (obj, version, langCode, site) { if (!obj.childPages) return obj - const versionedChildPages = await Promise.all(obj.childPages - // Drop child pages that do not apply to the current version. - .filter(childPage => childPage.page.applicableVersions.includes(version)) - // Version the child pages recursively. - .map(childPage => versionPages(Object.assign({}, childPage), version, langCode, site))) + const versionedChildPages = await Promise.all( + obj.childPages + // Drop child pages that do not apply to the current version. + .filter((childPage) => childPage.page.applicableVersions.includes(version)) + // Version the child pages recursively. + .map((childPage) => versionPages(Object.assign({}, childPage), version, langCode, site)) + ) obj.childPages = [...versionedChildPages] @@ -100,20 +114,24 @@ export async function versionPages (obj, version, langCode, site) { } // Derive a flat array of Page objects in all languages. -export async function loadPageList (unversionedTree) { - const rawTree = unversionedTree || await loadUnversionedTree() +export async function loadPageList(unversionedTree) { + const rawTree = unversionedTree || (await loadUnversionedTree()) const pageList = [] - await Promise.all(Object.keys(languages).map(async (langCode) => { - await addToCollection(rawTree[langCode], pageList) - })) + await Promise.all( + Object.keys(languages).map(async (langCode) => { + await addToCollection(rawTree[langCode], pageList) + }) + ) - async function addToCollection (item, collection) { + async function addToCollection(item, collection) { if (!item.page) return collection.push(item.page) if (!item.childPages) return - await Promise.all(item.childPages.map(async (childPage) => await addToCollection(childPage, collection))) + await Promise.all( + item.childPages.map(async (childPage) => await addToCollection(childPage, collection)) + ) } return pageList @@ -122,23 +140,19 @@ export async function loadPageList (unversionedTree) { export const loadPages = loadPageList // Create an object from the list of all pages with permalinks as keys for fast lookup. -export function createMapFromArray (pageList) { - const pageMap = - pageList.reduce( - (pageMap, page) => { - for (const permalink of page.permalinks) { - pageMap[permalink.href] = page - } - return pageMap - }, - {} - ) +export function createMapFromArray(pageList) { + const pageMap = pageList.reduce((pageMap, page) => { + for (const permalink of page.permalinks) { + pageMap[permalink.href] = page + } + return pageMap + }, {}) return pageMap } -export async function loadPageMap (pageList) { - const pages = pageList || await loadPageList() +export async function loadPageMap(pageList) { + const pages = pageList || (await loadPageList()) return createMapFromArray(pages) } @@ -146,5 +160,5 @@ export default { loadUnversionedTree, loadSiteTree, loadPages: loadPageList, - loadPageMap + loadPageMap, } diff --git a/lib/page.js b/lib/page.js index 5f62695c0c6b..a6287239ebe6 100644 --- a/lib/page.js +++ b/lib/page.js @@ -20,13 +20,13 @@ import getDocumentType from './get-document-type.js' import { union } from 'lodash-es' class Page { - static async init (opts) { + static async init(opts) { opts = await Page.read(opts) if (!opts) return return new Page(opts) } - static async read (opts) { + static async read(opts) { assert(opts.languageCode, 'languageCode is required') assert(opts.relativePath, 'relativePath is required') assert(opts.basePath, 'basePath is required') @@ -37,7 +37,11 @@ class Page { // Per https://nodejs.org/api/fs.html#fs_fs_exists_path_callback // its better to read and handle errors than to check access/stats first try { - const { data, content, errors: frontmatterErrors } = await readFileContents(fullPath, opts.languageCode) + const { + data, + content, + errors: frontmatterErrors, + } = await readFileContents(fullPath, opts.languageCode) return { ...opts, @@ -45,7 +49,7 @@ class Page { fullPath, ...data, markdown: content, - frontmatterErrors + frontmatterErrors, } } catch (err) { if (err.code === 'ENOENT') return false @@ -53,7 +57,7 @@ class Page { } } - constructor (opts) { + constructor(opts) { Object.assign(this, { ...opts }) if (this.frontmatterErrors.length) { @@ -85,14 +89,24 @@ class Page { // a page should only be available in versions that its parent product is available in const versionsParentProductIsNotAvailableIn = this.applicableVersions // only the homepage will not have this.parentProduct - .filter(availableVersion => this.parentProduct && !this.parentProduct.versions.includes(availableVersion)) + .filter( + (availableVersion) => + this.parentProduct && !this.parentProduct.versions.includes(availableVersion) + ) if (versionsParentProductIsNotAvailableIn.length) { - throw new Error(`\`versions\` frontmatter in ${this.fullPath} contains ${versionsParentProductIsNotAvailableIn}, which ${this.parentProduct.id} product is not available in!`) + throw new Error( + `\`versions\` frontmatter in ${this.fullPath} contains ${versionsParentProductIsNotAvailableIn}, which ${this.parentProduct.id} product is not available in!` + ) } // derive array of Permalink objects - this.permalinks = Permalink.derive(this.languageCode, this.relativePath, this.title, this.applicableVersions) + this.permalinks = Permalink.derive( + this.languageCode, + this.relativePath, + this.title, + this.applicableVersions + ) if (this.relativePath.endsWith('index.md')) { // get an array of linked items in product and category TOCs @@ -101,9 +115,7 @@ class Page { // if this is an article and it doesn't have showMiniToc = false, set mini TOC to true if (!this.relativePath.endsWith('index.md')) { - this.showMiniToc = this.showMiniToc === false - ? this.showMiniToc - : true + this.showMiniToc = this.showMiniToc === false ? this.showMiniToc : true } // Instrument the `_render` method, so externally we call #render @@ -113,14 +125,14 @@ class Page { return this } - buildRedirects () { + buildRedirects() { // create backwards-compatible old paths for page permalinks and frontmatter redirects this.redirects = generateRedirectsForPermalinks(this.permalinks, this.redirect_from) return this.redirects } // Infer the parent product ID from the page's relative file path - get parentProductId () { + get parentProductId() { // Each page's top-level content directory matches its product ID const id = this.relativePath.split('/')[0] @@ -138,17 +150,17 @@ class Page { return id } - get parentProduct () { + get parentProduct() { return productMap[this.parentProductId] } - async renderTitle (context, opts = {}) { + async renderTitle(context, opts = {}) { return this.shortTitle ? this.renderProp('shortTitle', context, opts) : this.renderProp('title', context, opts) } - async _render (context) { + async _render(context) { // use English IDs/anchors for translated headings, so links don't break (see #8572) if (this.languageCode !== 'en') { const englishHeadings = getEnglishHeadings(this, context) @@ -157,15 +169,27 @@ class Page { this.intro = await renderContent(this.rawIntro, context) this.introPlainText = await renderContent(this.rawIntro, context, { textOnly: true }) - this.title = await renderContent(this.rawTitle, context, { textOnly: true, encodeEntities: true }) + this.title = await renderContent(this.rawTitle, context, { + textOnly: true, + encodeEntities: true, + }) this.titlePlainText = await renderContent(this.rawTitle, context, { textOnly: true }) - this.shortTitle = await renderContent(this.shortTitle, context, { textOnly: true, encodeEntities: true }) + this.shortTitle = await renderContent(this.shortTitle, context, { + textOnly: true, + encodeEntities: true, + }) this.product_video = await renderContent(this.raw_product_video, context, { textOnly: true }) if (this.introLinks) { - this.introLinks.quickstart = await renderContent(this.introLinks.rawQuickstart, context, { textOnly: true }) - this.introLinks.reference = await renderContent(this.introLinks.rawReference, context, { textOnly: true }) - this.introLinks.overview = await renderContent(this.introLinks.rawOverview, context, { textOnly: true }) + this.introLinks.quickstart = await renderContent(this.introLinks.rawQuickstart, context, { + textOnly: true, + }) + this.introLinks.reference = await renderContent(this.introLinks.rawReference, context, { + textOnly: true, + }) + this.introLinks.overview = await renderContent(this.introLinks.rawOverview, context, { + textOnly: true, + }) } context.relativePath = this.relativePath @@ -183,7 +207,10 @@ class Page { // Learning tracks may contain Liquid and need to have versioning processed. if (this.learningTracks) { - const { featuredTrack, learningTracks } = await processLearningTracks(this.rawLearningTracks, context) + const { featuredTrack, learningTracks } = await processLearningTracks( + this.rawLearningTracks, + context + ) this.featuredTrack = featuredTrack this.learningTracks = learningTracks } @@ -195,8 +222,8 @@ class Page { const { page } = guide guide.type = page.type if (page.topics) { - this.allTopics = union(this.allTopics, page.topics).sort( - (a, b) => a.localeCompare(b, page.languageCode) + this.allTopics = union(this.allTopics, page.topics).sort((a, b) => + a.localeCompare(b, page.languageCode) ) guide.topics = page.topics } @@ -206,18 +233,17 @@ class Page { } // set a flag so layout knows whether to render a mac/windows/linux switcher element - this.includesPlatformSpecificContent = ( + this.includesPlatformSpecificContent = html.includes('extended-markdown mac') || html.includes('extended-markdown windows') || html.includes('extended-markdown linux') - ) return html } // Allow other modules (like custom liquid tags) to make one-off requests // for a page's rendered properties like `title` and `intro` - async renderProp (propName, context, opts = { unwrap: false }) { + async renderProp(propName, context, opts = { unwrap: false }) { let prop if (propName === 'title') { prop = this.rawTitle @@ -241,20 +267,21 @@ class Page { // infer current page's corresponding homepage // /en/articles/foo -> /en // /en/enterprise/2.14/user/articles/foo -> /en/enterprise/2.14/user - static getHomepage (requestPath) { + static getHomepage(requestPath) { return requestPath.replace(/\/articles.*/, '') } // given a page path, return an array of objects containing hrefs // for that page in all languages - static getLanguageVariants (href) { + static getLanguageVariants(href) { const suffix = pathUtils.getPathWithoutLanguage(href) - return Object.values(languages).map(({ name, code, hreflang }) => { // eslint-disable-line + return Object.values(languages).map(({ name, code, hreflang }) => { + // eslint-disable-line return { name, code, hreflang, - href: `/${code}${suffix}`.replace(patterns.trailingSlash, '$1') + href: `/${code}${suffix}`.replace(patterns.trailingSlash, '$1'), } }) } diff --git a/lib/path-utils.js b/lib/path-utils.js index d8a4e193b549..41366b8f1859 100644 --- a/lib/path-utils.js +++ b/lib/path-utils.js @@ -9,19 +9,21 @@ const supportedVersions = new Set(Object.keys(allVersions)) // Add the language to the given HREF // /articles/foo -> /en/articles/foo -export function getPathWithLanguage (href, languageCode) { - return slash(path.posix.join('/', languageCode, getPathWithoutLanguage(href))) - .replace(patterns.trailingSlash, '$1') +export function getPathWithLanguage(href, languageCode) { + return slash(path.posix.join('/', languageCode, getPathWithoutLanguage(href))).replace( + patterns.trailingSlash, + '$1' + ) } // Remove the language from the given HREF // /en/articles/foo -> /articles/foo -export function getPathWithoutLanguage (href) { +export function getPathWithoutLanguage(href) { return slash(href.replace(patterns.hasLanguageCode, '/')) } // Remove the version segment from the path -export function getPathWithoutVersion (href) { +export function getPathWithoutVersion(href) { const versionFromPath = getVersionStringFromPath(href) // If the derived version is not found in the list of all versions, just return the HREF @@ -31,7 +33,7 @@ export function getPathWithoutVersion (href) { } // Return the version segment in a path -export function getVersionStringFromPath (href) { +export function getVersionStringFromPath(href) { href = getPathWithoutLanguage(href) // Return immediately if this is a link to the homepage @@ -58,7 +60,7 @@ export function getVersionStringFromPath (href) { } // If it's just a plan with no @release (e.g., `enterprise-server`), return the latest release - const planObject = Object.values(allVersions).find(v => v.plan === versionFromPath) + const planObject = Object.values(allVersions).find((v) => v.plan === versionFromPath) if (planObject) { return allVersions[planObject.latestVersion].version } @@ -69,14 +71,14 @@ export function getVersionStringFromPath (href) { } // Return the corresponding object for the version segment in a path -export function getVersionObjectFromPath (href) { +export function getVersionObjectFromPath(href) { const versionFromPath = getVersionStringFromPath(href) return allVersions[versionFromPath] } // Return the product segment from the path -export function getProductStringFromPath (href) { +export function getProductStringFromPath(href) { href = getPathWithoutLanguage(href) if (href === '/') return 'homepage' @@ -85,12 +87,10 @@ export function getProductStringFromPath (href) { if (pathParts.includes('early-access')) return 'early-access' - return productIds.includes(pathParts[2]) - ? pathParts[2] - : pathParts[1] + return productIds.includes(pathParts[2]) ? pathParts[2] : pathParts[1] } -export function getCategoryStringFromPath (href) { +export function getCategoryStringFromPath(href) { href = getPathWithoutLanguage(href) if (href === '/') return null @@ -99,9 +99,7 @@ export function getCategoryStringFromPath (href) { if (pathParts.includes('early-access')) return null - const productIndex = productIds.includes(pathParts[2]) - ? 2 - : 1 + const productIndex = productIds.includes(pathParts[2]) ? 2 : 1 return pathParts[productIndex + 1] } @@ -113,5 +111,5 @@ export default { getVersionStringFromPath, getVersionObjectFromPath, getProductStringFromPath, - getCategoryStringFromPath + getCategoryStringFromPath, } diff --git a/lib/patterns.js b/lib/patterns.js index f28d992a45c6..8ce5f28d3dac 100644 --- a/lib/patterns.js +++ b/lib/patterns.js @@ -8,14 +8,14 @@ // and not capture: /github-foo export const githubDotcom = /\/github(\/|$|\?|#)/ - // we want to capture `/enterprise` and `/enterprise/foo` but NOT `/enterprise-admin` +// we want to capture `/enterprise` and `/enterprise/foo` but NOT `/enterprise-admin` export const enterprise = /\/enterprise(?:\/|$|\?)(\d+\.\d+)?/ export const admin = /enterprise\/(\d+\.\d+\/)?admin\/?/ export const gheUser = /enterprise\/(\d+\.\d+\/)?user(\/|$|\?)/ export const enterpriseHomepage = /\/enterprise\/?(\d+\.\d+)?$/ export const desktop = /desktop\// export const oldGuidesPath = /(\/admin|(^|\/)desktop)\/guides/ - // need to capture 11.10.340 and 2.0+ +// need to capture 11.10.340 and 2.0+ export const getEnterpriseVersionNumber = /^.*?enterprise\/(\d+\.\d+(?:\.340)?).*?$/ export const removeEnterpriseVersion = /(enterprise\/)\d+\.\d+\// export const guides = /guides\// @@ -33,11 +33,12 @@ export const assetPaths = /\/(?:javascripts|stylesheets|assets|node_modules|dist export const oldApiPath = /\/v[34]\/(?!guides|overview).+?\/.+/ export const staticRedirect = // export const enterpriseNoVersion = /\/enterprise\/([^\d].*$)/ - // a {{ currentVersion }} in internal links may inject '' into old paths, - // so the oldEnterprisePath regex must match: /enterprise/private-instances@latest/user, - // /enterprise/enterprise-server@2.22/user, /enterprise/2.22/user, and /enterprise/user -export const oldEnterprisePath = /\/([a-z]{2}\/)?(enterprise\/)?(\S+?@(\S+?\/))?(\d.\d+\/)?(user[/$])?/ - // new versioning format patterns +// a {{ currentVersion }} in internal links may inject '' into old paths, +// so the oldEnterprisePath regex must match: /enterprise/private-instances@latest/user, +// /enterprise/enterprise-server@2.22/user, /enterprise/2.22/user, and /enterprise/user +export const oldEnterprisePath = + /\/([a-z]{2}\/)?(enterprise\/)?(\S+?@(\S+?\/))?(\d.\d+\/)?(user[/$])?/ +// new versioning format patterns export const adminProduct = /\/admin(\/|$|\?|#)/ export const insightsProduct = /\/insights(\/|$|\?|#)/ export const enterpriseServer = /\/enterprise-server@/ diff --git a/lib/permalink.js b/lib/permalink.js index 80ca67ebe2cf..3ca9a935a8d0 100644 --- a/lib/permalink.js +++ b/lib/permalink.js @@ -5,7 +5,7 @@ import allVersions from './all-versions.js' import removeFPTFromPath from './remove-fpt-from-path.js' class Permalink { - constructor (languageCode, pageVersion, relativePath, title) { + constructor(languageCode, pageVersion, relativePath, title) { this.languageCode = languageCode this.pageVersion = pageVersion this.relativePath = relativePath @@ -13,30 +13,28 @@ class Permalink { const permalinkSuffix = this.constructor.relativePathToSuffix(relativePath) - this.href = removeFPTFromPath(path.posix.join('/', languageCode, pageVersion, permalinkSuffix)) - .replace(patterns.trailingSlash, '$1') + this.href = removeFPTFromPath( + path.posix.join('/', languageCode, pageVersion, permalinkSuffix) + ).replace(patterns.trailingSlash, '$1') this.pageVersionTitle = allVersions[pageVersion].versionTitle return this } - static derive (languageCode, relativePath, title, applicableVersions) { + static derive(languageCode, relativePath, title, applicableVersions) { assert(relativePath, 'relativePath is required') assert(languageCode, 'languageCode is required') - const permalinks = applicableVersions - .map(pageVersion => { - return new Permalink(languageCode, pageVersion, relativePath, title) - }) + const permalinks = applicableVersions.map((pageVersion) => { + return new Permalink(languageCode, pageVersion, relativePath, title) + }) return permalinks } - static relativePathToSuffix (relativePath) { - return '/' + relativePath - .replace('index.md', '') - .replace('.md', '') + static relativePathToSuffix(relativePath) { + return '/' + relativePath.replace('index.md', '').replace('.md', '') } } diff --git a/lib/prefix-stream-write.js b/lib/prefix-stream-write.js index 7bb231ebdcfc..0cb4610b8ecd 100644 --- a/lib/prefix-stream-write.js +++ b/lib/prefix-stream-write.js @@ -1,7 +1,7 @@ -export default function prefixStreamWrite (writableStream, prefix) { +export default function prefixStreamWrite(writableStream, prefix) { const oldWrite = writableStream.write - function newWrite (...args) { + function newWrite(...args) { const [chunk, encoding] = args // Prepend the prefix if the chunk is either a string or a Buffer. diff --git a/lib/process-learning-tracks.js b/lib/process-learning-tracks.js index fa55feca955c..55fece5814c7 100644 --- a/lib/process-learning-tracks.js +++ b/lib/process-learning-tracks.js @@ -6,7 +6,7 @@ const renderOpts = { textOnly: true, encodeEntities: true } // This module returns an object that contains a single featured learning track // and an array of all the other learning tracks for the current version. -export default async function processLearningTracks (rawLearningTracks, context) { +export default async function processLearningTracks(rawLearningTracks, context) { const learningTracks = [] let featuredTrack @@ -38,14 +38,16 @@ export default async function processLearningTracks (rawLearningTracks, context) description: await renderContent(track.description, context, renderOpts), // getLinkData respects versioning and only returns guides available in the current version; // if no guides are available, the learningTrack.guides property will be an empty array. - guides: await getLinkData(track.guides, context) + guides: await getLinkData(track.guides, context), } // Determine if this is the featured track. if (track.featured_track) { // Featured track properties may be booleans or string that include Liquid conditionals with versioning. // We need to parse any strings to determine if the featured track is relevant for this version. - isFeaturedTrack = track.featured_track === true || (await renderContent(track.featured_track, context, renderOpts) === 'true') + isFeaturedTrack = + track.featured_track === true || + (await renderContent(track.featured_track, context, renderOpts)) === 'true' if (isFeaturedTrack) { featuredTrack = learningTrack diff --git a/lib/product-names.js b/lib/product-names.js index 1733c23c4d0b..64f143f86a0a 100644 --- a/lib/product-names.js +++ b/lib/product-names.js @@ -1,10 +1,10 @@ import enterpriseServerReleases from '../lib/enterprise-server-releases.js' const productNames = { - dotcom: 'GitHub.com' + dotcom: 'GitHub.com', } -enterpriseServerReleases.all.forEach(version => { +enterpriseServerReleases.all.forEach((version) => { productNames[version] = `Enterprise Server ${version}` }) diff --git a/lib/read-file-contents.js b/lib/read-file-contents.js index 9800f5472d39..fd5e579006d7 100644 --- a/lib/read-file-contents.js +++ b/lib/read-file-contents.js @@ -5,7 +5,7 @@ import fm from './frontmatter.js' /** * Read only the frontmatter from file */ -export default async function fmfromf (filepath, languageCode) { +export default async function fmfromf(filepath, languageCode) { let fileContent = await readFileAsync(filepath, 'utf8') fileContent = encodeBracketedParentheses(fileContent) diff --git a/lib/read-frontmatter.js b/lib/read-frontmatter.js index 1060c123a636..5a515c364e02 100644 --- a/lib/read-frontmatter.js +++ b/lib/read-frontmatter.js @@ -2,7 +2,7 @@ import matter from 'gray-matter' import revalidator from 'revalidator' import { difference, intersection } from 'lodash-es' -function readFrontmatter (markdown, opts = { validateKeyNames: false, validateKeyOrder: false }) { +function readFrontmatter(markdown, opts = { validateKeyNames: false, validateKeyOrder: false }) { const schema = opts.schema || { properties: {} } const filepath = opts.filepath || null @@ -10,18 +10,20 @@ function readFrontmatter (markdown, opts = { validateKeyNames: false, validateKe let errors = [] try { - ({ content, data } = matter(markdown)) + ;({ content, data } = matter(markdown)) } catch (e) { const defaultReason = 'invalid frontmatter entry' const reason = e.reason - // make this common error message a little easier to understand - ? e.reason.startsWith('can not read a block mapping entry;') ? defaultReason : e.reason + ? // make this common error message a little easier to understand + e.reason.startsWith('can not read a block mapping entry;') + ? defaultReason + : e.reason : defaultReason const error = { reason, - message: 'YML parsing error!' + message: 'YML parsing error!', } if (filepath) error.filepath = filepath @@ -37,16 +39,16 @@ function readFrontmatter (markdown, opts = { validateKeyNames: false, validateKe // add filepath property to each error object if (errors.length && filepath) { - errors = errors.map(error => Object.assign(error, { filepath })) + errors = errors.map((error) => Object.assign(error, { filepath })) } // validate key names if (opts.validateKeyNames) { const invalidKeys = difference(existingKeys, allowedKeys) - invalidKeys.forEach(key => { + invalidKeys.forEach((key) => { const error = { property: key, - message: `not allowed. Allowed properties are: ${allowedKeys.join(', ')}` + message: `not allowed. Allowed properties are: ${allowedKeys.join(', ')}`, } if (filepath) error.filepath = filepath errors.push(error) @@ -57,7 +59,9 @@ function readFrontmatter (markdown, opts = { validateKeyNames: false, validateKe if (opts.validateKeyOrder && existingKeys.join('') !== expectedKeys.join('')) { const error = { property: 'keys', - message: `keys must be in order. Current: ${existingKeys.join(',')}; Expected: ${expectedKeys.join(',')}` + message: `keys must be in order. Current: ${existingKeys.join( + ',' + )}; Expected: ${expectedKeys.join(',')}`, } if (filepath) error.filepath = filepath errors.push(error) diff --git a/lib/read-json-file.js b/lib/read-json-file.js index a992166f5d15..b4fca9bf4c55 100644 --- a/lib/read-json-file.js +++ b/lib/read-json-file.js @@ -1,14 +1,6 @@ import fs from 'fs' import path from 'path' -export default function readJsonFile (xpath) { - return JSON.parse( - fs.readFileSync( - path.join( - process.cwd(), - xpath - ), - 'utf8' - ) - ) +export default function readJsonFile(xpath) { + return JSON.parse(fs.readFileSync(path.join(process.cwd(), xpath), 'utf8')) } diff --git a/lib/redirects/get-old-paths-from-permalink.js b/lib/redirects/get-old-paths-from-permalink.js index cc4899832bbd..e6faf07397be 100644 --- a/lib/redirects/get-old-paths-from-permalink.js +++ b/lib/redirects/get-old-paths-from-permalink.js @@ -1,5 +1,14 @@ -import { latest, deprecated, lastReleaseWithLegacyFormat, firstRestoredAdminGuides } from '../enterprise-server-releases.js' -import { getPathWithoutLanguage, getPathWithLanguage, getVersionStringFromPath } from '../path-utils.js' +import { + latest, + deprecated, + lastReleaseWithLegacyFormat, + firstRestoredAdminGuides, +} from '../enterprise-server-releases.js' +import { + getPathWithoutLanguage, + getPathWithLanguage, + getVersionStringFromPath, +} from '../path-utils.js' import patterns from '../patterns.js' import versionSatisfiesRange from '../version-satisfies-range.js' import xAllVersions from '../all-versions.js' @@ -9,16 +18,24 @@ const currentlySupportedVersions = Object.keys(xAllVersions) // This function takes a current path, applies what we know about historically // supported paths, and returns an array of ALL possible associated old // paths that users might try to hit. -export default function getOldPathsFromPath (currentPath, languageCode, currentVersion) { +export default function getOldPathsFromPath(currentPath, languageCode, currentVersion) { const oldPaths = new Set() const versionFromPath = getVersionStringFromPath(currentPath) // This only applies to Dotcom paths, so no need to determine whether the version is deprecated // create old path /free-pro-team@latest/github from new path /github (or from a frontmatter `redirect_from` path like /articles) - if (versionFromPath === 'homepage' || !(currentlySupportedVersions.includes(versionFromPath) || deprecated.includes(versionFromPath)) || (versionFromPath === nonEnterpriseDefaultVersion && !currentPath.includes(nonEnterpriseDefaultVersion))) { - oldPaths.add(currentPath - .replace(`/${languageCode}`, `/${languageCode}/${nonEnterpriseDefaultVersion}`)) + if ( + versionFromPath === 'homepage' || + !( + currentlySupportedVersions.includes(versionFromPath) || deprecated.includes(versionFromPath) + ) || + (versionFromPath === nonEnterpriseDefaultVersion && + !currentPath.includes(nonEnterpriseDefaultVersion)) + ) { + oldPaths.add( + currentPath.replace(`/${languageCode}`, `/${languageCode}/${nonEnterpriseDefaultVersion}`) + ) } // ------ BEGIN LEGACY VERSION FORMAT REPLACEMENTS ------// @@ -26,82 +43,78 @@ export default function getOldPathsFromPath (currentPath, languageCode, currentV // and archived versions paths. // create old path /insights from current path /enterprise/version/insights - oldPaths.add(currentPath - .replace(`/${languageCode}/enterprise/${latest}/user/insights`, '/insights')) + oldPaths.add( + currentPath.replace(`/${languageCode}/enterprise/${latest}/user/insights`, '/insights') + ) // create old path /desktop/guides from current path /desktop if (currentPath.includes('/desktop') && !currentPath.includes('/guides')) { - oldPaths.add(currentPath - .replace('/desktop', '/desktop/guides')) + oldPaths.add(currentPath.replace('/desktop', '/desktop/guides')) } // create old path /admin/guides from current path /admin if (currentPath.includes('admin') && !currentPath.includes('/guides')) { // ... but ONLY on versions <2.21 and in deep links on all versions - if (versionSatisfiesRange(currentVersion, `<${firstRestoredAdminGuides}`) || !currentPath.endsWith('/admin')) { - oldPaths.add(currentPath - .replace('/admin', '/admin/guides')) + if ( + versionSatisfiesRange(currentVersion, `<${firstRestoredAdminGuides}`) || + !currentPath.endsWith('/admin') + ) { + oldPaths.add(currentPath.replace('/admin', '/admin/guides')) } } // create old path /user from current path /user/github on 2.16+ only - if (currentlySupportedVersions.includes(currentVersion) || versionSatisfiesRange(currentVersion, '>2.15')) { - oldPaths.add(currentPath - .replace('/user/github', '/user')) + if ( + currentlySupportedVersions.includes(currentVersion) || + versionSatisfiesRange(currentVersion, '>2.15') + ) { + oldPaths.add(currentPath.replace('/user/github', '/user')) } // create old path /enterprise from current path /enterprise/latest - oldPaths.add(currentPath - .replace(`/enterprise/${latest}`, '/enterprise')) + oldPaths.add(currentPath.replace(`/enterprise/${latest}`, '/enterprise')) // create old path /enterprise/foo from current path /enterprise/user/foo // this supports old developer paths like /enterprise/webhooks with no /user in them if (currentPath.includes('/enterprise/')) { - oldPaths.add(currentPath - .replace('/user/', '/')) + oldPaths.add(currentPath.replace('/user/', '/')) } // ------ END LEGACY VERSION FORMAT REPLACEMENTS ------// // ------ BEGIN MODERN VERSION FORMAT REPLACEMENTS ------// - if (currentlySupportedVersions.includes(currentVersion) || versionSatisfiesRange(currentVersion, `>${lastReleaseWithLegacyFormat}`)) { - (new Set(oldPaths)).forEach(oldPath => { + if ( + currentlySupportedVersions.includes(currentVersion) || + versionSatisfiesRange(currentVersion, `>${lastReleaseWithLegacyFormat}`) + ) { + new Set(oldPaths).forEach((oldPath) => { // create old path /enterprise/ from new path /enterprise-server@ - oldPaths.add(oldPath - .replace(/\/enterprise-server@(\d)/, '/enterprise/$1')) + oldPaths.add(oldPath.replace(/\/enterprise-server@(\d)/, '/enterprise/$1')) // create old path /enterprise//user from new path /enterprise-server@/github - oldPaths.add(oldPath - .replace(/\/enterprise-server@(\d.+?)\/github/, '/enterprise/$1/user')) + oldPaths.add(oldPath.replace(/\/enterprise-server@(\d.+?)\/github/, '/enterprise/$1/user')) // create old path /insights from new path /enterprise-server@/insights - oldPaths.add(oldPath - .replace(`/enterprise-server@${latest}/insights`, '/insights')) + oldPaths.add(oldPath.replace(`/enterprise-server@${latest}/insights`, '/insights')) // create old path /admin from new path /enterprise-server@/admin - oldPaths.add(oldPath - .replace(`/enterprise-server@${latest}/admin`, '/admin')) + oldPaths.add(oldPath.replace(`/enterprise-server@${latest}/admin`, '/admin')) // create old path /enterprise from new path /enterprise-server@ - oldPaths.add(oldPath - .replace(`/enterprise-server@${latest}`, '/enterprise')) + oldPaths.add(oldPath.replace(`/enterprise-server@${latest}`, '/enterprise')) // create old path /enterprise-server from new path /enterprise-server@ - oldPaths.add(oldPath - .replace(`/enterprise-server@${latest}`, '/enterprise-server')) + oldPaths.add(oldPath.replace(`/enterprise-server@${latest}`, '/enterprise-server')) // create old path /enterprise-server@latest from new path /enterprise-server@ - oldPaths.add(oldPath - .replace(`/enterprise-server@${latest}`, '/enterprise-server@latest')) + oldPaths.add(oldPath.replace(`/enterprise-server@${latest}`, '/enterprise-server@latest')) if (!patterns.adminProduct.test(oldPath)) { // create old path /enterprise//user/foo from new path /enterprise-server@/foo - oldPaths.add(currentPath - .replace(/\/enterprise-server@(\d.+?)\//, '/enterprise/$1/user/')) + oldPaths.add(currentPath.replace(/\/enterprise-server@(\d.+?)\//, '/enterprise/$1/user/')) // create old path /enterprise/user/foo from new path /enterprise-server@/foo - oldPaths.add(currentPath - .replace(`/enterprise-server@${latest}/`, '/enterprise/user/')) + oldPaths.add(currentPath.replace(`/enterprise-server@${latest}/`, '/enterprise/user/')) } }) } @@ -110,14 +123,17 @@ export default function getOldPathsFromPath (currentPath, languageCode, currentV // ------ BEGIN ONEOFF REPLACEMENTS ------// // create special old path /enterprise-server-releases from current path /enterprise-server@/admin/all-releases - if (versionSatisfiesRange(currentVersion, `=${latest}`) && currentPath.endsWith('/admin/all-releases')) { + if ( + versionSatisfiesRange(currentVersion, `=${latest}`) && + currentPath.endsWith('/admin/all-releases') + ) { oldPaths.add('/enterprise-server-releases') } // ------ END ONEOFF REPLACEMENTS ------// // For each old path added to the set above, do the following... - (new Set(oldPaths)).forEach(oldPath => { + new Set(oldPaths).forEach((oldPath) => { // for English only, remove language code if (languageCode === 'en') { oldPaths.add(getPathWithoutLanguage(oldPath)) diff --git a/lib/redirects/permalinks.js b/lib/redirects/permalinks.js index a577d241a668..2a9cfaa328d5 100644 --- a/lib/redirects/permalinks.js +++ b/lib/redirects/permalinks.js @@ -7,7 +7,7 @@ import { getNewVersionedPath } from '../old-versions-utils.js' import removeFPTFromPath from '../remove-fpt-from-path.js' const supportedVersions = new Set(Object.keys(allVersions)) -export default function generateRedirectsForPermalinks (permalinks, redirectFrontmatter) { +export default function generateRedirectsForPermalinks(permalinks, redirectFrontmatter) { // account for Array or String frontmatter entries const redirectFrontmatterOldPaths = redirectFrontmatter ? Array.from([redirectFrontmatter]).flat() @@ -16,17 +16,21 @@ export default function generateRedirectsForPermalinks (permalinks, redirectFron const redirects = {} // for every permalink... - permalinks.forEach(permalink => { + permalinks.forEach((permalink) => { // get an array of possible old paths, e.g., /desktop/guides/ from current permalink /desktop/ - const possibleOldPaths = getOldPathsFromPermalink(permalink.href, permalink.languageCode, permalink.pageVersion) + const possibleOldPaths = getOldPathsFromPermalink( + permalink.href, + permalink.languageCode, + permalink.pageVersion + ) // for each old path, add a redirect to the current permalink - possibleOldPaths.forEach(oldPath => { + possibleOldPaths.forEach((oldPath) => { redirects[oldPath] = permalink.href }) // for every redirect frontmatter old path... - redirectFrontmatterOldPaths.forEach(frontmatterOldPath => { + redirectFrontmatterOldPaths.forEach((frontmatterOldPath) => { // remove trailing slashes (sometimes present in frontmatter) frontmatterOldPath = frontmatterOldPath.replace(patterns.trailingSlash, '$1') @@ -37,18 +41,28 @@ export default function generateRedirectsForPermalinks (permalinks, redirectFron } // get the old path for the current permalink version - let versionedFrontmatterOldPath = path.posix.join('/', permalink.languageCode, getNewVersionedPath(frontmatterOldPath)) + let versionedFrontmatterOldPath = path.posix.join( + '/', + permalink.languageCode, + getNewVersionedPath(frontmatterOldPath) + ) const versionFromPath = getVersionStringFromPath(versionedFrontmatterOldPath) - versionedFrontmatterOldPath = removeFPTFromPath(versionedFrontmatterOldPath.replace(versionFromPath, permalink.pageVersion)) + versionedFrontmatterOldPath = removeFPTFromPath( + versionedFrontmatterOldPath.replace(versionFromPath, permalink.pageVersion) + ) // add it to the redirects object redirects[versionedFrontmatterOldPath] = permalink.href // then get an array of possible alternative old paths from the current versioned old path - const possibleOldPathsForVersionedOldPaths = getOldPathsFromPermalink(versionedFrontmatterOldPath, permalink.languageCode, permalink.pageVersion) + const possibleOldPathsForVersionedOldPaths = getOldPathsFromPermalink( + versionedFrontmatterOldPath, + permalink.languageCode, + permalink.pageVersion + ) // and add each one to the redirects object - possibleOldPathsForVersionedOldPaths.forEach(oldPath => { + possibleOldPathsForVersionedOldPaths.forEach((oldPath) => { redirects[oldPath] = permalink.href }) }) diff --git a/lib/redirects/precompile.js b/lib/redirects/precompile.js index 83521d8aa57d..0c39769f9b12 100755 --- a/lib/redirects/precompile.js +++ b/lib/redirects/precompile.js @@ -5,17 +5,22 @@ const latestDevRedirects = {} // Replace hardcoded 'latest' with real value in the redirected path Object.entries(developerRedirects).forEach(([oldPath, newPath]) => { - latestDevRedirects[oldPath] = newPath.replace('enterprise-server@latest', `enterprise-server@${latest}`) + latestDevRedirects[oldPath] = newPath.replace( + 'enterprise-server@latest', + `enterprise-server@${latest}` + ) }) // This function runs at server warmup and precompiles possible redirect routes. // It outputs them in key-value pairs within a neat Javascript object: { oldPath: newPath } -export default async function precompileRedirects (pageList) { +export default async function precompileRedirects(pageList) { const allRedirects = Object.assign({}, latestDevRedirects) // CURRENT PAGES PERMALINKS AND FRONTMATTER // create backwards-compatible old paths for page permalinks and frontmatter redirects - await Promise.all(pageList.map(async (page) => Object.assign(allRedirects, page.buildRedirects()))) + await Promise.all( + pageList.map(async (page) => Object.assign(allRedirects, page.buildRedirects())) + ) return allRedirects } diff --git a/lib/redis-accessor.js b/lib/redis-accessor.js index a75c5577cb51..7b92e3442e37 100644 --- a/lib/redis-accessor.js +++ b/lib/redis-accessor.js @@ -8,19 +8,19 @@ const { CI, NODE_ENV, REDIS_URL } = process.env const useRealRedis = !CI && NODE_ENV !== 'test' && !!REDIS_URL class RedisAccessor { - constructor ({ + constructor({ databaseNumber = 0, prefix = null, allowSetFailures = false, allowGetFailures = false, - name = null + name = null, } = {}) { const redisClient = useRealRedis ? createRedisClient({ - url: REDIS_URL, - db: databaseNumber, - name: name || 'redis-accessor' - }) + url: REDIS_URL, + db: databaseNumber, + name: name || 'redis-accessor', + }) : InMemoryRedis.createClient() this._client = redisClient @@ -35,7 +35,7 @@ class RedisAccessor { } /** @private */ - prefix (key) { + prefix(key) { if (typeof key !== 'string' || !key) { throw new TypeError(`Key must be a non-empty string but was: ${JSON.stringify(key)}`) } @@ -43,14 +43,14 @@ class RedisAccessor { return this._prefix + key } - static translateSetArguments (options = {}) { + static translateSetArguments(options = {}) { const setArgs = [] const defaults = { newOnly: false, existingOnly: false, expireIn: null, // No expiration - rollingExpiration: true + rollingExpiration: true, } const opts = { ...defaults, ...options } @@ -83,7 +83,7 @@ class RedisAccessor { return setArgs } - async set (key, value, options = {}) { + async set(key, value, options = {}) { const setAsync = promisify(this._client.set).bind(this._client) const fullKey = this.prefix(key) @@ -112,7 +112,7 @@ Error: ${err.message}` } } - async get (key) { + async get(key) { const getAsync = promisify(this._client.get).bind(this._client) const fullKey = this.prefix(key) diff --git a/lib/redis/create-client.js b/lib/redis/create-client.js index 3b5f368b461d..5783661262e4 100644 --- a/lib/redis/create-client.js +++ b/lib/redis/create-client.js @@ -9,7 +9,7 @@ const redisMaxDb = REDIS_MAX_DB || 15 // Maximum delay between reconnection attempts after backoff const maxReconnectDelay = 5000 -function formatRedisError (error) { +function formatRedisError(error) { const errorCode = error ? error.code : null const errorName = error ? error.constructor.name : 'Server disconnection' const errorMsg = error ? error.toString() : 'unknown (commonly a server idle timeout)' @@ -17,7 +17,7 @@ function formatRedisError (error) { return preamble + ': ' + errorMsg } -export default function createClient (options = {}) { +export default function createClient(options = {}) { const { db, name, url } = options // If no Redis URL is provided, bail out @@ -29,13 +29,15 @@ export default function createClient (options = {}) { if (db != null) { if (!Number.isInteger(db) || db < redisMinDb || db > redisMaxDb) { throw new TypeError( - `Redis database number must be an integer between ${redisMinDb} and ${redisMaxDb} but was: ${JSON.stringify(db)}` + `Redis database number must be an integer between ${redisMinDb} and ${redisMaxDb} but was: ${JSON.stringify( + db + )}` ) } } let pingInterval = null - function stopPinging () { + function stopPinging() { if (pingInterval) { clearInterval(pingInterval) pingInterval = null @@ -46,12 +48,12 @@ export default function createClient (options = {}) { const client = Redis.createClient(url, { // Only add this configuration for TLS-enabled Redis URL values. // Otherwise, it breaks for local Redis instances without TLS enabled. - ...url.startsWith('rediss://') && { + ...(url.startsWith('rediss://') && { tls: { // Required for production Heroku Redis - rejectUnauthorized: false - } - }, + rejectUnauthorized: false, + }, + }), // Any running command that is unfulfilled when a connection is lost should // NOT be retried after the connection has been reestablished. @@ -73,26 +75,25 @@ export default function createClient (options = {}) { // Be aware that this retry (NOT just reconnection) strategy appears to // be a major point of confusion (and possibly legitimate issues) between // reconnecting and retrying failed commands. - retry_strategy: - function ({ - attempt, - error, - total_retry_time: totalRetryTime, - times_connected: timesConnected - }) { - let delayPerAttempt = 100 - - // If the server appears to be unavailable, slow down faster - if (error && error.code === 'ECONNREFUSED') { - delayPerAttempt *= 5 - } - - // Reconnect after delay - return Math.min(attempt * delayPerAttempt, maxReconnectDelay) - }, + retry_strategy: function ({ + attempt, + error, + total_retry_time: totalRetryTime, + times_connected: timesConnected, + }) { + let delayPerAttempt = 100 + + // If the server appears to be unavailable, slow down faster + if (error && error.code === 'ECONNREFUSED') { + delayPerAttempt *= 5 + } + + // Reconnect after delay + return Math.min(attempt * delayPerAttempt, maxReconnectDelay) + }, // Expand whatever other options and overrides were provided - ...options + ...options, }) // Handle connection errors to prevent killing the Node.js process @@ -115,10 +116,9 @@ export default function createClient (options = {}) { // Start pinging the server once per minute to prevent Redis connection // from closing when its idle `timeout` configuration value expires - pingInterval = setInterval( - () => { client.ping(() => {}) }, - 60 * 1000 - ) + pingInterval = setInterval(() => { + client.ping(() => {}) + }, 60 * 1000) }) client.on('end', () => { @@ -130,9 +130,15 @@ export default function createClient (options = {}) { const logPrefix = '[redis' + (name ? ` (${name})` : '') + ']' // Add event listeners for basic logging - client.on('connect', () => { console.log(logPrefix, 'Connection opened') }) - client.on('ready', () => { console.log(logPrefix, 'Ready to receive commands') }) - client.on('end', () => { console.log(logPrefix, 'Connection closed') }) + client.on('connect', () => { + console.log(logPrefix, 'Connection opened') + }) + client.on('ready', () => { + console.log(logPrefix, 'Ready to receive commands') + }) + client.on('end', () => { + console.log(logPrefix, 'Connection closed') + }) client.on( 'reconnecting', ({ @@ -141,7 +147,7 @@ export default function createClient (options = {}) { // The rest are unofficial properties but currently supported error, total_retry_time: totalRetryTime, - times_connected: timesConnected + times_connected: timesConnected, }) => { console.log( logPrefix, @@ -154,8 +160,12 @@ export default function createClient (options = {}) { ) } ) - client.on('warning', (msg) => { console.warn(logPrefix, 'Warning:', msg) }) - client.on('error', (error) => { console.error(logPrefix, formatRedisError(error)) }) + client.on('warning', (msg) => { + console.warn(logPrefix, 'Warning:', msg) + }) + client.on('error', (error) => { + console.error(logPrefix, formatRedisError(error)) + }) return client } diff --git a/lib/release-notes-utils.js b/lib/release-notes-utils.js index 0f8f1d7f9a73..67315f578b9d 100644 --- a/lib/release-notes-utils.js +++ b/lib/release-notes-utils.js @@ -5,20 +5,20 @@ import renderContent from './render-content/index.js' * Turn { [key]: { notes, intro, date } } * into [{ version, notes, intro, date }] */ -export function sortPatchKeys (release, version, options = {}) { +export function sortPatchKeys(release, version, options = {}) { const keys = Object.keys(release) - .map(key => { + .map((key) => { const keyWithDots = key.replace(/-/g, '.') return { version: `${version}.${keyWithDots}`, patchVersion: keyWithDots, downloadVersion: `${version}.${keyWithDots.replace(/\.rc\d*$/, '')}`, release: version, // TODO this naming :/ we are not currently using this value, but we may want to. - ...release[key] + ...release[key], } }) // Filter out any deprecated patches - .filter(key => !key.deprecated) + .filter((key) => !key.deprecated) // Versions with numbered releases like GHES 2.22, 3.0, etc. need additional semver sorting; // Versions with date releases need to be sorted by date. @@ -27,25 +27,24 @@ export function sortPatchKeys (release, version, options = {}) { : keys.sort((a, b) => new Date(b.date) - new Date(a.date)) } -export function semverSort (keys) { - return keys - .sort((a, b) => { - let aTemp = a.version - let bTemp = b.version +export function semverSort(keys) { + return keys.sort((a, b) => { + let aTemp = a.version + let bTemp = b.version - // There's an RC version here, so doing regular semver - // comparisons won't work. So, we'll convert the incompatible version - // strings to real semver strings, then compare. - const [aBase, aRc] = a.version.split('.rc') - if (aRc) aTemp = `${aBase}-rc.${aRc}` + // There's an RC version here, so doing regular semver + // comparisons won't work. So, we'll convert the incompatible version + // strings to real semver strings, then compare. + const [aBase, aRc] = a.version.split('.rc') + if (aRc) aTemp = `${aBase}-rc.${aRc}` - const [bBase, bRc] = b.version.split('.rc') - if (bRc) bTemp = `${bBase}-rc.${bRc}` + const [bBase, bRc] = b.version.split('.rc') + if (bRc) bTemp = `${bBase}-rc.${bRc}` - if (semver.gt(aTemp, bTemp)) return -1 - if (semver.lt(aTemp, bTemp)) return 1 - return 0 - }) + if (semver.gt(aTemp, bTemp)) return -1 + if (semver.lt(aTemp, bTemp)) return 1 + return 0 + }) } /** @@ -53,17 +52,22 @@ export function semverSort (keys) { * sections and rendering either `note` or `note.notes` in the * case of a sub-section */ -export async function renderPatchNotes (patch, ctx) { +export async function renderPatchNotes(patch, ctx) { // Run the notes through the markdown rendering pipeline for (const key in patch.sections) { - await Promise.all(patch.sections[key].map(async (noteOrHeading, index) => { - patch.sections[key][index] = typeof noteOrHeading === 'string' - ? await renderContent(noteOrHeading, ctx) - : { - ...noteOrHeading, - notes: await Promise.all(noteOrHeading.notes.map(note => renderContent(note, ctx))) - } - })) + await Promise.all( + patch.sections[key].map(async (noteOrHeading, index) => { + patch.sections[key][index] = + typeof noteOrHeading === 'string' + ? await renderContent(noteOrHeading, ctx) + : { + ...noteOrHeading, + notes: await Promise.all( + noteOrHeading.notes.map((note) => renderContent(note, ctx)) + ), + } + }) + ) } // Also render the patch's intro @@ -74,21 +78,21 @@ export async function renderPatchNotes (patch, ctx) { return patch } -export function sortReleasesByDate (releaseNotes) { +export function sortReleasesByDate(releaseNotes) { return Object.keys(releaseNotes) - .map(release => { + .map((release) => { const [year, month] = release.split('-') return { name: release, - date: new Date(`20${year}`, month - 1, '1') + date: new Date(`20${year}`, month - 1, '1'), } }) .sort((releaseEntry1, releaseEntry2) => releaseEntry2.date - releaseEntry1.date) - .map(releaseEntry => releaseEntry.name) + .map((releaseEntry) => releaseEntry.name) } -export function getAllReleases (releases, releaseNotesPerPlan, hasNumberedReleases) { - return releases.map(version => { +export function getAllReleases(releases, releaseNotesPerPlan, hasNumberedReleases) { + return releases.map((version) => { const release = releaseNotesPerPlan[version.replace(/\./g, '-')] if (!release) return { version } const patches = sortPatchKeys(release, version, { semverSort: hasNumberedReleases }) @@ -100,5 +104,5 @@ export default { sortReleasesByDate, sortPatchKeys, renderPatchNotes, - getAllReleases + getAllReleases, } diff --git a/lib/remove-deprecated-frontmatter.js b/lib/remove-deprecated-frontmatter.js index 14227351593c..5dfd00eb04d2 100644 --- a/lib/remove-deprecated-frontmatter.js +++ b/lib/remove-deprecated-frontmatter.js @@ -1,6 +1,11 @@ import { getEnterpriseServerNumber } from './patterns.js' -export default function removeDeprecatedFrontmatter (file, frontmatterVersions, versionToDeprecate, nextOldestVersion) { +export default function removeDeprecatedFrontmatter( + file, + frontmatterVersions, + versionToDeprecate, + nextOldestVersion +) { // skip files with no versions or Enterprise Server versions frontmatter if (!frontmatterVersions) return if (!frontmatterVersions['enterprise-server']) return @@ -16,7 +21,10 @@ export default function removeDeprecatedFrontmatter (file, frontmatterVersions, // if the release to deprecate is 2.13, and the FM is either '>=2.13' or '>=2.14', // we can safely change the FM to enterprise-server: '*' - if (enterpriseRange === `>=${releaseToDeprecate}` || enterpriseRange === `>=${nextOldestRelease}`) { + if ( + enterpriseRange === `>=${releaseToDeprecate}` || + enterpriseRange === `>=${nextOldestRelease}` + ) { frontmatterVersions['enterprise-server'] = '*' } } diff --git a/lib/remove-fpt-from-path.js b/lib/remove-fpt-from-path.js index 2ae739e5aa78..292b852674c7 100644 --- a/lib/remove-fpt-from-path.js +++ b/lib/remove-fpt-from-path.js @@ -4,6 +4,6 @@ import nonEnterpriseDefaultVersion from './non-enterprise-default-version.js' // This is a convenience function to remove free-pro-team@latest from all // **user-facing** aspects of the site (particularly URLs) while continuing to support // free-pro-team@latest as a version both in the codebase and in content/data files. -export default function removeFPTFromPath (path) { +export default function removeFPTFromPath(path) { return slash(path.replace(`/${nonEnterpriseDefaultVersion}`, '')) } diff --git a/lib/remove-liquid-statements.js b/lib/remove-liquid-statements.js index 2b9a7dea6a40..f845a477c87c 100644 --- a/lib/remove-liquid-statements.js +++ b/lib/remove-liquid-statements.js @@ -12,27 +12,87 @@ const firstIfRegex = new RegExp(ifStatement.source, 'm') const elseRegex = new RegExp(startTag.source + ' else ?' + endTag.source) const endRegex = new RegExp(startTag.source + ' endif ?' + endTag.source, 'g') const captureEndRegex = new RegExp('(' + endRegex.source + ')', 'g') -const dropSecondEndIf = new RegExp('(' + endRegex.source + contentRegex.source + ')' + endRegex.source, 'm') +const dropSecondEndIf = new RegExp( + '(' + endRegex.source + contentRegex.source + ')' + endRegex.source, + 'm' +) const inlineEndRegex = new RegExp(nonEmptyString.source + endRegex.source, 'gm') const inlineIfRegex = new RegExp(nonEmptyString.source + ifStatement.source, 'gm') // include one level of nesting -const liquidBlockRegex = new RegExp(ifRegex.source + '((' + ifRegex.source + contentRegex.source + endRegex.source + '|[\\s\\S])*?)' + endRegex.source, 'gm') -const elseBlockRegex = new RegExp(elseRegex.source + '(?:' + ifRegex.source + contentRegex.source + endRegex.source + '|[\\s\\S])*?' + endRegex.source, 'gm') +const liquidBlockRegex = new RegExp( + ifRegex.source + + '((' + + ifRegex.source + + contentRegex.source + + endRegex.source + + '|[\\s\\S])*?)' + + endRegex.source, + 'gm' +) +const elseBlockRegex = new RegExp( + elseRegex.source + + '(?:' + + ifRegex.source + + contentRegex.source + + endRegex.source + + '|[\\s\\S])*?' + + endRegex.source, + 'gm' +) // ----- END LIQUID PATTERNS ----- // -export default function removeLiquidStatements (content, versionToDeprecate, nextOldestVersion) { +export default function removeLiquidStatements(content, versionToDeprecate, nextOldestVersion) { // see tests/fixtures/remove-liquid-statements for examples const regexes = { // remove liquid only - greaterThanVersionToDeprecate: new RegExp(startTag.source + ` if ?(?: currentVersion == ('|")'?free-pro-team@latest'?('|") ?or)? currentVersion ver_gt ('|")${versionToDeprecate}('|") ` + endTag.source, 'gm'), - andGreaterThanVersionToDeprecate1: new RegExp('(' + startTag.source + ` if .*?) and currentVersion ver_gt (?:'|")${versionToDeprecate}(?:'|")( ` + endTag.source + ')', 'gm'), - andGreaterThanVersionToDeprecate2: new RegExp('(' + startTag.source + ` if )currentVersion ver_gt (?:'|")${versionToDeprecate}(?:'|") and (.*? ` + endTag.source + ')', 'gm'), - notEqualsVersionToDeprecate: new RegExp('(' + startTag.source + ` if)(?:( currentVersion .*?) or)? currentVersion != (?:'|")${versionToDeprecate}(?:'|")( ` + endTag.source + ')', 'gm'), - andNotEqualsVersionToDeprecate: new RegExp('(' + startTag.source + ` if .*?) and currentVersion != (?:'|")${versionToDeprecate}(?:'|")( ` + endTag.source + ')', 'gm'), + greaterThanVersionToDeprecate: new RegExp( + startTag.source + + ` if ?(?: currentVersion == ('|")'?free-pro-team@latest'?('|") ?or)? currentVersion ver_gt ('|")${versionToDeprecate}('|") ` + + endTag.source, + 'gm' + ), + andGreaterThanVersionToDeprecate1: new RegExp( + '(' + + startTag.source + + ` if .*?) and currentVersion ver_gt (?:'|")${versionToDeprecate}(?:'|")( ` + + endTag.source + + ')', + 'gm' + ), + andGreaterThanVersionToDeprecate2: new RegExp( + '(' + + startTag.source + + ` if )currentVersion ver_gt (?:'|")${versionToDeprecate}(?:'|") and (.*? ` + + endTag.source + + ')', + 'gm' + ), + notEqualsVersionToDeprecate: new RegExp( + '(' + + startTag.source + + ` if)(?:( currentVersion .*?) or)? currentVersion != (?:'|")${versionToDeprecate}(?:'|")( ` + + endTag.source + + ')', + 'gm' + ), + andNotEqualsVersionToDeprecate: new RegExp( + '(' + + startTag.source + + ` if .*?) and currentVersion != (?:'|")${versionToDeprecate}(?:'|")( ` + + endTag.source + + ')', + 'gm' + ), // remove liquid and content - lessThanNextOldestVersion: new RegExp(startTag.source + ` if .*?ver_lt ('|")${nextOldestVersion}('|") ?` + endTag.source, 'm'), - equalsVersionToDeprecate: new RegExp(startTag.source + ` if .*?== ('|")${versionToDeprecate}('|") ?` + endTag.source, 'm') + lessThanNextOldestVersion: new RegExp( + startTag.source + ` if .*?ver_lt ('|")${nextOldestVersion}('|") ?` + endTag.source, + 'm' + ), + equalsVersionToDeprecate: new RegExp( + startTag.source + ` if .*?== ('|")${versionToDeprecate}('|") ?` + endTag.source, + 'm' + ), } let allLiquidBlocks = getLiquidBlocks(content) @@ -55,7 +115,7 @@ export default function removeLiquidStatements (content, versionToDeprecate, nex return content } -function getLiquidBlocks (content) { +function getLiquidBlocks(content) { const liquidBlocks = content.match(liquidBlockRegex) if (!liquidBlocks) return @@ -65,10 +125,10 @@ function getLiquidBlocks (content) { return innerBlocks ? liquidBlocks.concat(innerBlocks) : liquidBlocks } -function getInnerBlocks (liquidBlocks) { +function getInnerBlocks(liquidBlocks) { const innerBlocks = [] - liquidBlocks.forEach(block => { + liquidBlocks.forEach((block) => { const ifStatements = block.match(ifRegex) if (!ifStatements) return @@ -81,7 +141,7 @@ function getInnerBlocks (liquidBlocks) { const innerIfStatements = newBlock.match(liquidBlockRegex) // add each inner if statement to array of blocks - innerIfStatements.forEach(innerIfStatement => { + innerIfStatements.forEach((innerIfStatement) => { innerBlocks.push(innerIfStatement) }) }) @@ -89,20 +149,24 @@ function getInnerBlocks (liquidBlocks) { return innerBlocks } -function removeLiquidOnly (content, allLiquidBlocks, regexes) { - const blocksToUpdate = allLiquidBlocks - .filter(block => { - // inner blocks are processed separately, so we only care about first if statements - const firstIf = block.match(firstIfRegex) - if (block.match(regexes.greaterThanVersionToDeprecate)) return firstIf[0] === block.match(regexes.greaterThanVersionToDeprecate)[0] - if (block.match(regexes.andGreaterThanVersionToDeprecate1)) return firstIf[0] === block.match(regexes.andGreaterThanVersionToDeprecate1)[0] - if (block.match(regexes.andGreaterThanVersionToDeprecate2)) return firstIf[0] === block.match(regexes.andGreaterThanVersionToDeprecate2)[0] - if (block.match(regexes.notEqualsVersionToDeprecate)) return firstIf[0] === block.match(regexes.notEqualsVersionToDeprecate)[0] - if (block.match(regexes.andNotEqualsVersionToDeprecate)) return firstIf[0] === block.match(regexes.andNotEqualsVersionToDeprecate)[0] - return false - }) +function removeLiquidOnly(content, allLiquidBlocks, regexes) { + const blocksToUpdate = allLiquidBlocks.filter((block) => { + // inner blocks are processed separately, so we only care about first if statements + const firstIf = block.match(firstIfRegex) + if (block.match(regexes.greaterThanVersionToDeprecate)) + return firstIf[0] === block.match(regexes.greaterThanVersionToDeprecate)[0] + if (block.match(regexes.andGreaterThanVersionToDeprecate1)) + return firstIf[0] === block.match(regexes.andGreaterThanVersionToDeprecate1)[0] + if (block.match(regexes.andGreaterThanVersionToDeprecate2)) + return firstIf[0] === block.match(regexes.andGreaterThanVersionToDeprecate2)[0] + if (block.match(regexes.notEqualsVersionToDeprecate)) + return firstIf[0] === block.match(regexes.notEqualsVersionToDeprecate)[0] + if (block.match(regexes.andNotEqualsVersionToDeprecate)) + return firstIf[0] === block.match(regexes.andNotEqualsVersionToDeprecate)[0] + return false + }) - blocksToUpdate.forEach(block => { + blocksToUpdate.forEach((block) => { let newBlock = block if (newBlock.match(regexes.andGreaterThanVersionToDeprecate1)) { @@ -118,7 +182,10 @@ function removeLiquidOnly (content, allLiquidBlocks, regexes) { } if (newBlock.match(regexes.andNotEqualsVersionToDeprecate)) { - newBlock = newBlock.replace(regexes.andNotEqualsVersionToDeprecate, matchAndNotEqualsStatement) + newBlock = newBlock.replace( + regexes.andNotEqualsVersionToDeprecate, + matchAndNotEqualsStatement + ) } // replace else block with endif @@ -128,7 +195,10 @@ function removeLiquidOnly (content, allLiquidBlocks, regexes) { newBlock = newBlock.replace(`${elseBlock}`, '{% endif %}') } - if (newBlock.match(regexes.greaterThanVersionToDeprecate) || newBlock.match(regexes.notEqualsVersionToDeprecate)) { + if ( + newBlock.match(regexes.greaterThanVersionToDeprecate) || + newBlock.match(regexes.notEqualsVersionToDeprecate) + ) { newBlock = newBlock.replace(liquidBlockRegex, matchGreaterThan) } @@ -156,37 +226,38 @@ function removeLiquidOnly (content, allLiquidBlocks, regexes) { return content } -function matchGreaterThan (match, p1) { +function matchGreaterThan(match, p1) { return p1 } -function matchAndStatement1 (match, p1, p2) { +function matchAndStatement1(match, p1, p2) { return p1 + p2 } -function matchAndStatement2 (match, p1, p2) { +function matchAndStatement2(match, p1, p2) { return p1 + p2 } -function matchNotEqualsStatement (match, p1, p2, p3) { +function matchNotEqualsStatement(match, p1, p2, p3) { if (!p2) return match return p1 + p2 + p3 } -function matchAndNotEqualsStatement (match, p1, p2) { +function matchAndNotEqualsStatement(match, p1, p2) { return p1 + p2 } -function removeLiquidAndContent (content, allLiquidBlocks, regexes) { - const blocksToRemove = allLiquidBlocks - .filter(block => { - const firstIf = block.match(firstIfRegex) - if (block.match(regexes.lessThanNextOldestVersion)) return firstIf[0] === block.match(regexes.lessThanNextOldestVersion)[0] - if (block.match(regexes.equalsVersionToDeprecate)) return firstIf[0] === block.match(regexes.equalsVersionToDeprecate)[0] - return false - }) +function removeLiquidAndContent(content, allLiquidBlocks, regexes) { + const blocksToRemove = allLiquidBlocks.filter((block) => { + const firstIf = block.match(firstIfRegex) + if (block.match(regexes.lessThanNextOldestVersion)) + return firstIf[0] === block.match(regexes.lessThanNextOldestVersion)[0] + if (block.match(regexes.equalsVersionToDeprecate)) + return firstIf[0] === block.match(regexes.equalsVersionToDeprecate)[0] + return false + }) - blocksToRemove.forEach(block => { + blocksToRemove.forEach((block) => { const elseBlock = getElseBlock(block) // remove else conditionals but leave content @@ -212,24 +283,24 @@ function removeLiquidAndContent (content, allLiquidBlocks, regexes) { return content } -function removeFirstNewline (block) { +function removeFirstNewline(block) { const lines = block.split(/\r?\n/) if (!first(lines).match(emptyString)) return block return drop(lines, 1).join('\n') } -function removeLastNewline (block) { +function removeLastNewline(block) { const lines = block.split(/\r?\n/) if (!last(lines).match(emptyString)) return block return dropRight(lines, 1).join('\n') } -function removeFirstAndLastNewlines (block) { +function removeFirstAndLastNewlines(block) { block = removeFirstNewline(block) return removeLastNewline(block) } -function getElseBlock (block) { +function getElseBlock(block) { const firstIf = block.match(firstIfRegex) const elseBlock = block.match(elseBlockRegex) @@ -260,6 +331,6 @@ function getElseBlock (block) { return block.match(elseBlockRegex) } -function removeExtraNewlines (content) { +function removeExtraNewlines(content) { return content.replace(/(\r?\n){3,4}/gm, '\n\n') } diff --git a/lib/render-content/create-processor.js b/lib/render-content/create-processor.js index f693b5d27a5f..26454f9fd7a6 100644 --- a/lib/render-content/create-processor.js +++ b/lib/render-content/create-processor.js @@ -16,7 +16,7 @@ import rewriteLegacyAssetPaths from './plugins/rewrite-legacy-asset-paths.js' import wrapInElement from './plugins/wrap-in-element.js' const graphql = xHighlightjsGraphql.definer -export default function createProcessor (context) { +export default function createProcessor(context) { return unified() .use(markdown) .use(remarkCodeExtra, { transform: codeHeader }) @@ -29,6 +29,9 @@ export default function createProcessor (context) { .use(raw) .use(rewriteLegacyAssetPaths, context) .use(wrapInElement, { selector: 'ol > li img', wrapper: 'span.procedural-image-wrapper' }) - .use(rewriteLocalLinks, { languageCode: context.currentLanguage, version: context.currentVersion }) + .use(rewriteLocalLinks, { + languageCode: context.currentLanguage, + version: context.currentVersion, + }) .use(html) } diff --git a/lib/render-content/index.js b/lib/render-content/index.js index 93c2c80465d6..93b47836ad65 100644 --- a/lib/render-content/index.js +++ b/lib/render-content/index.js @@ -33,7 +33,7 @@ for (const tag in tags) { * Like the `size` filter, but specifically for * getting the number of keys in an object */ -renderContent.liquid.registerFilter('obj_size', input => { +renderContent.liquid.registerFilter('obj_size', (input) => { if (!input) return 0 return Object.keys(input).length }) @@ -42,14 +42,14 @@ renderContent.liquid.registerFilter('obj_size', input => { * Returns the version number of a GHES version string * ex: enterprise-server@2.22 => 2.22 */ -renderContent.liquid.registerFilter('version_num', input => { +renderContent.liquid.registerFilter('version_num', (input) => { return input.split('@')[1] }) /** * Convert the input to a slug */ -renderContent.liquid.registerFilter('slugify', input => { +renderContent.liquid.registerFilter('slugify', (input) => { const slugger = new GithubSlugger() return slugger.slug(input) }) diff --git a/lib/render-content/liquid.js b/lib/render-content/liquid.js index d66075d9c169..1296c764eff8 100644 --- a/lib/render-content/liquid.js +++ b/lib/render-content/liquid.js @@ -4,14 +4,14 @@ import semver from 'semver' // GHE versions are not valid SemVer, but can be coerced... // https://github.com/npm/node-semver#coercion -function matchesVersionString (input) { +function matchesVersionString(input) { return typeof input === 'string' && input.match(/^(?:[a-z](?:[a-z-]*[a-z])?@)?\d+(?:\.\d+)*/) } // Support new version formats where version = plan@release // e.g., enterprise-server@2.21, where enterprise-server is the plan and 2.21 is the release // e.g., free-pro-team@latest, where free-pro-team is the plan and latest is the release // in addition to legacy formats where the version passed is simply 2.21 -function splitVersion (version) { +function splitVersion(version) { // The default plan when working with versions is "enterprise-server". // Default to that value here to support backward compatibility from before // plans were explicitly included. @@ -56,8 +56,8 @@ const engine = new Liquid({ if (leftPlan !== rightPlan) return false return semver.lt(semver.coerce(leftRelease), semver.coerce(rightRelease)) - } - } + }, + }, }) export default engine diff --git a/lib/render-content/plugins/code-header.js b/lib/render-content/plugins/code-header.js index 0dc0aae42c0a..4c984d789bc7 100644 --- a/lib/render-content/plugins/code-header.js +++ b/lib/render-content/plugins/code-header.js @@ -70,7 +70,7 @@ const LANGUAGE_MAP = { // Unofficial languages shellsession: 'Shell', - jsx: 'JSX' + jsx: 'JSX', } const COPY_REGEX = /\{:copy\}$/ @@ -78,7 +78,7 @@ const COPY_REGEX = /\{:copy\}$/ /** * Adds a bar above code blocks that shows the language and a copy button */ -export default function addCodeHeader (node) { +export default function addCodeHeader(node) { // Check if the language matches `lang{:copy}` const hasCopy = node.lang && COPY_REGEX.test(node.lang) @@ -109,30 +109,24 @@ export default function addCodeHeader (node) { 'p-2', 'text-small', 'rounded-top-1', - 'border' - ] + 'border', + ], }, [ h('span', language), h( 'button', { - class: [ - 'js-btn-copy', - 'btn', - 'btn-sm', - 'tooltipped', - 'tooltipped-nw' - ], + class: ['js-btn-copy', 'btn', 'btn-sm', 'tooltipped', 'tooltipped-nw'], 'data-clipboard-text': node.value, - 'aria-label': 'Copy code to clipboard' + 'aria-label': 'Copy code to clipboard', }, btnIcon - ) + ), ] ) return { - before: [header] + before: [header], } } diff --git a/lib/render-content/plugins/rewrite-legacy-asset-paths.js b/lib/render-content/plugins/rewrite-legacy-asset-paths.js index c308f9659e22..8cd9ab2d2375 100644 --- a/lib/render-content/plugins/rewrite-legacy-asset-paths.js +++ b/lib/render-content/plugins/rewrite-legacy-asset-paths.js @@ -3,12 +3,11 @@ import fs from 'fs' import { legacyAssetVersions } from '../../enterprise-server-releases.js' import allVersions from '../../all-versions.js' -const matcher = node => ( +const matcher = (node) => node.type === 'element' && node.tagName === 'img' && node.properties.src && node.properties.src.startsWith('/assets/images') -) // This module rewrites asset paths for specific Enterprise versions that // were migrated from AWS S3 image storage. Only images that were unique @@ -17,7 +16,7 @@ const matcher = node => ( // can remove this module. // Source example: /assets/images/foo.png // Rewritten: /assets/enterprise/2.20/assets/images/foo.png -export default function checkForLegacyAssetPaths ({ currentVersion, relativePath }) { +export default function checkForLegacyAssetPaths({ currentVersion, relativePath }) { // Bail if we don't have a relativePath in this context if (!relativePath) return // skip if this is the homepage @@ -32,11 +31,14 @@ export default function checkForLegacyAssetPaths ({ currentVersion, relativePath visit(tree, matcher, visitor) await Promise.all(promises) - function visitor (node) { + function visitor(node) { const legacyAssetPath = `/assets/images/enterprise/legacy-format/${enterpriseRelease}${node.properties.src}` - const p = fs.promises.access((`${process.cwd()}${legacyAssetPath}`), fs.constants.F_OK) + const p = fs.promises + .access(`${process.cwd()}${legacyAssetPath}`, fs.constants.F_OK) // rewrite the nodes src - .then(() => { node.properties.src = `${legacyAssetPath}` }) + .then(() => { + node.properties.src = `${legacyAssetPath}` + }) .catch(() => node) promises.push(p) } diff --git a/lib/render-content/plugins/rewrite-local-links.js b/lib/render-content/plugins/rewrite-local-links.js index 146bb03d3fbb..153de27af654 100644 --- a/lib/render-content/plugins/rewrite-local-links.js +++ b/lib/render-content/plugins/rewrite-local-links.js @@ -9,27 +9,25 @@ import allVersions from '../../all-versions.js' import removeFPTFromPath from '../../remove-fpt-from-path.js' import readJsonFile from '../../read-json-file.js' const supportedVersions = Object.keys(allVersions) -const supportedPlans = Object.values(allVersions).map(v => v.plan) +const supportedPlans = Object.values(allVersions).map((v) => v.plan) const externalRedirects = Object.keys(readJsonFile('./lib/redirects/external-sites.json')) - // Matches any tags with an href that starts with `/` -const matcher = node => ( +const matcher = (node) => node.type === 'element' && node.tagName === 'a' && node.properties && node.properties.href && node.properties.href.startsWith('/') -) // Content authors write links like `/some/article/path`, but they need to be // rewritten on the fly to match the current language and page version -export default function rewriteLocalLinks ({ languageCode, version }) { +export default function rewriteLocalLinks({ languageCode, version }) { // There's no languageCode or version passed, so nothing to do if (!languageCode || !version) return - return tree => { - visit(tree, matcher, node => { + return (tree) => { + visit(tree, matcher, (node) => { const newHref = getNewHref(node, languageCode, version) if (newHref) { node.properties.href = newHref @@ -38,7 +36,7 @@ export default function rewriteLocalLinks ({ languageCode, version }) { } } -function getNewHref (node, languageCode, version) { +function getNewHref(node, languageCode, version) { const { href } = node.properties // Exceptions to link rewriting if (href.startsWith('/assets')) return @@ -52,7 +50,11 @@ function getNewHref (node, languageCode, version) { // /enterprise-server/rest/reference/oauth-authorizations (this redirects to the latest version) // /enterprise-server@latest/rest/reference/oauth-authorizations (this redirects to the latest version) const firstLinkSegment = href.split('/')[1] - if ([...supportedPlans, ...supportedVersions, 'enterprise-server@latest'].some(v => firstLinkSegment.startsWith(v))) { + if ( + [...supportedPlans, ...supportedVersions, 'enterprise-server@latest'].some((v) => + firstLinkSegment.startsWith(v) + ) + ) { newHref = path.join('/', languageCode, href) } diff --git a/lib/render-content/plugins/use-english-headings.js b/lib/render-content/plugins/use-english-headings.js index dcb2362c894e..adaaa16d1bff 100644 --- a/lib/render-content/plugins/use-english-headings.js +++ b/lib/render-content/plugins/use-english-headings.js @@ -6,16 +6,13 @@ const Entities = xHtmlEntities.XmlEntities const slugger = new GithubSlugger() const entities = new Entities() -const matcher = node => ( - node.type === 'element' && - ['h2', 'h3', 'h4'].includes(node.tagName) -) +const matcher = (node) => node.type === 'element' && ['h2', 'h3', 'h4'].includes(node.tagName) // replace translated IDs and links in headings with English -export default function useEnglishHeadings ({ englishHeadings }) { +export default function useEnglishHeadings({ englishHeadings }) { if (!englishHeadings) return - return tree => { - visit(tree, matcher, node => { + return (tree) => { + visit(tree, matcher, (node) => { slugger.reset() // Get the plain text content of the heading node const text = toString(node) diff --git a/lib/render-content/plugins/wrap-in-element.js b/lib/render-content/plugins/wrap-in-element.js index 935c7890c620..14cebfd4ec70 100644 --- a/lib/render-content/plugins/wrap-in-element.js +++ b/lib/render-content/plugins/wrap-in-element.js @@ -5,7 +5,7 @@ import parseSelector from 'hast-util-parse-selector' /* * Attacher */ -export default options => { +export default (options) => { options = options || {} const selector = options.selector || options.select || 'body' const wrapper = options.wrapper || options.wrap @@ -13,7 +13,7 @@ export default options => { /* * Transformer */ - return tree => { + return (tree) => { if (typeof wrapper !== 'string') { throw new TypeError('Expected a `string` as wrapper') } diff --git a/lib/render-content/renderContent.js b/lib/render-content/renderContent.js index 86f71ae060db..91862f8336c6 100644 --- a/lib/render-content/renderContent.js +++ b/lib/render-content/renderContent.js @@ -10,10 +10,7 @@ const entities = new Entities() const endLine = '\r?\n' const blankLine = '\\s*?[\r\n]*' const startNextLine = '[^\\S\r\n]*?[-\\*] ?)\n?`, 'gm') // parse multiple times because some templates contain more templates. :] -async function renderContent ( - template = '', - context = {}, - options = {} -) { +async function renderContent(template = '', context = {}, options = {}) { try { // remove any newlines that precede html comments, then remove the comments if (template) { @@ -61,10 +54,7 @@ async function renderContent ( if (html.includes('')) html = removeNewlinesFromInlineTags(html) if (options.textOnly) { - html = cheerio - .load(html) - .text() - .trim() + html = cheerio.load(html).text().trim() } if (options.cheerioObject) { @@ -82,20 +72,14 @@ async function renderContent ( } } -function removeNewlinesFromInlineTags (html) { +function removeNewlinesFromInlineTags(html) { const $ = cheerio.load(html) // see https://cheerio.js.org/#html-htmlstring- $(inlineTags.join(',')) .parents('td') .get() - .map(tag => - $(tag).html( - $(tag) - .html() - .replace(inlineTagRegex, '$1') - ) - ) + .map((tag) => $(tag).html($(tag).html().replace(inlineTagRegex, '$1'))) return $('body').html() } diff --git a/lib/rest/index.js b/lib/rest/index.js index c614bf8f7e67..16a631b0a492 100644 --- a/lib/rest/index.js +++ b/lib/rest/index.js @@ -6,16 +6,15 @@ import allVersions from '../all-versions.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const schemasPath = path.join(__dirname, 'static/decorated') export const operations = {} -fs.readdirSync(schemasPath) - .forEach(filename => { - const key = filename.replace('.json', '') - const value = JSON.parse(fs.readFileSync(path.join(schemasPath, filename))) - operations[key] = value - }) +fs.readdirSync(schemasPath).forEach((filename) => { + const key = filename.replace('.json', '') + const value = JSON.parse(fs.readFileSync(path.join(schemasPath, filename))) + operations[key] = value +}) const allVersionKeys = Object.keys(allVersions) let allCategories = [] -allVersionKeys.forEach(currentVersion => { +allVersionKeys.forEach((currentVersion) => { // Translate the versions from the openapi to versions used in the docs const openApiVersion = allVersions[currentVersion].openApiVersionName @@ -29,11 +28,15 @@ allVersionKeys.forEach(currentVersion => { // so we can verify that the names of the markdown files // in content/rest/reference/*.md are congruous with the // set of REST resource names like activity, gists, repos, etc. - allCategories = allCategories.concat(chain(operations[currentVersion]).map('category').sort().uniq().value()) + allCategories = allCategories.concat( + chain(operations[currentVersion]).map('category').sort().uniq().value() + ) // Attach convenience properties to each operation that can't easily be created in Liquid - operations[currentVersion].forEach(operation => { - operation.hasRequiredPreviews = get(operation, 'x-github.previews', []).some(preview => preview.required) + operations[currentVersion].forEach((operation) => { + operation.hasRequiredPreviews = get(operation, 'x-github.previews', []).some( + (preview) => preview.required + ) }) }) @@ -44,7 +47,7 @@ const categories = [...new Set(allCategories)] // It's grouped by resource title to make rendering easier const operationsEnabledForGitHubApps = allVersionKeys.reduce((acc, currentVersion) => { acc[currentVersion] = chain(operations[currentVersion] || []) - .filter(operation => operation['x-github'].enabledForGitHubApps) + .filter((operation) => operation['x-github'].enabledForGitHubApps) .orderBy('category') .value() acc[currentVersion] = groupBy(acc[currentVersion], 'category') @@ -54,5 +57,5 @@ const operationsEnabledForGitHubApps = allVersionKeys.reduce((acc, currentVersio export default { categories, operations, - operationsEnabledForGitHubApps + operationsEnabledForGitHubApps, } diff --git a/lib/rewrite-local-links.js b/lib/rewrite-local-links.js index 3021f6cded83..dbc077ef1f61 100644 --- a/lib/rewrite-local-links.js +++ b/lib/rewrite-local-links.js @@ -9,12 +9,12 @@ import allVersions from './all-versions.js' import removeFPTFromPath from './remove-fpt-from-path.js' import readJsonFile from './read-json-file.js' const supportedVersions = Object.keys(allVersions) -const supportedPlans = Object.values(allVersions).map(v => v.plan) +const supportedPlans = Object.values(allVersions).map((v) => v.plan) const externalRedirects = readJsonFile('./lib/redirects/external-sites.json') // Content authors write links like `/some/article/path`, but they need to be // rewritten on the fly to match the current language and page version -export default function rewriteLocalLinks ($, version, languageCode) { +export default function rewriteLocalLinks($, version, languageCode) { assert(languageCode, 'languageCode is required') $('a[href^="/"]').each((i, el) => { @@ -22,7 +22,7 @@ export default function rewriteLocalLinks ($, version, languageCode) { }) } -function getNewHref (link, languageCode, version) { +function getNewHref(link, languageCode, version) { const href = link.attr('href') // Exceptions to link rewriting @@ -37,7 +37,9 @@ function getNewHref (link, languageCode, version) { // /enterprise-server/rest/reference/oauth-authorizations (this redirects to the latest version) // /enterprise-server@latest/rest/reference/oauth-authorizations (this redirects to the latest version) const firstLinkSegment = href.split('/')[1] - if ([...supportedPlans, ...supportedVersions, 'enterprise-server@latest'].includes(firstLinkSegment)) { + if ( + [...supportedPlans, ...supportedVersions, 'enterprise-server@latest'].includes(firstLinkSegment) + ) { newHref = path.join('/', languageCode, href) } diff --git a/lib/schema-event.js b/lib/schema-event.js index b72eeebbda65..39ab7ddeec94 100644 --- a/lib/schema-event.js +++ b/lib/schema-event.js @@ -3,70 +3,65 @@ import languages from './languages.js' const context = { type: 'object', additionalProperties: false, - required: [ - 'event_id', - 'user', - 'version', - 'created', - 'path' - ], + required: ['event_id', 'user', 'version', 'created', 'path'], properties: { // Required of all events event_id: { type: 'string', description: 'The unique identifier of the event.', - format: 'uuid' + format: 'uuid', }, user: { type: 'string', - description: 'The unique identifier of the current user performing the event. Please use randomly generated values or hashed values; we don\'t want to be able to look up in a database.', - format: 'uuid' + description: + "The unique identifier of the current user performing the event. Please use randomly generated values or hashed values; we don't want to be able to look up in a database.", + format: 'uuid', }, version: { type: 'string', description: 'The version of the event schema.', - pattern: '^\\d+(\\.\\d+)?(\\.\\d+)?$' // eslint-disable-line + pattern: '^\\d+(\\.\\d+)?(\\.\\d+)?$', // eslint-disable-line }, created: { type: 'string', format: 'date-time', - description: 'The time we created the event; please reference UTC.' + description: 'The time we created the event; please reference UTC.', }, page_event_id: { type: 'string', description: 'The id of the corresponding `page` event.', - format: 'uuid' + format: 'uuid', }, // Content information path: { type: 'string', description: 'The browser value of `location.pathname`.', - format: 'uri-reference' + format: 'uri-reference', }, hostname: { type: 'string', description: 'The browser value of `location.hostname.`', - format: 'uri-reference' + format: 'uri-reference', }, referrer: { type: 'string', description: 'The browser value of `document.referrer`.', - format: 'uri-reference' + format: 'uri-reference', }, search: { type: 'string', - description: 'The browser value of `location.search`.' + description: 'The browser value of `location.search`.', }, href: { type: 'string', description: 'The browser value of `location.href`.', - format: 'uri' + format: 'uri', }, site_language: { type: 'string', description: 'The language the user is viewing.', - enum: Object.keys(languages) + enum: Object.keys(languages), }, // Device information @@ -74,41 +69,41 @@ const context = { type: 'string', description: 'The type of operating system the user is working with.', enum: ['windows', 'mac', 'linux', 'ios', 'android', 'cros', 'other'], - default: 'other' + default: 'other', }, os_version: { type: 'string', - description: 'The version of the operating system the user is using.' + description: 'The version of the operating system the user is using.', }, browser: { type: 'string', description: 'The type of browser the user is browsing with.', enum: ['chrome', 'safari', 'firefox', 'edge', 'ie', 'other'], - default: 'other' + default: 'other', }, browser_version: { type: 'string', - description: 'The version of the browser the user is browsing with.' + description: 'The version of the browser the user is browsing with.', }, viewport_width: { type: 'number', description: 'The viewport width, not the overall device size.', - minimum: 1 + minimum: 1, }, viewport_height: { type: 'number', description: 'The viewport height, not the overall device height.', - minimum: 1 + minimum: 1, }, // Location information timezone: { type: 'number', - description: 'The timezone the user is in, as `new Date().getTimezoneOffset() / -60`.' + description: 'The timezone the user is in, as `new Date().getTimezoneOffset() / -60`.', }, user_language: { type: 'string', - description: 'The browser value of `navigator.language`.' + description: 'The browser value of `navigator.language`.', }, // Preference information @@ -119,299 +114,261 @@ const context = { application_preference: { type: 'string', enum: ['webui', 'cli', 'desktop', 'curl'], - description: 'The application selected by the user.' - } + description: 'The application selected by the user.', + }, /* color_mode_preference: { type: 'string', description: 'The color mode selected by the user.' } */ - } + }, } const pageSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context' - ], + required: ['type', 'context'], properties: { context, type: { type: 'string', - pattern: '^page$' - } - } + pattern: '^page$', + }, + }, } const exitSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context' - ], + required: ['type', 'context'], properties: { context, type: { type: 'string', - pattern: '^exit$' + pattern: '^exit$', }, exit_render_duration: { type: 'number', description: 'How long the server took to render the page content, in seconds.', - minimum: 0.001 + minimum: 0.001, }, exit_first_paint: { type: 'number', minimum: 0.001, - description: 'The duration until `first-contentful-paint`, in seconds. Informs CSS performance.' + description: + 'The duration until `first-contentful-paint`, in seconds. Informs CSS performance.', }, exit_dom_interactive: { type: 'number', minimum: 0.001, - description: 'The duration until `PerformanceNavigationTiming.domInteractive`, in seconds. Informs JavaScript loading performance.' + description: + 'The duration until `PerformanceNavigationTiming.domInteractive`, in seconds. Informs JavaScript loading performance.', }, exit_dom_complete: { type: 'number', minimum: 0.001, - description: 'The duration until `PerformanceNavigationTiming.domComplete`, in seconds. Informs JavaScript execution performance.' + description: + 'The duration until `PerformanceNavigationTiming.domComplete`, in seconds. Informs JavaScript execution performance.', }, exit_visit_duration: { type: 'number', minimum: 0.001, - description: 'The duration of exit.timestamp - page.timestamp, in seconds. Informs bounce rate.' + description: + 'The duration of exit.timestamp - page.timestamp, in seconds. Informs bounce rate.', }, exit_scroll_length: { type: 'number', minimum: 0, maximum: 1, - description: 'The percentage of how far the user scrolled on the page.' - } - } + description: 'The percentage of how far the user scrolled on the page.', + }, + }, } const linkSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context', - 'link_url' - ], + required: ['type', 'context', 'link_url'], properties: { context, type: { type: 'string', - pattern: '^link$' + pattern: '^link$', }, link_url: { type: 'string', format: 'uri', - description: 'The href of the anchor tag the user clicked, or the page or object they directed their browser to.' - } - } + description: + 'The href of the anchor tag the user clicked, or the page or object they directed their browser to.', + }, + }, } const searchSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context', - 'search_query' - ], + required: ['type', 'context', 'search_query'], properties: { context, type: { type: 'string', - pattern: '^search$' + pattern: '^search$', }, search_query: { type: 'string', - description: 'The actual text content of the query string the user sent to the service.' + description: 'The actual text content of the query string the user sent to the service.', }, search_context: { type: 'string', - description: 'Any additional search context, such as component searched.' - } - } + description: 'Any additional search context, such as component searched.', + }, + }, } const navigateSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context' - ], + required: ['type', 'context'], properties: { context, type: { type: 'string', - pattern: '^navigate$' + pattern: '^navigate$', }, navigate_label: { type: 'string', - description: 'An identifier for where the user is navigating.' - } - } + description: 'An identifier for where the user is navigating.', + }, + }, } const surveySchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context', - 'survey_vote' - ], + required: ['type', 'context', 'survey_vote'], properties: { context, type: { type: 'string', - pattern: '^survey$' + pattern: '^survey$', }, survey_vote: { type: 'boolean', - description: 'Whether the user found the page helpful.' + description: 'Whether the user found the page helpful.', }, survey_comment: { type: 'string', - description: 'Any textual comments the user wanted to provide.' + description: 'Any textual comments the user wanted to provide.', }, survey_email: { type: 'string', format: 'email', - description: 'The user\'s email address, if the user provided and consented.' - } - } + description: "The user's email address, if the user provided and consented.", + }, + }, } const experimentSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context', - 'experiment_name', - 'experiment_variation' - ], + required: ['type', 'context', 'experiment_name', 'experiment_variation'], properties: { context, type: { type: 'string', - pattern: '^experiment$' + pattern: '^experiment$', }, experiment_name: { type: 'string', - description: 'The test that this event is part of.' + description: 'The test that this event is part of.', }, experiment_variation: { type: 'string', enum: ['control', 'treatment'], - description: 'The variation this user we bucketed in, such as control or treatment.' + description: 'The variation this user we bucketed in, such as control or treatment.', }, experiment_success: { type: 'boolean', default: true, - description: 'Whether or not the user successfully performed the test goal.' - } - } + description: 'Whether or not the user successfully performed the test goal.', + }, + }, } const redirectSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context', - 'redirect_from', - 'redirect_to' - ], + required: ['type', 'context', 'redirect_from', 'redirect_to'], properties: { context, type: { type: 'string', - pattern: '^redirect$' + pattern: '^redirect$', }, redirect_from: { type: 'string', description: 'The requested href.', - format: 'uri-reference' + format: 'uri-reference', }, redirect_to: { type: 'string', description: 'The destination href of the redirect.', - format: 'uri-reference' - } - } + format: 'uri-reference', + }, + }, } const clipboardSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context', - 'clipboard_operation' - ], + required: ['type', 'context', 'clipboard_operation'], properties: { context, type: { type: 'string', - pattern: '^clipboard$' + pattern: '^clipboard$', }, clipboard_operation: { type: 'string', description: 'Which clipboard operation the user is performing.', - enum: ['copy', 'paste', 'cut'] - } - } + enum: ['copy', 'paste', 'cut'], + }, + }, } const printSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context' - ], + required: ['type', 'context'], properties: { context, type: { type: 'string', - pattern: '^print$' - } - } + pattern: '^print$', + }, + }, } const preferenceSchema = { type: 'object', additionalProperties: false, - required: [ - 'type', - 'context', - 'preference_name', - 'preference_value' - ], + required: ['type', 'context', 'preference_name', 'preference_value'], properties: { context, type: { type: 'string', - pattern: '^preference$' + pattern: '^preference$', }, preference_name: { type: 'string', enum: ['application'], // os, color_mode - description: 'The preference name, such as os, application, or color_mode' + description: 'The preference name, such as os, application, or color_mode', }, preference_value: { type: 'string', enum: ['webui', 'cli', 'desktop', 'curl'], - description: 'The application selected by the user.' - } - } + description: 'The application selected by the user.', + }, + }, } export default { @@ -426,6 +383,6 @@ export default { redirectSchema, clipboardSchema, printSchema, - preferenceSchema - ] + preferenceSchema, + ], } diff --git a/lib/search/algolia-search.js b/lib/search/algolia-search.js index 519eec5fef3a..ae5acd09556c 100644 --- a/lib/search/algolia-search.js +++ b/lib/search/algolia-search.js @@ -6,7 +6,7 @@ import { namePrefix } from './config.js' // This API key is public. There's also a private API key for writing to the Algolia API const searchClient = algoliasearch('ZI5KPY1HBE', '685df617246c3a10abba589b4599288f') -export default async function loadAlgoliaResults ({ version, language, query, filters, limit }) { +export default async function loadAlgoliaResults({ version, language, query, filters, limit }) { const indexName = `${namePrefix}-${version}-${language}` const index = searchClient.initIndex(indexName) @@ -17,15 +17,15 @@ export default async function loadAlgoliaResults ({ version, language, query, fi advancedSyntax: true, highlightPreTag: '', highlightPostTag: '', - filters + filters, }) - return hits.map(hit => ({ + return hits.map((hit) => ({ url: hit.objectID, breadcrumbs: get(hit, '_highlightResult.breadcrumbs.value'), heading: get(hit, '_highlightResult.heading.value'), title: get(hit, '_highlightResult.title.value'), content: get(hit, '_highlightResult.content.value'), - topics: hit.topics + topics: hit.topics, })) } diff --git a/lib/search/compress.js b/lib/search/compress.js index c4c7c4f5407b..895f3a2f097a 100644 --- a/lib/search/compress.js +++ b/lib/search/compress.js @@ -6,19 +6,19 @@ const brotliDecompress = promisify(zlib.brotliDecompress) const options = { params: { [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, - [zlib.constants.BROTLI_PARAM_QUALITY]: 6 - } + [zlib.constants.BROTLI_PARAM_QUALITY]: 6, + }, } -export async function compress (data) { +export async function compress(data) { return brotliCompress(data, options) } -export async function decompress (data) { +export async function decompress(data) { return brotliDecompress(data, options) } export default { compress, - decompress + decompress, } diff --git a/lib/search/config.js b/lib/search/config.js index 635d4e6a2c43..fb40bfd5cbe6 100644 --- a/lib/search/config.js +++ b/lib/search/config.js @@ -6,5 +6,5 @@ export default { // records must be truncated to avoid going over Algolia's 10K limit maxRecordLength, maxContentLength, - namePrefix + namePrefix, } diff --git a/lib/search/lunr-search.js b/lib/search/lunr-search.js index 86c136d171b0..25ba354ce046 100644 --- a/lib/search/lunr-search.js +++ b/lib/search/lunr-search.js @@ -23,13 +23,14 @@ const LUNR_DIR = './indexes' const lunrIndexes = new Map() const lunrRecords = new Map() -export default async function loadLunrResults ({ version, language, query, limit }) { +export default async function loadLunrResults({ version, language, query, limit }) { const indexName = `${namePrefix}-${version}-${language}` if (!lunrIndexes.has(indexName) || !lunrRecords.has(indexName)) { lunrIndexes.set(indexName, await loadLunrIndex(indexName)) lunrRecords.set(indexName, await loadLunrRecords(indexName)) } - const results = lunrIndexes.get(indexName) + const results = lunrIndexes + .get(indexName) .search(query) .slice(0, limit) .map((result) => { @@ -41,37 +42,32 @@ export default async function loadLunrResults ({ version, language, query, limit title: field(result, record, 'title'), content: field(result, record, 'content'), // don't highlight the topics array - topics: record.topics + topics: record.topics, } }) return results } -async function loadLunrIndex (indexName) { +async function loadLunrIndex(indexName) { const filePath = path.posix.join(__dirname, LUNR_DIR, `${indexName}.json.br`) // Do not set to 'utf8' on file reads - return readFileAsync(filePath) - .then(decompress) - .then(JSON.parse) - .then(lunr.Index.load) + return readFileAsync(filePath).then(decompress).then(JSON.parse).then(lunr.Index.load) } -async function loadLunrRecords (indexName) { +async function loadLunrRecords(indexName) { const filePath = path.posix.join(__dirname, LUNR_DIR, `${indexName}-records.json.br`) // Do not set to 'utf8' on file reads - return readFileAsync(filePath) - .then(decompress) - .then(JSON.parse) + return readFileAsync(filePath).then(decompress).then(JSON.parse) } // Highlight a match within an attribute field -function field (result, record, name) { +function field(result, record, name) { const text = record[name] if (!text) return text // First, get a list of all the positions of the matching tokens const positions = Object.values(result.matchData.metadata) - .map(fields => get(fields, [name, 'position'])) + .map((fields) => get(fields, [name, 'position'])) .filter(Boolean) .flat() .sort((a, b) => a[0] - b[0]) @@ -86,13 +82,13 @@ function field (result, record, name) { .map(([prev, start, end], i) => [ text.slice(prev, start), mark(text.slice(start, end)), - i === positions.length - 1 && text.slice(end) + i === positions.length - 1 && text.slice(end), ]) .flat() .filter(Boolean) .join('') } -function mark (text) { +function mark(text) { return `${text}` } diff --git a/lib/search/versions.js b/lib/search/versions.js index be04a0de7752..5573cdfda562 100644 --- a/lib/search/versions.js +++ b/lib/search/versions.js @@ -1,14 +1,13 @@ import allVersions from '../all-versions.js' export default Object.fromEntries( - Object.entries(allVersions) - .map(([versionStr, versionObject]) => [ - versionStr, - // if GHES, resolves to the release number like 2.21, 2.22, etc. - // if FPT, resolves to 'dotcom' - // if GHAE, resolves to 'ghae' - versionObject.plan === 'enterprise-server' - ? versionObject.currentRelease - : versionObject.miscBaseName - ]) + Object.entries(allVersions).map(([versionStr, versionObject]) => [ + versionStr, + // if GHES, resolves to the release number like 2.21, 2.22, etc. + // if FPT, resolves to 'dotcom' + // if GHAE, resolves to 'ghae' + versionObject.plan === 'enterprise-server' + ? versionObject.currentRelease + : versionObject.miscBaseName, + ]) ) diff --git a/lib/site-data.js b/lib/site-data.js index 2b09cde50c24..e48dc9c55bb3 100755 --- a/lib/site-data.js +++ b/lib/site-data.js @@ -5,20 +5,19 @@ import languages from './languages.js' import dataDirectory from './data-directory.js' import encodeBracketedParentheses from './encode-bracketed-parentheses.js' -const loadSiteDataFromDir = dir => ({ +const loadSiteDataFromDir = (dir) => ({ site: { data: dataDirectory(path.join(dir, 'data'), { - preprocess: dataString => - encodeBracketedParentheses(dataString.trimEnd()), - ignorePatterns: [/README\.md$/] - }) - } + preprocess: (dataString) => encodeBracketedParentheses(dataString.trimEnd()), + ignorePatterns: [/README\.md$/], + }), + }, }) -export default function loadSiteData () { +export default function loadSiteData() { // load english site data const siteData = { - en: loadSiteDataFromDir(languages.en.dir) + en: loadSiteDataFromDir(languages.en.dir), } // load and add other language data to siteData where keys match english keys, @@ -28,11 +27,7 @@ export default function loadSiteData () { if (language.code === 'en') continue const data = loadSiteDataFromDir(language.dir) for (const key of englishKeys) { - set( - siteData, - `${language.code}.${key}`, - get(data, key) || get(siteData.en, key) - ) + set(siteData, `${language.code}.${key}`, get(data, key) || get(siteData.en, key)) } } @@ -44,8 +39,8 @@ export default function loadSiteData () { // Sort glossary by language-specific function if (language.code !== 'en') { - siteData[language.code].site.data.glossaries.external.sort( - (a, b) => a.term.localeCompare(b.term, language.code) + siteData[language.code].site.data.glossaries.external.sort((a, b) => + a.term.localeCompare(b.term, language.code) ) } } diff --git a/lib/statsd.js b/lib/statsd.js index ba1b9e1fbfd1..136692e7db99 100644 --- a/lib/statsd.js +++ b/lib/statsd.js @@ -10,6 +10,6 @@ export default new StatsD({ mock, globalTags: { app: 'docs', - heroku_app: process.env.HEROKU_APP_NAME - } + heroku_app: process.env.HEROKU_APP_NAME, + }, }) diff --git a/lib/use-english-headings.js b/lib/use-english-headings.js index 1152d58f7707..420f7f19ec76 100644 --- a/lib/use-english-headings.js +++ b/lib/use-english-headings.js @@ -5,7 +5,7 @@ const Entities = xHtmlEntities.XmlEntities const entities = new Entities() // replace translated IDs and links in headings with English -export default function useEnglishHeadings ($, englishHeadings) { +export default function useEnglishHeadings($, englishHeadings) { $('h2, h3, h4').each((i, el) => { slugger.reset() diff --git a/lib/version-satisfies-range.js b/lib/version-satisfies-range.js index a0556b73db0e..c7f9825a97e2 100644 --- a/lib/version-satisfies-range.js +++ b/lib/version-satisfies-range.js @@ -2,7 +2,7 @@ import semver from 'semver' // Where "version" is an Enterprise Server release number, like `3.1`, // and "range" is a semver range operator with another number, like `<=3.2`. -export default function versionSatisfiesRange (version, range) { +export default function versionSatisfiesRange(version, range) { // workaround for Enterprise Server 11.10.340 because we can't use semver to // compare it to 2.x like we can with 2.0+ if (version === '11.10.340') return range.startsWith('<') diff --git a/lib/warm-server.js b/lib/warm-server.js index 5767a4e303c0..b58c9d0a348d 100644 --- a/lib/warm-server.js +++ b/lib/warm-server.js @@ -11,13 +11,13 @@ const dog = { loadPages: statsd.asyncTimer(loadPages, 'load_pages'), loadPageMap: statsd.asyncTimer(loadPageMap, 'load_page_map'), loadRedirects: statsd.asyncTimer(loadRedirects, 'load_redirects'), - loadSiteData: statsd.timer(loadSiteData, 'load_site_data') + loadSiteData: statsd.timer(loadSiteData, 'load_site_data'), } // For multiple-triggered Promise sharing let promisedWarmServer -async function warmServer () { +async function warmServer() { const startTime = Date.now() if (process.env.NODE_ENV !== 'test') { @@ -40,7 +40,7 @@ async function warmServer () { site, redirects, unversionedTree, - siteTree + siteTree, } } @@ -50,7 +50,7 @@ dog.warmServer = statsd.asyncTimer(warmServer, 'warm_server') // We only want statistics if the priming needs to occur, so let's wrap the // real method and return early [without statistics] whenever possible -export default async function warmServerWrapper () { +export default async function warmServerWrapper() { // Handle receiving multiple calls to this method from multiple page requests // by holding the in-progress Promise and returning it instead of allowing // the server to actually load all of the files multiple times. diff --git a/lib/webhooks/index.js b/lib/webhooks/index.js index b70c371bab00..f5a24bdc9679 100644 --- a/lib/webhooks/index.js +++ b/lib/webhooks/index.js @@ -12,23 +12,30 @@ const payloads = {} // array of versions based on subdirectory names: lib/webhooks/static/ const versions = fs.readdirSync(staticDir) -versions.forEach(version => { +versions.forEach((version) => { const payloadsPerVersion = {} const versionSubdir = path.join(staticDir, version) - walk(versionSubdir, { includeBasePath: true }) - .forEach(payloadFile => { - // payload file: /path/to/check_run.completed.payload.json - // payload path: check_run.completed - const payloadPath = path.basename(payloadFile).replace('.payload.json', '') - set(payloadsPerVersion, payloadPath, formatAsJsonCodeBlock(JSON.parse(fs.readFileSync(payloadFile)))) - }) + walk(versionSubdir, { includeBasePath: true }).forEach((payloadFile) => { + // payload file: /path/to/check_run.completed.payload.json + // payload path: check_run.completed + const payloadPath = path.basename(payloadFile).replace('.payload.json', '') + set( + payloadsPerVersion, + payloadPath, + formatAsJsonCodeBlock(JSON.parse(fs.readFileSync(payloadFile))) + ) + }) payloads[version] = payloadsPerVersion }) -function formatAsJsonCodeBlock (payloadObj) { - return '
\n\n```json\n' + JSON.stringify(payloadObj, null, 2) + '\n```\n\n
' +function formatAsJsonCodeBlock(payloadObj) { + return ( + '
\n\n```json\n' + + JSON.stringify(payloadObj, null, 2) + + '\n```\n\n
' + ) } export default payloads diff --git a/middleware/abort.js b/middleware/abort.js index 4603730ee721..9df940993abb 100644 --- a/middleware/abort.js +++ b/middleware/abort.js @@ -1,4 +1,4 @@ -export default function abort (req, res, next) { +export default function abort(req, res, next) { // If the client aborts the connection, send an error req.once('aborted', () => { // ignore aborts from next, usually has to do with webpack-hmr diff --git a/middleware/archived-enterprise-versions-assets.js b/middleware/archived-enterprise-versions-assets.js index 5986390a8bf5..8570aa575008 100644 --- a/middleware/archived-enterprise-versions-assets.js +++ b/middleware/archived-enterprise-versions-assets.js @@ -11,7 +11,7 @@ const ONE_DAY = 24 * 60 * 60 // 1 day in seconds // // See also ./archived-enterprise-versions.js for non-CSS/JS paths -export default async function archivedEnterpriseVersionsAssets (req, res, next) { +export default async function archivedEnterpriseVersionsAssets(req, res, next) { const { isArchived, requestedVersion } = isArchivedVersion(req) if (!isArchived) return next() @@ -22,7 +22,9 @@ export default async function archivedEnterpriseVersionsAssets (req, res, next) const proxyPath = path.join('/', requestedVersion, assetPath) try { - const r = await got(`https://github.github.com/help-docs-archived-enterprise-versions${proxyPath}`) + const r = await got( + `https://github.github.com/help-docs-archived-enterprise-versions${proxyPath}` + ) res.set('accept-ranges', 'bytes') res.set('content-type', r.headers['content-type']) res.set('content-length', r.headers['content-length']) diff --git a/middleware/archived-enterprise-versions.js b/middleware/archived-enterprise-versions.js index 1c53cc54f0b3..75426f768fdf 100644 --- a/middleware/archived-enterprise-versions.js +++ b/middleware/archived-enterprise-versions.js @@ -1,18 +1,25 @@ import path from 'path' import slash from 'slash' -import { firstVersionDeprecatedOnNewSite, lastVersionWithoutArchivedRedirectsFile } from '../lib/enterprise-server-releases.js' +import { + firstVersionDeprecatedOnNewSite, + lastVersionWithoutArchivedRedirectsFile, +} from '../lib/enterprise-server-releases.js' import patterns from '../lib/patterns.js' import versionSatisfiesRange from '../lib/version-satisfies-range.js' import isArchivedVersion from '../lib/is-archived-version.js' import got from 'got' import readJsonFile from '../lib/read-json-file.js' -const archivedRedirects = readJsonFile('./lib/redirects/static/archived-redirects-from-213-to-217.json') -const archivedFrontmatterFallbacks = readJsonFile('./lib/redirects/static/archived-frontmatter-fallbacks.json') +const archivedRedirects = readJsonFile( + './lib/redirects/static/archived-redirects-from-213-to-217.json' +) +const archivedFrontmatterFallbacks = readJsonFile( + './lib/redirects/static/archived-frontmatter-fallbacks.json' +) // This module handles requests for deprecated GitHub Enterprise versions // by routing them to static content in help-docs-archived-enterprise-versions -export default async function archivedEnterpriseVersions (req, res, next) { +export default async function archivedEnterpriseVersions(req, res, next) { const { isArchived, requestedVersion } = isArchivedVersion(req) if (!isArchived) return next() @@ -21,14 +28,19 @@ export default async function archivedEnterpriseVersions (req, res, next) { // redirect language-prefixed URLs like /en/enterprise/2.10 -> /enterprise/2.10 // (this only applies to versions <2.13) - if (req.path.startsWith('/en/') && versionSatisfiesRange(requestedVersion, `<${firstVersionDeprecatedOnNewSite}`)) { + if ( + req.path.startsWith('/en/') && + versionSatisfiesRange(requestedVersion, `<${firstVersionDeprecatedOnNewSite}`) + ) { return res.redirect(301, req.baseUrl + req.path.replace(/^\/en/, '')) } // find redirects for versions between 2.13 and 2.17 // starting with 2.18, we updated the archival script to create a redirects.json file - if (versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) && - versionSatisfiesRange(requestedVersion, `<=${lastVersionWithoutArchivedRedirectsFile}`)) { + if ( + versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) && + versionSatisfiesRange(requestedVersion, `<=${lastVersionWithoutArchivedRedirectsFile}`) + ) { const redirect = archivedRedirects[req.path] if (redirect && redirect !== req.path) { return res.redirect(301, redirect) @@ -79,7 +91,7 @@ export default async function archivedEnterpriseVersions (req, res, next) { // paths are slightly different depending on the version // for >=2.13: /2.13/en/enterprise/2.13/user/articles/viewing-contributions-on-your-profile // for <2.13: /2.12/user/articles/viewing-contributions-on-your-profile -function getProxyPath (reqPath, requestedVersion) { +function getProxyPath(reqPath, requestedVersion) { const proxyPath = versionSatisfiesRange(requestedVersion, `>=${firstVersionDeprecatedOnNewSite}`) ? slash(path.join('/', requestedVersion, reqPath)) : reqPath.replace(/^\/enterprise/, '') @@ -89,9 +101,11 @@ function getProxyPath (reqPath, requestedVersion) { // from 2.13 to 2.17, we lost access to frontmatter redirects during the archival process // this workaround finds potentially relevant frontmatter redirects in currently supported pages -function getFallbackRedirects (req, requestedVersion) { +function getFallbackRedirects(req, requestedVersion) { if (versionSatisfiesRange(requestedVersion, `<${firstVersionDeprecatedOnNewSite}`)) return if (versionSatisfiesRange(requestedVersion, `>${lastVersionWithoutArchivedRedirectsFile}`)) return - return archivedFrontmatterFallbacks.find(arrayOfFallbacks => arrayOfFallbacks.includes(req.path)) + return archivedFrontmatterFallbacks.find((arrayOfFallbacks) => + arrayOfFallbacks.includes(req.path) + ) } diff --git a/middleware/block-robots.js b/middleware/block-robots.js index a42530d28699..d308514e80e7 100644 --- a/middleware/block-robots.js +++ b/middleware/block-robots.js @@ -5,32 +5,29 @@ import { deprecated } from '../lib/enterprise-server-releases.js' const pathRegExps = [ // Disallow indexing of WIP localized content ...Object.values(languages) - .filter(language => language.wip) - .map(language => new RegExp(`^/${language.code}(/.*)?$`, 'i')), + .filter((language) => language.wip) + .map((language) => new RegExp(`^/${language.code}(/.*)?$`, 'i')), // Disallow indexing of WIP products ...Object.values(productMap) - .filter(product => product.wip || product.hidden) - .map(product => [ + .filter((product) => product.wip || product.hidden) + .map((product) => [ new RegExp(`^/.*?${product.href}`, 'i'), - ...product.versions.map( - version => new RegExp(`^/.*?${version}/${product.id}`, 'i') - ) + ...product.versions.map((version) => new RegExp(`^/.*?${version}/${product.id}`, 'i')), ]), // Disallow indexing of deprecated enterprise versions - ...deprecated - .map(version => [ - new RegExp(`^/.*?/enterprise-server@${version}/.*?`, 'i'), - new RegExp(`^/.*?/enterprise/${version}/.*?`, 'i') - ]) + ...deprecated.map((version) => [ + new RegExp(`^/.*?/enterprise-server@${version}/.*?`, 'i'), + new RegExp(`^/.*?/enterprise/${version}/.*?`, 'i'), + ]), ].flat() -export function blockIndex (path) { - return pathRegExps.some(pathRe => pathRe.test(path)) +export function blockIndex(path) { + return pathRegExps.some((pathRe) => pathRe.test(path)) } -const middleware = function blockRobots (req, res, next) { +const middleware = function blockRobots(req, res, next) { if (blockIndex(req.path)) res.set('x-robots-tag', 'noindex') return next() } diff --git a/middleware/catch-bad-accept-language.js b/middleware/catch-bad-accept-language.js index a9b48a41bc52..4c0fc0b36a38 100644 --- a/middleware/catch-bad-accept-language.js +++ b/middleware/catch-bad-accept-language.js @@ -3,7 +3,7 @@ import accept from '@hapi/accept' // Next.JS uses the @hapi/accept package to parse and detect languages. If the accept-language header is malformed // it throws an error from within Next.JS, which results in a 500 response. This ends up being noisy because we // track 500s. To counteract this, we'll try to catch the error first and make sure it doesn't happen -export default function catchBadAcceptLanguage (req, res, next) { +export default function catchBadAcceptLanguage(req, res, next) { try { accept.language(req.headers['accept-language']) } catch (e) { diff --git a/middleware/categories-for-support.js b/middleware/categories-for-support.js index e83b40cc08c3..08a9756b11a3 100644 --- a/middleware/categories-for-support.js +++ b/middleware/categories-for-support.js @@ -3,36 +3,42 @@ const renderOpts = { textOnly: true, encodeEntities: true } // This middleware exposes a list of all categories and child articles at /categories.json. // GitHub Support uses this for internal ZenDesk search functionality. -export default async function categoriesForSupport (req, res, next) { +export default async function categoriesForSupport(req, res, next) { const englishSiteTree = req.context.siteTree.en const allCategories = [] - await Promise.all(Object.keys(englishSiteTree).map(async (version) => { - await Promise.all(englishSiteTree[version].childPages.map(async (productPage) => { - if (productPage.page.relativePath.startsWith('early-access')) return - if (!productPage.childPages) return - - await Promise.all(productPage.childPages.map(async (categoryPage) => { - // We can't get the rendered titles from middleware/render-tree-titles - // here because that middleware only runs on the current version, and this - // middleware processes all versions. - const name = categoryPage.page.title.includes('{') - ? await categoryPage.page.renderProp('title', req.context, renderOpts) - : categoryPage.page.title - - allCategories.push({ - name, - published_articles: await findArticlesPerCategory(categoryPage, [], req.context) + await Promise.all( + Object.keys(englishSiteTree).map(async (version) => { + await Promise.all( + englishSiteTree[version].childPages.map(async (productPage) => { + if (productPage.page.relativePath.startsWith('early-access')) return + if (!productPage.childPages) return + + await Promise.all( + productPage.childPages.map(async (categoryPage) => { + // We can't get the rendered titles from middleware/render-tree-titles + // here because that middleware only runs on the current version, and this + // middleware processes all versions. + const name = categoryPage.page.title.includes('{') + ? await categoryPage.page.renderProp('title', req.context, renderOpts) + : categoryPage.page.title + + allCategories.push({ + name, + published_articles: await findArticlesPerCategory(categoryPage, [], req.context), + }) + }) + ) }) - })) - })) - })) + ) + }) + ) return res.json(allCategories) } -async function findArticlesPerCategory (currentPage, articlesArray, context) { +async function findArticlesPerCategory(currentPage, articlesArray, context) { if (currentPage.page.documentType === 'article') { const title = currentPage.page.title.includes('{') ? await currentPage.page.renderProp('title', context, renderOpts) @@ -40,16 +46,18 @@ async function findArticlesPerCategory (currentPage, articlesArray, context) { articlesArray.push({ title, - slug: path.basename(currentPage.href) + slug: path.basename(currentPage.href), }) } if (!currentPage.childPages) return articlesArray // Run recursively to find any articles deeper in the tree. - await Promise.all(currentPage.childPages.map(async (childPage) => { - await findArticlesPerCategory(childPage, articlesArray, context) - })) + await Promise.all( + currentPage.childPages.map(async (childPage) => { + await findArticlesPerCategory(childPage, articlesArray, context) + }) + ) return articlesArray } diff --git a/middleware/connect-datadog.js b/middleware/connect-datadog.js index 285619d081dd..6b89323c6cde 100644 --- a/middleware/connect-datadog.js +++ b/middleware/connect-datadog.js @@ -12,6 +12,6 @@ export default (req, res, next) => { dogstatsd: statsd, method: true, // Track HTTP methods (GET, POST, etc) response_code: true, // Track response codes - tags + tags, })(req, res, next) } diff --git a/middleware/context.js b/middleware/context.js index 3fd19da8b6eb..72d3eea79469 100644 --- a/middleware/context.js +++ b/middleware/context.js @@ -9,18 +9,20 @@ import readJsonFile from '../lib/read-json-file.js' import builtAssets from '../lib/built-asset-urls.js' import searchVersions from '../lib/search/versions.js' import nonEnterpriseDefaultVersion from '../lib/non-enterprise-default-version.js' -const activeProducts = Object.values(productMap).filter(product => !product.wip && !product.hidden) +const activeProducts = Object.values(productMap).filter( + (product) => !product.wip && !product.hidden +) const { getVersionStringFromPath, getProductStringFromPath, getCategoryStringFromPath, - getPathWithoutLanguage + getPathWithoutLanguage, } = xPathUtils const featureFlags = Object.keys(readJsonFile('./feature-flags.json')) // Supply all route handlers with a baseline `req.context` object // Note that additional middleware in middleware/index.js adds to this context object -export default async function contextualize (req, res, next) { +export default async function contextualize(req, res, next) { // Ensure that we load some data only once on first request const { site, redirects, siteTree, pages: pageMap } = await warmServer() @@ -28,7 +30,7 @@ export default async function contextualize (req, res, next) { // make feature flag environment variables accessible in layouts req.context.process = { env: {} } - featureFlags.forEach(featureFlagName => { + featureFlags.forEach((featureFlagName) => { req.context[featureFlagName] = process.env[featureFlagName] }) @@ -48,7 +50,9 @@ export default async function contextualize (req, res, next) { req.context.languages = languages req.context.productNames = productNames req.context.enterpriseServerReleases = enterpriseServerReleases - req.context.enterpriseServerVersions = Object.keys(allVersions).filter(version => version.startsWith('enterprise-server@')) + req.context.enterpriseServerVersions = Object.keys(allVersions).filter((version) => + version.startsWith('enterprise-server@') + ) req.context.redirects = redirects req.context.site = site[req.language].site req.context.siteTree = siteTree @@ -63,10 +67,10 @@ export default async function contextualize (req, res, next) { searchOptions: { languages: Object.keys(languages), versions: searchVersions, - nonEnterpriseDefaultVersion + nonEnterpriseDefaultVersion, }, // `|| undefined` won't show at all for production - airgap: Boolean(process.env.AIRGAP || req.cookies.AIRGAP) || undefined + airgap: Boolean(process.env.AIRGAP || req.cookies.AIRGAP) || undefined, }) if (process.env.AIRGAP || req.cookies.AIRGAP) req.context.AIRGAP = true req.context.searchVersions = searchVersions diff --git a/middleware/contextualizers/breadcrumbs.js b/middleware/contextualizers/breadcrumbs.js index fecc7eba5dd7..1b1870564be2 100644 --- a/middleware/contextualizers/breadcrumbs.js +++ b/middleware/contextualizers/breadcrumbs.js @@ -1,4 +1,4 @@ -export default async function breadcrumbs (req, res, next) { +export default async function breadcrumbs(req, res, next) { if (!req.context.page) return next() if (req.context.page.hidden) return next() @@ -9,7 +9,8 @@ export default async function breadcrumbs (req, res, next) { return next() } - const currentSiteTree = req.context.siteTree[req.context.currentLanguage][req.context.currentVersion] + const currentSiteTree = + req.context.siteTree[req.context.currentLanguage][req.context.currentVersion] await createBreadcrumb( // Array of child pages on the root, i.e., the product level. @@ -20,20 +21,22 @@ export default async function breadcrumbs (req, res, next) { return next() } -async function createBreadcrumb (pageArray, context) { +async function createBreadcrumb(pageArray, context) { // Find each page in the siteTree's array of child pages that starts with the requested path. - let childPage = pageArray.find(page => context.currentPath.startsWith(page.href)) + let childPage = pageArray.find((page) => context.currentPath.startsWith(page.href)) // Fall back to English if needed if (!childPage) { - childPage = pageArray.find(page => context.currentPath.startsWith(page.href.replace(`/${context.currentLanguage}`, '/en'))) + childPage = pageArray.find((page) => + context.currentPath.startsWith(page.href.replace(`/${context.currentLanguage}`, '/en')) + ) if (!childPage) return } context.breadcrumbs.push({ documentType: childPage.page.documentType, href: childPage.href, - title: childPage.renderedShortTitle || childPage.renderedFullTitle + title: childPage.renderedShortTitle || childPage.renderedFullTitle, }) // Recursively loop through the siteTree and create each breadcrumb, until we reach the diff --git a/middleware/contextualizers/current-product-tree.js b/middleware/contextualizers/current-product-tree.js index f4fe54831b42..3bef4d0d55e2 100644 --- a/middleware/contextualizers/current-product-tree.js +++ b/middleware/contextualizers/current-product-tree.js @@ -3,16 +3,28 @@ import findPageInSiteTree from '../../lib/find-page-in-site-tree.js' import removeFPTFromPath from '../../lib/remove-fpt-from-path.js' // This module adds currentProductTree to the context object for use in layouts. -export default function currentProductTree (req, res, next) { +export default function currentProductTree(req, res, next) { if (!req.context.page) return next() if (req.context.page.documentType === 'homepage') return next() // We need this so we can fall back to English if localized pages are out of sync. req.context.currentEnglishTree = req.context.siteTree.en[req.context.currentVersion] - const currentRootTree = req.context.siteTree[req.context.currentLanguage][req.context.currentVersion] - const currentProductPath = removeFPTFromPath(path.posix.join('/', req.context.currentLanguage, req.context.currentVersion, req.context.currentProduct)) - const currentProductTree = findPageInSiteTree(currentRootTree, req.context.currentEnglishTree, currentProductPath) + const currentRootTree = + req.context.siteTree[req.context.currentLanguage][req.context.currentVersion] + const currentProductPath = removeFPTFromPath( + path.posix.join( + '/', + req.context.currentLanguage, + req.context.currentVersion, + req.context.currentProduct + ) + ) + const currentProductTree = findPageInSiteTree( + currentRootTree, + req.context.currentEnglishTree, + currentProductPath + ) req.context.currentProductTree = currentProductTree diff --git a/middleware/contextualizers/early-access-breadcrumbs.js b/middleware/contextualizers/early-access-breadcrumbs.js index de4d4c8eae7d..5805ec14a3e3 100644 --- a/middleware/contextualizers/early-access-breadcrumbs.js +++ b/middleware/contextualizers/early-access-breadcrumbs.js @@ -1,4 +1,4 @@ -export default async function breadcrumbs (req, res, next) { +export default async function breadcrumbs(req, res, next) { if (!req.context.page) return next() if (!req.context.page.relativePath.startsWith('early-access')) return next() @@ -9,14 +9,16 @@ export default async function breadcrumbs (req, res, next) { return next() } - const earlyAccessProduct = req.context.siteTree[req.language][req.context.currentVersion].childPages.find(childPage => childPage.page.relativePath === 'early-access/index.md') + const earlyAccessProduct = req.context.siteTree[req.language][ + req.context.currentVersion + ].childPages.find((childPage) => childPage.page.relativePath === 'early-access/index.md') if (!earlyAccessProduct) return next() // Create initial landing page breadcrumb req.context.breadcrumbs.push({ documentType: earlyAccessProduct.page.documentType, href: '', - title: earlyAccessProduct.page.title + title: earlyAccessProduct.page.title, }) // If this is the Early Access landing page, return now @@ -25,26 +27,25 @@ export default async function breadcrumbs (req, res, next) { } // Otherwise, create breadcrumbs - await createBreadcrumb( - earlyAccessProduct.childPages, - req.context - ) + await createBreadcrumb(earlyAccessProduct.childPages, req.context) return next() } -async function createBreadcrumb (pageArray, context) { +async function createBreadcrumb(pageArray, context) { // Find each page in the siteTree's array of child pages that starts with the requested path. - const childPage = pageArray.find(page => context.currentPath.startsWith(page.href)) + const childPage = pageArray.find((page) => context.currentPath.startsWith(page.href)) // Gray out product breadcrumb links and `Articles` categories - const hideHref = childPage.page.documentType === 'product' || - (childPage.page.documentType === 'category' && childPage.page.relativePath.endsWith('/articles/index.md')) + const hideHref = + childPage.page.documentType === 'product' || + (childPage.page.documentType === 'category' && + childPage.page.relativePath.endsWith('/articles/index.md')) context.breadcrumbs.push({ documentType: childPage.page.documentType, href: hideHref ? '' : childPage.href, - title: await childPage.page.renderTitle(context, { textOnly: true, encodeEntities: true }) + title: await childPage.page.renderTitle(context, { textOnly: true, encodeEntities: true }), }) // Recursively loop through the siteTree and create each breadcrumb, until we reach the diff --git a/middleware/contextualizers/early-access-links.js b/middleware/contextualizers/early-access-links.js index 9022d50290c7..a106a0ca7d4e 100644 --- a/middleware/contextualizers/early-access-links.js +++ b/middleware/contextualizers/early-access-links.js @@ -1,20 +1,27 @@ import { uniq } from 'lodash-es' -export default function earlyAccessContext (req, res, next) { +export default function earlyAccessContext(req, res, next) { if (process.env.NODE_ENV === 'production') { return next(404) } // Get a list of all hidden pages per version - const earlyAccessPageLinks = uniq(Object.values(req.context.pages) - .filter(page => page.hidden && page.relativePath.startsWith('early-access') && !page.relativePath.endsWith('index.md')) - .map(page => page.permalinks) - .flat()) + const earlyAccessPageLinks = uniq( + Object.values(req.context.pages) + .filter( + (page) => + page.hidden && + page.relativePath.startsWith('early-access') && + !page.relativePath.endsWith('index.md') + ) + .map((page) => page.permalinks) + .flat() + ) // Get links for the current version - .filter(permalink => req.context.currentVersion === permalink.pageVersion) + .filter((permalink) => req.context.currentVersion === permalink.pageVersion) .sort() // Create Markdown links - .map(permalink => `- [${permalink.title}](${permalink.href})`) + .map((permalink) => `- [${permalink.title}](${permalink.href})`) // Add to the rendering context // This is only used in the separate EA repo on local development diff --git a/middleware/contextualizers/features.js b/middleware/contextualizers/features.js index d1c393af7e11..36c2b934ab5e 100644 --- a/middleware/contextualizers/features.js +++ b/middleware/contextualizers/features.js @@ -1,16 +1,18 @@ import getApplicableVersions from '../../lib/get-applicable-versions.js' -export default async function features (req, res, next) { +export default async function features(req, res, next) { if (!req.context.page) return next() // Determine whether the currentVersion belongs to the list of versions the feature is available in. - Object.keys(req.context.site.data.features).forEach(featureName => { + Object.keys(req.context.site.data.features).forEach((featureName) => { const { versions } = req.context.site.data.features[featureName] const applicableVersions = getApplicableVersions(versions, req.path) // Adding the resulting boolean to the context object gives us the ability to use // `{% if featureName ... %}` conditionals in content files. - const isFeatureAvailableInCurrentVersion = applicableVersions.includes(req.context.currentVersion) + const isFeatureAvailableInCurrentVersion = applicableVersions.includes( + req.context.currentVersion + ) req.context[featureName] = isFeatureAvailableInCurrentVersion }) diff --git a/middleware/contextualizers/generic-toc.js b/middleware/contextualizers/generic-toc.js index defd82e88238..05d07bf39cc1 100644 --- a/middleware/contextualizers/generic-toc.js +++ b/middleware/contextualizers/generic-toc.js @@ -3,11 +3,12 @@ import findPageInSiteTree from '../../lib/find-page-in-site-tree.js' // This module adds either flatTocItems or nestedTocItems to the context object for // product, categorie, and map topic TOCs that don't have other layouts specified. // They are rendered by includes/generic-toc-flat.html or inclueds/generic-toc-nested.html. -export default async function genericToc (req, res, next) { +export default async function genericToc(req, res, next) { if (!req.context.page) return next() if (req.context.currentLayoutName !== 'default') return next() // This middleware can only run on product, category, and map topics. - if (req.context.page.documentType === 'homepage' || req.context.page.documentType === 'article') return next() + if (req.context.page.documentType === 'homepage' || req.context.page.documentType === 'article') + return next() // This one product TOC is weird. const isOneOffProductToc = req.context.page.relativePath === 'github/index.md' @@ -16,17 +17,23 @@ export default async function genericToc (req, res, next) { const tocTypes = { product: 'flat', category: 'nested', - mapTopic: 'flat' + mapTopic: 'flat', } // Find the current TOC type based on the current document type. const currentTocType = tocTypes[req.context.page.documentType] // Find the part of the site tree that corresponds to the current path. - const treePage = findPageInSiteTree(req.context.currentProductTree, req.context.currentEnglishTree, req.pagePath) + const treePage = findPageInSiteTree( + req.context.currentProductTree, + req.context.currentEnglishTree, + req.pagePath + ) // Do not include hidden child items on a TOC page unless it's an Early Access category page. - req.context.showHiddenTocItems = req.context.page.documentType === 'category' && req.context.currentPath.includes('/early-access/') + req.context.showHiddenTocItems = + req.context.page.documentType === 'category' && + req.context.currentPath.includes('/early-access/') // Conditionally run getTocItems() recursively. let isRecursive @@ -37,30 +44,48 @@ export default async function genericToc (req, res, next) { if (currentTocType === 'flat' && !isOneOffProductToc) { isRecursive = false renderIntros = true - req.context.genericTocFlat = await getTocItems(treePage.childPages, req.context, isRecursive, renderIntros) + req.context.genericTocFlat = await getTocItems( + treePage.childPages, + req.context, + isRecursive, + renderIntros + ) } // Get an array of child map topics and their child articles and add it to the context object. if (currentTocType === 'nested' || isOneOffProductToc) { isRecursive = !isOneOffProductToc renderIntros = false - req.context.genericTocNested = await getTocItems(treePage.childPages, req.context, isRecursive, renderIntros) + req.context.genericTocNested = await getTocItems( + treePage.childPages, + req.context, + isRecursive, + renderIntros + ) } return next() } -async function getTocItems (pagesArray, context, isRecursive, renderIntros) { - return (await Promise.all(pagesArray.map(async (child) => { - if (child.page.hidden && !context.showHiddenTocItems) return +async function getTocItems(pagesArray, context, isRecursive, renderIntros) { + return ( + await Promise.all( + pagesArray.map(async (child) => { + if (child.page.hidden && !context.showHiddenTocItems) return - return { - title: child.renderedFullTitle, - fullPath: child.href, - // renderProp is the most expensive part of this function. - intro: renderIntros ? await child.page.renderProp('intro', context, { unwrap: true }) : null, - childTocItems: isRecursive && child.childPages ? getTocItems(child.childPages, context, isRecursive, renderIntros) : null - } - }))) - .filter(Boolean) + return { + title: child.renderedFullTitle, + fullPath: child.href, + // renderProp is the most expensive part of this function. + intro: renderIntros + ? await child.page.renderProp('intro', context, { unwrap: true }) + : null, + childTocItems: + isRecursive && child.childPages + ? getTocItems(child.childPages, context, isRecursive, renderIntros) + : null, + } + }) + ) + ).filter(Boolean) } diff --git a/middleware/contextualizers/graphql.js b/middleware/contextualizers/graphql.js index 2d41c6d55cb1..142d56a3130a 100644 --- a/middleware/contextualizers/graphql.js +++ b/middleware/contextualizers/graphql.js @@ -8,11 +8,12 @@ const changelog = readJsonFile('./lib/graphql/static/changelog.json') const prerenderedObjects = readJsonFile('./lib/graphql/static/prerendered-objects.json') const prerenderedInputObjects = readJsonFile('./lib/graphql/static/prerendered-input-objects.json') -const explorerUrl = process.env.NODE_ENV === 'production' - ? 'https://graphql.github.com/explorer' - : 'http://localhost:3000' +const explorerUrl = + process.env.NODE_ENV === 'production' + ? 'https://graphql.github.com/explorer' + : 'http://localhost:3000' -export default function graphqlContext (req, res, next) { +export default function graphqlContext(req, res, next) { const currentVersionObj = allVersions[req.context.currentVersion] // ignore requests to non-GraphQL reference paths // and to versions that don't exist @@ -26,13 +27,15 @@ export default function graphqlContext (req, res, next) { const graphqlVersion = currentVersionObj.miscVersionName req.context.graphql = { - schemaForCurrentVersion: JSON.parse(fs.readFileSync(path.join(process.cwd(), `lib/graphql/static/schema-${graphqlVersion}.json`))), + schemaForCurrentVersion: JSON.parse( + fs.readFileSync(path.join(process.cwd(), `lib/graphql/static/schema-${graphqlVersion}.json`)) + ), previewsForCurrentVersion: previews[graphqlVersion], upcomingChangesForCurrentVersion: upcomingChanges[graphqlVersion], prerenderedObjectsForCurrentVersion: prerenderedObjects[graphqlVersion], prerenderedInputObjectsForCurrentVersion: prerenderedInputObjects[graphqlVersion], explorerUrl, - changelog + changelog, } return next() diff --git a/middleware/contextualizers/layout.js b/middleware/contextualizers/layout.js index 01c306281853..0536ad63811f 100644 --- a/middleware/contextualizers/layout.js +++ b/middleware/contextualizers/layout.js @@ -1,6 +1,6 @@ import layouts from '../../lib/layouts.js' -export default function layoutContext (req, res, next) { +export default function layoutContext(req, res, next) { if (!req.context.page) return next() const layoutOptsByType = { @@ -10,10 +10,10 @@ export default function layoutContext (req, res, next) { // A `layout: false` value means use no layout. boolean: '', // For all other files (like articles and the homepage), use the `default` layout. - undefined: 'default' + undefined: 'default', } - const layoutName = layoutOptsByType[typeof (req.context.page.layout)] + const layoutName = layoutOptsByType[typeof req.context.page.layout] // Attach to the context object req.context.currentLayoutName = layoutName diff --git a/middleware/contextualizers/product-examples.js b/middleware/contextualizers/product-examples.js index b25ad7f7a91a..765b6123b6d1 100644 --- a/middleware/contextualizers/product-examples.js +++ b/middleware/contextualizers/product-examples.js @@ -1,6 +1,6 @@ import getApplicableVersions from '../../lib/get-applicable-versions.js' -export default async function productExamples (req, res, next) { +export default async function productExamples(req, res, next) { if (!req.context.page) return next() if (req.context.currentLayoutName !== 'product-landing') return next() @@ -12,10 +12,14 @@ export default async function productExamples (req, res, next) { // We currently only support versioning in code examples. // TODO support versioning across all example types. - req.context.productCodeExamples = productExamples['code-examples'] && productExamples['code-examples'] - .filter(example => { + req.context.productCodeExamples = + productExamples['code-examples'] && + productExamples['code-examples'].filter((example) => { // If an example block does NOT contain the versions prop, assume it's available in all versions - return !example.versions || getApplicableVersions(example.versions).includes(req.context.currentVersion) + return ( + !example.versions || + getApplicableVersions(example.versions).includes(req.context.currentVersion) + ) }) return next() diff --git a/middleware/contextualizers/release-notes.js b/middleware/contextualizers/release-notes.js index e834647567bb..56c0d54bca70 100644 --- a/middleware/contextualizers/release-notes.js +++ b/middleware/contextualizers/release-notes.js @@ -1,16 +1,21 @@ import semver from 'semver' import { all, latest, firstReleaseNote } from '../../lib/enterprise-server-releases.js' -import { sortReleasesByDate, sortPatchKeys, renderPatchNotes, getAllReleases } from '../../lib/release-notes-utils.js' +import { + sortReleasesByDate, + sortPatchKeys, + renderPatchNotes, + getAllReleases, +} from '../../lib/release-notes-utils.js' // Display all GHES release notes, regardless of deprecation status, // starting with the first release notes in 2.20 -const supported = all.filter(release => { - return semver.gte( - semver.coerce(release), semver.coerce(firstReleaseNote) - ) && release !== '11.10.340' +const supported = all.filter((release) => { + return ( + semver.gte(semver.coerce(release), semver.coerce(firstReleaseNote)) && release !== '11.10.340' + ) }) -export default async function releaseNotesContext (req, res, next) { +export default async function releaseNotesContext(req, res, next) { // The `/release-notes` sub-path if (!(req.pagePath.endsWith('/release-notes') || req.pagePath.endsWith('/admin'))) return next() @@ -35,17 +40,22 @@ export default async function releaseNotesContext (req, res, next) { : next() } - const patches = sortPatchKeys(currentReleaseNotes, requestedRelease, { semverSort: hasNumberedReleases }) - req.context.releaseNotes = await Promise.all(patches.map(async patch => renderPatchNotes(patch, req.context))) + const patches = sortPatchKeys(currentReleaseNotes, requestedRelease, { + semverSort: hasNumberedReleases, + }) + req.context.releaseNotes = await Promise.all( + patches.map(async (patch) => renderPatchNotes(patch, req.context)) + ) req.context.releases = getAllReleases(supported, releaseNotesPerPlan, hasNumberedReleases) // Add firstPreviousRelease and secondPreviousRelease convenience props for use in includes/product-releases.html - req.context.releases.forEach(release => { - release.firstPreviousRelease = all[all.findIndex(v => v === release.version) + 1] - release.secondPreviousRelease = all[all.findIndex(v => v === release.firstPreviousRelease) + 1] + req.context.releases.forEach((release) => { + release.firstPreviousRelease = all[all.findIndex((v) => v === release.version) + 1] + release.secondPreviousRelease = + all[all.findIndex((v) => v === release.firstPreviousRelease) + 1] }) - const releaseIndex = supported.findIndex(release => release === requestedRelease) + const releaseIndex = supported.findIndex((release) => release === requestedRelease) req.context.nextRelease = supported[releaseIndex - 1] req.context.prevRelease = supported[releaseIndex + 1] @@ -56,12 +66,16 @@ export default async function releaseNotesContext (req, res, next) { // GHAE gets handled here... if (!hasNumberedReleases) { const sortedReleases = sortReleasesByDate(releaseNotesPerPlan) - const sortedNotes = sortedReleases.map(release => sortPatchKeys(releaseNotesPerPlan[release], release, { semverSort: false })).flat() + const sortedNotes = sortedReleases + .map((release) => sortPatchKeys(releaseNotesPerPlan[release], release, { semverSort: false })) + .flat() - req.context.releaseNotes = await Promise.all(sortedNotes.map(async patch => renderPatchNotes(patch, req.context))) + req.context.releaseNotes = await Promise.all( + sortedNotes.map(async (patch) => renderPatchNotes(patch, req.context)) + ) req.context.releases = getAllReleases(sortedReleases, releaseNotesPerPlan, hasNumberedReleases) // do some date format massaging, since we want the friendly date to render as the "version" - .map(r => { + .map((r) => { const d = r.patches[0].friendlyDate.split(' ') d.splice(1, 1) r.version = d.join(' ') diff --git a/middleware/contextualizers/rest.js b/middleware/contextualizers/rest.js index d9adf45881b7..3a695eaba81e 100644 --- a/middleware/contextualizers/rest.js +++ b/middleware/contextualizers/rest.js @@ -2,17 +2,14 @@ import path from 'path' import rest from '../../lib/rest/index.js' import removeFPTFromPath from '../../lib/remove-fpt-from-path.js' -export default function restContext (req, res, next) { +export default function restContext(req, res, next) { req.context.rest = rest // link to include in `Works with GitHub Apps` notes // e.g. /ja/rest/reference/apps or /en/enterprise/2.20/user/rest/reference/apps - req.context.restGitHubAppsLink = removeFPTFromPath(path.join( - '/', - req.context.currentLanguage, - req.context.currentVersion, - '/developers/apps' - )) + req.context.restGitHubAppsLink = removeFPTFromPath( + path.join('/', req.context.currentLanguage, req.context.currentVersion, '/developers/apps') + ) // ignore requests to non-REST reference paths if (!req.pagePath.includes('rest/reference')) return next() @@ -29,7 +26,9 @@ export default function restContext (req, res, next) { const operationsForCurrentProduct = req.context.rest.operations[req.context.currentVersion] || [] // find all operations with a category matching the current path - req.context.currentRestOperations = operationsForCurrentProduct.filter(operation => operation.category === category) + req.context.currentRestOperations = operationsForCurrentProduct.filter( + (operation) => operation.category === category + ) return next() } diff --git a/middleware/contextualizers/short-versions.js b/middleware/contextualizers/short-versions.js index 88dbe791de37..bbe1b65a5caa 100644 --- a/middleware/contextualizers/short-versions.js +++ b/middleware/contextualizers/short-versions.js @@ -6,8 +6,7 @@ // {% if ghes %} // // For the custom operator handling in statements like {% if ghes > 3.0 %}, see `lib/liquid-tags/if-ver.js`. -export default async function shortVersions (req, res, next) { - +export default async function shortVersions(req, res, next) { const { allVersions, currentVersion } = req.context const currentVersionObj = allVersions[currentVersion] if (!currentVersionObj) return next() diff --git a/middleware/contextualizers/webhooks.js b/middleware/contextualizers/webhooks.js index 9ad0b3e616f7..475a63f163c1 100644 --- a/middleware/contextualizers/webhooks.js +++ b/middleware/contextualizers/webhooks.js @@ -3,7 +3,7 @@ import webhookPayloads from '../../lib/webhooks/index.js' import nonEnterpriseDefaultVersion from '../../lib/non-enterprise-default-version.js' import allVersions from '../../lib/all-versions.js' -export default function webhooksContext (req, res, next) { +export default function webhooksContext(req, res, next) { const currentVersionObj = allVersions[req.context.currentVersion] // ignore requests to non-webhook reference paths // and to versions that don't exist @@ -20,9 +20,13 @@ export default function webhooksContext (req, res, next) { const webhookPayloadsForCurrentVersion = webhookPayloads[webhookPayloadDir] // if current version is non-dotcom, include dotcom payloads in object so we can fall back to them if needed - req.context.webhookPayloadsForCurrentVersion = req.context.currentVersion === nonEnterpriseDefaultVersion - ? webhookPayloadsForCurrentVersion - : defaults(webhookPayloadsForCurrentVersion, webhookPayloads[allVersions[nonEnterpriseDefaultVersion].miscVersionName]) + req.context.webhookPayloadsForCurrentVersion = + req.context.currentVersion === nonEnterpriseDefaultVersion + ? webhookPayloadsForCurrentVersion + : defaults( + webhookPayloadsForCurrentVersion, + webhookPayloads[allVersions[nonEnterpriseDefaultVersion].miscVersionName] + ) return next() } diff --git a/middleware/contextualizers/whats-new-changelog.js b/middleware/contextualizers/whats-new-changelog.js index 2beaf5ac74ed..522573449e69 100644 --- a/middleware/contextualizers/whats-new-changelog.js +++ b/middleware/contextualizers/whats-new-changelog.js @@ -1,7 +1,7 @@ import { getRssFeed, getChangelogItems } from '../../lib/changelog.js' import getApplicableVersions from '../../lib/get-applicable-versions.js' -export default async function whatsNewChangelog (req, res, next) { +export default async function whatsNewChangelog(req, res, next) { if (!req.context.page) return next() if (!req.context.page.changelog) return next() const label = req.context.page.changelog.label @@ -18,7 +18,7 @@ export default async function whatsNewChangelog (req, res, next) { const labelUrls = { education: 'https://github.blog/category/community/education', - enterprise: 'https://github.blog/category/enterprise/' + enterprise: 'https://github.blog/category/enterprise/', } req.context.changelogUrl = labelUrls[label] || `https://github.blog/changelog/label/${label}` diff --git a/middleware/cookie-parser.js b/middleware/cookie-parser.js index 9319447435ea..151644265879 100644 --- a/middleware/cookie-parser.js +++ b/middleware/cookie-parser.js @@ -1,6 +1,3 @@ import xCookieParser from 'cookie-parser' import xCookieSettings from '../lib/cookie-settings.js' -export default xCookieParser( - process.env.COOKIE_SECRET, - xCookieSettings -) +export default xCookieParser(process.env.COOKIE_SECRET, xCookieSettings) diff --git a/middleware/cors.js b/middleware/cors.js index 4a710344a8e7..546beabbaf01 100644 --- a/middleware/cors.js +++ b/middleware/cors.js @@ -1,5 +1,5 @@ import xCors from 'cors' export default xCors({ origin: '*', - methods: ['GET', 'HEAD'] + methods: ['GET', 'HEAD'], }) diff --git a/middleware/csp.js b/middleware/csp.js index 86b2f18a8d72..573791c9e5f7 100644 --- a/middleware/csp.js +++ b/middleware/csp.js @@ -8,20 +8,12 @@ const { contentSecurityPolicy } = helmet const AZURE_STORAGE_URL = 'githubdocs.azureedge.net' -export default function csp (req, res, next) { +export default function csp(req, res, next) { const csp = { directives: { defaultSrc: ["'none'"], - connectSrc: [ - "'self'", - '*.algolia.net', - '*.algolianet.com' - ], - fontSrc: [ - "'self'", - 'data:', - AZURE_STORAGE_URL - ], + connectSrc: ["'self'", '*.algolia.net', '*.algolianet.com'], + fontSrc: ["'self'", 'data:', AZURE_STORAGE_URL], imgSrc: [ "'self'", 'data:', @@ -29,41 +21,48 @@ export default function csp (req, res, next) { AZURE_STORAGE_URL, 'placehold.it', '*.githubusercontent.com', - 'github.com' - ], - objectSrc: [ - "'self'" + 'github.com', ], + objectSrc: ["'self'"], scriptSrc: [ "'self'", 'data:', // For use during development only! This allows us to use a performant webpack devtool setting (eval) // https://webpack.js.org/configuration/devtool/#devtool - process.env.NODE_ENV === 'development' && "'unsafe-eval'" + process.env.NODE_ENV === 'development' && "'unsafe-eval'", ].filter(Boolean), - frameSrc: [ // exceptions for GraphQL Explorer + frameSrc: [ + // exceptions for GraphQL Explorer 'https://graphql-explorer.githubapp.com', // production env 'https://graphql.github.com/', 'http://localhost:3000', // development env - 'https://www.youtube-nocookie.com' - ], - styleSrc: [ - "'self'", - "'unsafe-inline'" + 'https://www.youtube-nocookie.com', ], + styleSrc: ["'self'", "'unsafe-inline'"], childSrc: [ - "'self'" // exception for search in deprecated GHE versions - ] - } + "'self'", // exception for search in deprecated GHE versions + ], + }, } const { requestedVersion } = isArchivedVersion(req) // Exception for Algolia instantsearch in deprecated Enterprise docs (Node.js era) - if (versionSatisfiesRange(requestedVersion, '<=2.19') && versionSatisfiesRange(requestedVersion, '>2.12')) { - csp.directives.scriptSrc.push("'unsafe-eval'", "'unsafe-inline'", 'http://www.google-analytics.com', 'https://ssl.google-analytics.com') + if ( + versionSatisfiesRange(requestedVersion, '<=2.19') && + versionSatisfiesRange(requestedVersion, '>2.12') + ) { + csp.directives.scriptSrc.push( + "'unsafe-eval'", + "'unsafe-inline'", + 'http://www.google-analytics.com', + 'https://ssl.google-analytics.com' + ) csp.directives.connectSrc.push('https://www.google-analytics.com') - csp.directives.imgSrc.push('http://www.google-analytics.com', 'https://ssl.google-analytics.com') + csp.directives.imgSrc.push( + 'http://www.google-analytics.com', + 'https://ssl.google-analytics.com' + ) } // Exception for search in deprecated Enterprise docs <=2.12 (static site era) diff --git a/middleware/csrf.js b/middleware/csrf.js index 2ce500f0bde7..bdc6f774363a 100644 --- a/middleware/csrf.js +++ b/middleware/csrf.js @@ -3,5 +3,5 @@ import xCsurf from 'csurf' export default xCsurf({ cookie: cookieSettings, - ignoreMethods: ['GET', 'HEAD', 'OPTIONS'] + ignoreMethods: ['GET', 'HEAD', 'OPTIONS'], }) diff --git a/middleware/detect-language.js b/middleware/detect-language.js index e81dce1bdb21..b40621dcfe10 100644 --- a/middleware/detect-language.js +++ b/middleware/detect-language.js @@ -4,24 +4,25 @@ const languageCodes = Object.keys(xLanguages) const chineseRegions = ['CN', 'HK'] -function translationExists (language) { +function translationExists(language) { if (language.code === 'zh') { return chineseRegions.includes(language.region) } return languageCodes.includes(language.code) } -function getLanguageCode (language) { +function getLanguageCode(language) { return language.code === 'zh' && chineseRegions.includes(language.region) ? 'cn' : language.code } -function getUserLanguage (browserLanguages) { +function getUserLanguage(browserLanguages) { try { let userLanguage = getLanguageCode(browserLanguages[0]) let numTopPreferences = 1 for (let lang = 0; lang < browserLanguages.length; lang++) { // If language has multiple regions, Chrome adds the non-region language to list - if (lang > 0 && browserLanguages[lang].code !== browserLanguages[lang - 1].code) numTopPreferences++ + if (lang > 0 && browserLanguages[lang].code !== browserLanguages[lang - 1].code) + numTopPreferences++ if (translationExists(browserLanguages[lang]) && numTopPreferences < 3) { userLanguage = getLanguageCode(browserLanguages[lang]) break @@ -33,7 +34,7 @@ function getUserLanguage (browserLanguages) { } } -export default function detectLanguage (req, res, next) { +export default function detectLanguage(req, res, next) { // determine language code from first part of URL, or default to English // /en/articles/foo // ^^ diff --git a/middleware/dev-toc.js b/middleware/dev-toc.js index 8265231a2be0..54425c36c877 100644 --- a/middleware/dev-toc.js +++ b/middleware/dev-toc.js @@ -2,13 +2,12 @@ import { liquid } from '../lib/render-content/index.js' import layouts from '../lib/layouts.js' import nonEnterpriseDefaultVersion from '../lib/non-enterprise-default-version.js' -export default async function devToc (req, res, next) { +export default async function devToc(req, res, next) { if (process.env.NODE_ENV !== 'development') return next() if (!req.path.endsWith('/dev-toc')) return next() - req.context.devTocVersion = req.path === '/dev-toc' - ? nonEnterpriseDefaultVersion - : req.context.currentVersion + req.context.devTocVersion = + req.path === '/dev-toc' ? nonEnterpriseDefaultVersion : req.context.currentVersion req.context.devTocTree = req.context.siteTree.en[req.context.devTocVersion] diff --git a/middleware/disable-caching-on-safari.js b/middleware/disable-caching-on-safari.js index 126fdffdbb60..413ee53a4b57 100644 --- a/middleware/disable-caching-on-safari.js +++ b/middleware/disable-caching-on-safari.js @@ -1,7 +1,7 @@ -export default function disableCachingOnSafari (req, res, next) { +export default function disableCachingOnSafari(req, res, next) { const isSafari = /^((?!chrome|android).)*safari/i.test(req.headers['user-agent']) if (isSafari) { - res.header('Last-Modified', (new Date()).toUTCString()) + res.header('Last-Modified', new Date().toUTCString()) } return next() } diff --git a/middleware/events.js b/middleware/events.js index 426d9372f3b5..29f6206dee06 100644 --- a/middleware/events.js +++ b/middleware/events.js @@ -11,7 +11,7 @@ addFormats(ajv) const router = express.Router() -router.post('/', async function postEvents (req, res, next) { +router.post('/', async function postEvents(req, res, next) { const isDev = process.env.NODE_ENV === 'development' const fields = omit(req.body, '_csrf') @@ -23,10 +23,7 @@ router.post('/', async function postEvents (req, res, next) { if (req.hydro.maySend()) { // intentionally don't await this async request // so that the http response afterwards is sent immediately - req.hydro.publish( - req.hydro.schemas[fields.type], - omit(fields, OMIT_FIELDS) - ).catch((e) => { + req.hydro.publish(req.hydro.schemas[fields.type], omit(fields, OMIT_FIELDS)).catch((e) => { if (isDev) console.error(e) }) } diff --git a/middleware/featured-links.js b/middleware/featured-links.js index 9faf9e522a54..98460462260e 100644 --- a/middleware/featured-links.js +++ b/middleware/featured-links.js @@ -1,16 +1,25 @@ import getLinkData from '../lib/get-link-data.js' // this middleware adds properties to the context object -export default async function featuredLinks (req, res, next) { +export default async function featuredLinks(req, res, next) { if (!req.context.page) return next() - if (!(req.context.page.relativePath.endsWith('index.md') || req.context.page.layout === 'product-landing')) return next() + if ( + !( + req.context.page.relativePath.endsWith('index.md') || + req.context.page.layout === 'product-landing' + ) + ) + return next() if (!req.context.page.featuredLinks) return next() req.context.featuredLinks = {} for (const key in req.context.page.featuredLinks) { - req.context.featuredLinks[key] = await getLinkData(req.context.page.featuredLinks[key], req.context) + req.context.featuredLinks[key] = await getLinkData( + req.context.page.featuredLinks[key], + req.context + ) } return next() diff --git a/middleware/find-page.js b/middleware/find-page.js index 91362a6f5b40..f9926722561f 100644 --- a/middleware/find-page.js +++ b/middleware/find-page.js @@ -1,6 +1,6 @@ // This middleware uses the request path to find a page in the preloaded context.pages object -export default async function findPage (req, res, next) { +export default async function findPage(req, res, next) { let page = req.context.pages[req.pagePath] // if this is a localized request that can't be found, try finding an English variant diff --git a/middleware/halt-on-dropped-connection.js b/middleware/halt-on-dropped-connection.js index 8fc5d70b70c9..93a7d5c352fb 100644 --- a/middleware/halt-on-dropped-connection.js +++ b/middleware/halt-on-dropped-connection.js @@ -1,11 +1,11 @@ -export function isConnectionDropped (req, res) { +export function isConnectionDropped(req, res) { // Have the flags been set for: // - a global request timeout (via the express-timeout-handler middleware)? // - an aborted request connection (via Node.js core's HTTP IncomingMessage)? return Boolean(res.globalTimeout || req.aborted) } -export function haltOnDroppedConnection (req, res, next) { +export function haltOnDroppedConnection(req, res, next) { // Only proceed if the flag has not been set for the express-timeout-handler middleware if (!isConnectionDropped(req, res)) { return next() diff --git a/middleware/handle-csrf-errors.js b/middleware/handle-csrf-errors.js index 297d98d37c3c..b6b86e4aa381 100644 --- a/middleware/handle-csrf-errors.js +++ b/middleware/handle-csrf-errors.js @@ -1,4 +1,4 @@ -export default function handleCSRFError (error, req, res, next) { +export default function handleCSRFError(error, req, res, next) { // If the CSRF token is bad if (error.code === 'EBADCSRFTOKEN') { return res.sendStatus(403) diff --git a/middleware/handle-errors.js b/middleware/handle-errors.js index 8a62d2876f11..cb232b14404b 100644 --- a/middleware/handle-errors.js +++ b/middleware/handle-errors.js @@ -4,12 +4,12 @@ import FailBot from '../lib/failbot.js' import loadSiteData from '../lib/site-data.js' import builtAssets from '../lib/built-asset-urls.js' -function shouldLogException (error) { +function shouldLogException(error) { const IGNORED_ERRORS = [ // avoid sending CSRF token errors (from bad-actor POST requests) 'EBADCSRFTOKEN', // Client connected aborted - 'ECONNRESET' + 'ECONNRESET', ] if (IGNORED_ERRORS.includes(error.code)) { @@ -20,15 +20,15 @@ function shouldLogException (error) { return true } -async function logException (error, req) { +async function logException(error, req) { if (process.env.NODE_ENV !== 'test' && shouldLogException(error)) { await FailBot.report(error, { - path: req.path + path: req.path, }) } } -export default async function handleError (error, req, res, next) { +export default async function handleError(error, req, res, next) { try { // If the headers have already been sent or the request was aborted... if (res.headersSent || req.aborted) { @@ -53,9 +53,7 @@ export default async function handleError (error, req, res, next) { // Special handling for when a middleware calls `next(404)` if (error === 404) { - return res - .status(404) - .send(await liquid.parseAndRender(layouts['error-404'], req.context)) + return res.status(404).send(await liquid.parseAndRender(layouts['error-404'], req.context)) } // If the error contains a status code, just send that back. This is usually diff --git a/middleware/handle-invalid-paths.js b/middleware/handle-invalid-paths.js index 03f4ac7abdff..6e384f26e334 100644 --- a/middleware/handle-invalid-paths.js +++ b/middleware/handle-invalid-paths.js @@ -1,6 +1,6 @@ import patterns from '../lib/patterns.js' -export default function handleInvalidPaths (req, res, next) { +export default function handleInvalidPaths(req, res, next) { // prevent open redirect vulnerability if (req.path.match(patterns.multipleSlashes)) { return next(404) diff --git a/middleware/handle-next-data-path.js b/middleware/handle-next-data-path.js index 04447500a73d..a4af213a1c42 100644 --- a/middleware/handle-next-data-path.js +++ b/middleware/handle-next-data-path.js @@ -1,6 +1,6 @@ -export default async function handleNextDataPath (req, res, next) { - if (req.path.startsWith('/_next/data') && req.path.endsWith('.json')) { - // translate a nextjs data request to a page path that the server can use on context +export default async function handleNextDataPath(req, res, next) { + if (req.path.startsWith('/_next/data') && req.path.endsWith('.json')) { + // translate a nextjs data request to a page path that the server can use on context // this is triggered via client-side route tranistions // example path: // /_next/data/development/en/free-pro-team%40latest/github/setting-up-and-managing-your-github-user-account.json @@ -8,7 +8,7 @@ export default async function handleNextDataPath (req, res, next) { const parts = decodedPath.split('/').slice(4) // free-pro-team@latest should not be included in the page path if (parts[1] === 'free-pro-team@latest') { - parts.splice(1,1) + parts.splice(1, 1) } req.pagePath = '/' + parts.join('/').replace(/.json+$/, '') } else { diff --git a/middleware/index.js b/middleware/index.js index c94a3652ef9b..50e4ea705a53 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -66,10 +66,9 @@ const isTest = NODE_ENV === 'test' || process.env.GITHUB_ACTIONS === 'true' // Catch unhandled promise rejections and passing them to Express's error handler // https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016 -const asyncMiddleware = fn => - (req, res, next) => { - Promise.resolve(fn(req, res, next)).catch(next) - } +const asyncMiddleware = (fn) => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next) +} export default function (app) { // *** Request connection management *** @@ -94,13 +93,15 @@ export default function (app) { // *** Security *** app.use(cors) - app.use(helmet({ - // Override referrerPolicy to match the browser's default: "strict-origin-when-cross-origin". - // Helmet now defaults to "no-referrer", which is a problem for our archived assets proxying. - referrerPolicy: { - policy: 'strict-origin-when-cross-origin' - } - })) + app.use( + helmet({ + // Override referrerPolicy to match the browser's default: "strict-origin-when-cross-origin". + // Helmet now defaults to "no-referrer", which is a problem for our archived assets proxying. + referrerPolicy: { + policy: 'strict-origin-when-cross-origin', + }, + }) + ) app.use(csp) // Must come after helmet app.use(cookieParser) // Must come before csrf app.use(express.json()) // Must come before csrf @@ -138,26 +139,39 @@ export default function (app) { // *** Rendering, 2xx responses *** // I largely ordered these by use frequency - app.use(asyncMiddleware(instrument(archivedEnterpriseVersionsAssets, './archived-enterprise-versions-assets'))) // Must come before static/assets - app.use('/dist', express.static('dist', { - index: false, - etag: false, - immutable: true, - lastModified: false, - maxAge: '28 days' // Could be infinite given our fingerprinting - })) - app.use('/assets', express.static('assets', { - index: false, - etag: false, - lastModified: false, - maxAge: '1 day' // Relatively short in case we update images - })) - app.use('/public', express.static('data/graphql', { - index: false, - etag: false, - lastModified: false, - maxAge: '7 days' // A bit longer since releases are more sparse - })) + app.use( + asyncMiddleware( + instrument(archivedEnterpriseVersionsAssets, './archived-enterprise-versions-assets') + ) + ) // Must come before static/assets + app.use( + '/dist', + express.static('dist', { + index: false, + etag: false, + immutable: true, + lastModified: false, + maxAge: '28 days', // Could be infinite given our fingerprinting + }) + ) + app.use( + '/assets', + express.static('assets', { + index: false, + etag: false, + lastModified: false, + maxAge: '1 day', // Relatively short in case we update images + }) + ) + app.use( + '/public', + express.static('data/graphql', { + index: false, + etag: false, + lastModified: false, + maxAge: '7 days', // A bit longer since releases are more sparse + }) + ) app.use('/events', asyncMiddleware(instrument(events, './events'))) app.use('/search', asyncMiddleware(instrument(search, './search'))) @@ -166,8 +180,14 @@ export default function (app) { app.use(asyncMiddleware(instrument(archivedEnterpriseVersions, './archived-enterprise-versions'))) app.use(instrument(robots, './robots')) - app.use(/(\/.*)?\/early-access$/, instrument(earlyAccessLinks, './contextualizers/early-access-links')) - app.use('/categories.json', asyncMiddleware(instrument(categoriesForSupport, './categories-for-support'))) + app.use( + /(\/.*)?\/early-access$/, + instrument(earlyAccessLinks, './contextualizers/early-access-links') + ) + app.use( + '/categories.json', + asyncMiddleware(instrument(categoriesForSupport, './categories-for-support')) + ) app.use(instrument(loaderio, './loaderio-verification')) app.get('/_500', asyncMiddleware(instrument(triggerError, './trigger-error'))) @@ -184,7 +204,11 @@ export default function (app) { app.use(instrument(currentProductTree, './contextualizers/current-product-tree')) app.use(asyncMiddleware(instrument(genericToc, './contextualizers/generic-toc'))) app.use(asyncMiddleware(instrument(breadcrumbs, './contextualizers/breadcrumbs'))) - app.use(asyncMiddleware(instrument(earlyAccessBreadcrumbs, './contextualizers/early-access-breadcrumbs'))) + app.use( + asyncMiddleware( + instrument(earlyAccessBreadcrumbs, './contextualizers/early-access-breadcrumbs') + ) + ) app.use(asyncMiddleware(instrument(features, './contextualizers/features'))) app.use(asyncMiddleware(instrument(productExamples, './contextualizers/product-examples'))) diff --git a/middleware/is-next-request.js b/middleware/is-next-request.js index 8ede80cb47fe..01e5e9aa8968 100644 --- a/middleware/is-next-request.js +++ b/middleware/is-next-request.js @@ -1,6 +1,6 @@ const { FEATURE_NEXTJS } = process.env -export default function isNextRequest (req, res, next) { +export default function isNextRequest(req, res, next) { req.renderWithNextjs = false if (FEATURE_NEXTJS) { diff --git a/middleware/learning-track.js b/middleware/learning-track.js index 068547e75d6b..5187f8f05b8d 100644 --- a/middleware/learning-track.js +++ b/middleware/learning-track.js @@ -1,7 +1,7 @@ import { getPathWithoutLanguage, getPathWithoutVersion } from '../lib/path-utils.js' import getLinkData from '../lib/get-link-data.js' -export default async function learningTrack (req, res, next) { +export default async function learningTrack(req, res, next) { const noTrack = () => { req.context.currentLearningTrack = {} return next() diff --git a/middleware/loaderio-verification.js b/middleware/loaderio-verification.js index a9bc28fce5eb..346f612fe27a 100644 --- a/middleware/loaderio-verification.js +++ b/middleware/loaderio-verification.js @@ -1,6 +1,6 @@ // prove to loader.io that we own this site // by responding to requests like `/loaderio-12345/` with `loaderio-12345` -export default function loaderIoVerification (req, res, next) { +export default function loaderIoVerification(req, res, next) { if (!req.path.startsWith('/loaderio-')) return next() return res.send(req.path.replace(/\//g, '')) } diff --git a/middleware/next.js b/middleware/next.js index 331b1cf8dbdd..617930b7f680 100644 --- a/middleware/next.js +++ b/middleware/next.js @@ -9,7 +9,7 @@ if (nextApp) { nextApp.prepare() } -function renderPageWithNext (req, res, next) { +function renderPageWithNext(req, res, next) { if (req.path.startsWith('/_next') && !req.path.startsWith('/_next/data')) { return nextHandleRequest(req, res) } diff --git a/middleware/rate-limit.js b/middleware/rate-limit.js index 25c536c22cf4..e52c7a5a7cee 100644 --- a/middleware/rate-limit.js +++ b/middleware/rate-limit.js @@ -9,22 +9,24 @@ const EXPIRES_IN_AS_SECONDS = 60 export default rateLimit({ // 1 minute (or practically unlimited outside of production) - windowMs: isProduction ? (EXPIRES_IN_AS_SECONDS * 1000) : 1, // Non-Redis configuration in `ms`. Used as a fallback when Redis is not working or active. + windowMs: isProduction ? EXPIRES_IN_AS_SECONDS * 1000 : 1, // Non-Redis configuration in `ms`. Used as a fallback when Redis is not working or active. // limit each IP to X requests per windowMs max: 250, // Don't rate limit requests for 200s and redirects // Or anything with a status code less than 400 skipSuccessfulRequests: true, // When available, use Redis; if not, defaults to an in-memory store - store: REDIS_URL && new RedisStore({ - client: createRedisClient({ - url: REDIS_URL, - db: rateLimitDatabaseNumber, - name: 'rate-limit' + store: + REDIS_URL && + new RedisStore({ + client: createRedisClient({ + url: REDIS_URL, + db: rateLimitDatabaseNumber, + name: 'rate-limit', + }), + // 1 minute (or practically unlimited outside of production) + expiry: isProduction ? EXPIRES_IN_AS_SECONDS : 1, // Redis configuration in `s` + // If Redis is not connected, let the request succeed as failover + passIfNotConnected: true, }), - // 1 minute (or practically unlimited outside of production) - expiry: isProduction ? EXPIRES_IN_AS_SECONDS : 1, // Redis configuration in `s` - // If Redis is not connected, let the request succeed as failover - passIfNotConnected: true - }) }) diff --git a/middleware/record-redirect.js b/middleware/record-redirect.js index 45e6a79fe982..1d3d39372c1c 100644 --- a/middleware/record-redirect.js +++ b/middleware/record-redirect.js @@ -1,9 +1,9 @@ import { v4 as uuidv4 } from 'uuid' -export default function recordRedirects (req, res, next) { +export default function recordRedirects(req, res, next) { if (!req.hydro.maySend()) return next() - res.on('finish', async function recordRedirect () { + res.on('finish', async function recordRedirect() { // We definitely don't want 304 if (![301, 302, 303, 307, 308].includes(res.statusCode)) return const schemaName = req.hydro.schemas.redirect @@ -14,10 +14,10 @@ export default function recordRedirects (req, res, next) { version: '1.0.0', created: new Date().toISOString(), path: req.path, - referrer: req.get('referer') + referrer: req.get('referer'), }, redirect_from: req.originalUrl, - redirect_to: res.get('location') + redirect_to: res.get('location'), } const hydroRes = await req.hydro.publish(schemaName, redirectEvent) if (!hydroRes.ok) console.log('Failed to record redirect to Hydro') diff --git a/middleware/redirects/external.js b/middleware/redirects/external.js index a38418e87de1..192515885014 100644 --- a/middleware/redirects/external.js +++ b/middleware/redirects/external.js @@ -2,7 +2,7 @@ import readJsonFile from '../../lib/read-json-file.js' const externalSites = readJsonFile('./lib/redirects/external-sites.json') // blanket redirects to external websites -export default function externalRedirects (req, res, next) { +export default function externalRedirects(req, res, next) { if (req.path in externalSites) { return res.redirect(301, externalSites[req.path]) } else { diff --git a/middleware/redirects/handle-redirects.js b/middleware/redirects/handle-redirects.js index d682a25f4a62..503aab1bdd5c 100644 --- a/middleware/redirects/handle-redirects.js +++ b/middleware/redirects/handle-redirects.js @@ -1,7 +1,7 @@ import patterns from '../../lib/patterns.js' import { URL } from 'url' -export default function handleRedirects (req, res, next) { +export default function handleRedirects(req, res, next) { // never redirect assets if (patterns.assetPaths.test(req.path)) return next() @@ -27,7 +27,8 @@ export default function handleRedirects (req, res, next) { // look for a redirect in the global object // for example, given an incoming path /v3/activity/event_types // find /en/developers/webhooks-and-events/github-event-types - redirectWithoutQueryParams = req.context.redirects[redirectWithoutQueryParams] || redirectWithoutQueryParams + redirectWithoutQueryParams = + req.context.redirects[redirectWithoutQueryParams] || redirectWithoutQueryParams // add query params back in redirect = queryParams ? redirectWithoutQueryParams + queryParams : redirectWithoutQueryParams @@ -50,6 +51,6 @@ export default function handleRedirects (req, res, next) { return res.redirect(301, redirect) } -function removeQueryParams (redirect) { +function removeQueryParams(redirect) { return new URL(redirect, 'https://docs.github.com').pathname } diff --git a/middleware/redirects/help-to-docs.js b/middleware/redirects/help-to-docs.js index a968ad954c66..ab7ebb8688a5 100644 --- a/middleware/redirects/help-to-docs.js +++ b/middleware/redirects/help-to-docs.js @@ -3,7 +3,7 @@ import patterns from '../../lib/patterns.js' // redirect help.github.com requests to docs.github.com -export default function helpToDocs (req, res, next) { +export default function helpToDocs(req, res, next) { if (req.hostname === 'help.github.com') { // prevent open redirect security vulnerability const path = req.originalUrl.replace(patterns.multipleSlashes, '/') diff --git a/middleware/redirects/language-code-redirects.js b/middleware/redirects/language-code-redirects.js index 7b70845f1a57..58bbef5aeac0 100644 --- a/middleware/redirects/language-code-redirects.js +++ b/middleware/redirects/language-code-redirects.js @@ -5,7 +5,7 @@ import languages from '../../lib/languages.js' // Examples: // /jp* -> /ja* // /zh-TW* -> /cn* -export default function languageCodeRedirects (req, res, next) { +export default function languageCodeRedirects(req, res, next) { for (const code in languages) { const language = languages[code] const redirectPatterns = language.redirectPatterns || [] diff --git a/middleware/render-page.js b/middleware/render-page.js index 8a38fca65236..2ed6a3cb4a26 100644 --- a/middleware/render-page.js +++ b/middleware/render-page.js @@ -21,21 +21,21 @@ const pageCache = new RedisAccessor({ allowSetFailures: true, // Allow for graceful failures if a Redis GET operation fails allowGetFailures: true, - name: 'page-cache' + name: 'page-cache', }) // a list of query params that *do* alter the rendered page, and therefore should be cached separately const cacheableQueries = ['learn'] -function modifyOutput (req, text) { +function modifyOutput(req, text) { return addColorMode(req, addCsrf(req, text)) } -function addCsrf (req, text) { +function addCsrf(req, text) { return text.replace('$CSRFTOKEN$', req.csrfToken()) } -function addColorMode (req, text) { +function addColorMode(req, text) { let colorMode = 'auto' let darkTheme = 'dark' let lightTheme = 'light' @@ -55,20 +55,19 @@ function addColorMode (req, text) { .replace('$LIGHTTHEME$', lightTheme) } -export default async function renderPage (req, res, next) { +export default async function renderPage(req, res, next) { const page = req.context.page // render a 404 page if (!page) { if (process.env.NODE_ENV !== 'test' && req.context.redirectNotFound) { - console.error(`\nTried to redirect to ${req.context.redirectNotFound}, but that page was not found.\n`) - } - return res.status(404).send( - modifyOutput( - req, - await liquid.parseAndRender(layouts['error-404'], req.context) + console.error( + `\nTried to redirect to ${req.context.redirectNotFound}, but that page was not found.\n` ) - ) + } + return res + .status(404) + .send(modifyOutput(req, await liquid.parseAndRender(layouts['error-404'], req.context))) } if (req.method === 'HEAD') { @@ -95,7 +94,7 @@ export default async function renderPage (req, res, next) { const isGraphQLExplorer = req.context.currentPathWithoutLanguage === '/graphql/overview/explorer' // Serve from the cache if possible - const isCacheable = ( + const isCacheable = // Skip for CI !process.env.CI && // Skip for tests @@ -110,7 +109,6 @@ export default async function renderPage (req, res, next) { !isAirgapped && // Skip for the GraphQL Explorer page !isGraphQLExplorer - ) if (isCacheable) { // Stop processing if the connection was already dropped @@ -149,15 +147,21 @@ export default async function renderPage (req, res, next) { // handle special-case prerendered GraphQL objects page if (req.pagePath.endsWith('graphql/reference/objects')) { // concat the markdown source miniToc items and the prerendered miniToc items - context.miniTocItems = context.miniTocItems.concat(req.context.graphql.prerenderedObjectsForCurrentVersion.miniToc) - context.renderedPage = context.renderedPage + req.context.graphql.prerenderedObjectsForCurrentVersion.html + context.miniTocItems = context.miniTocItems.concat( + req.context.graphql.prerenderedObjectsForCurrentVersion.miniToc + ) + context.renderedPage = + context.renderedPage + req.context.graphql.prerenderedObjectsForCurrentVersion.html } // handle special-case prerendered GraphQL input objects page if (req.pagePath.endsWith('graphql/reference/input-objects')) { // concat the markdown source miniToc items and the prerendered miniToc items - context.miniTocItems = context.miniTocItems.concat(req.context.graphql.prerenderedInputObjectsForCurrentVersion.miniToc) - context.renderedPage = context.renderedPage + req.context.graphql.prerenderedInputObjectsForCurrentVersion.html + context.miniTocItems = context.miniTocItems.concat( + req.context.graphql.prerenderedInputObjectsForCurrentVersion.miniToc + ) + context.renderedPage = + context.renderedPage + req.context.graphql.prerenderedInputObjectsForCurrentVersion.html } // Create string for tag @@ -165,7 +169,8 @@ export default async function renderPage (req, res, next) { // add localized ` - GitHub Docs` suffix to <title> tag (except for the homepage) if (!patterns.homepagePath.test(req.pagePath)) { - context.page.fullTitle = context.page.fullTitle + ' - ' + context.site.data.ui.header.github_docs + context.page.fullTitle = + context.page.fullTitle + ' - ' + context.site.data.ui.header.github_docs } // `?json` query param for debugging request context @@ -176,8 +181,9 @@ export default async function renderPage (req, res, next) { } else { // dump all the keys: ?json return res.json({ - message: 'The full context object is too big to display! Try one of the individual keys below, e.g. ?json=page. You can also access nested props like ?json=site.data.reusables', - keys: Object.keys(context) + message: + 'The full context object is too big to display! Try one of the individual keys below, e.g. ?json=page. You can also access nested props like ?json=site.data.reusables', + keys: Object.keys(context), }) } } diff --git a/middleware/req-utils.js b/middleware/req-utils.js index d4b4f3500616..f64c1ed5eebb 100644 --- a/middleware/req-utils.js +++ b/middleware/req-utils.js @@ -1,6 +1,6 @@ import Hydro from '../lib/hydro.js' -export default function reqUtils (req, res, next) { +export default function reqUtils(req, res, next) { req.hydro = new Hydro() return next() } diff --git a/middleware/robots.js b/middleware/robots.js index a77a96f59eb9..ac89bee9655e 100644 --- a/middleware/robots.js +++ b/middleware/robots.js @@ -3,7 +3,7 @@ const defaultResponse = 'User-agent: *' const disallowAll = `User-agent: * Disallow: /` -export default function robots (req, res, next) { +export default function robots(req, res, next) { if (req.path !== '/robots.txt') return next() res.type('text/plain') diff --git a/middleware/search.js b/middleware/search.js index d6559a31b9cb..1743068ab512 100644 --- a/middleware/search.js +++ b/middleware/search.js @@ -8,10 +8,10 @@ const versions = new Set(Object.values(searchVersions)) const router = express.Router() -router.get('/', async function postSearch (req, res, next) { +router.get('/', async function postSearch(req, res, next) { res.set({ 'surrogate-control': 'private, no-store', - 'cache-control': 'private, no-store' + 'cache-control': 'private, no-store', }) const { query, version, language, filters, limit: limit_ } = req.query @@ -24,9 +24,10 @@ router.get('/', async function postSearch (req, res, next) { } try { - const results = process.env.AIRGAP || req.cookies.AIRGAP - ? await loadLunrResults({ version, language, query: `${query} ${filters || ''}`, limit }) - : await loadAlgoliaResults({ version, language, query, filters, limit }) + const results = + process.env.AIRGAP || req.cookies.AIRGAP + ? await loadLunrResults({ version, language, query: `${query} ${filters || ''}`, limit }) + : await loadAlgoliaResults({ version, language, query, filters, limit }) // Only reply if the headers have not been sent and the request was not aborted... if (!res.headersSent && !req.aborted) { diff --git a/middleware/set-fastly-cache-headers.js b/middleware/set-fastly-cache-headers.js index e238611b4b4a..c747736f771c 100644 --- a/middleware/set-fastly-cache-headers.js +++ b/middleware/set-fastly-cache-headers.js @@ -1,8 +1,8 @@ -export default function setFastlyCacheHeaders (req, res, next) { +export default function setFastlyCacheHeaders(req, res, next) { // Disallow both Fastly AND the browser from caching HTML pages res.set({ 'surrogate-control': 'private, no-store', - 'cache-control': 'private, no-store' + 'cache-control': 'private, no-store', }) return next() } diff --git a/middleware/set-fastly-surrogate-key.js b/middleware/set-fastly-surrogate-key.js index 9865b209b147..593c11f651c8 100644 --- a/middleware/set-fastly-surrogate-key.js +++ b/middleware/set-fastly-surrogate-key.js @@ -1,4 +1,4 @@ -export default function setFastlySurrogateKey (req, res, next) { +export default function setFastlySurrogateKey(req, res, next) { // Fastly provides a Soft Purge feature that allows you to mark content as outdated (stale) instead of permanently // purging and thereby deleting it from Fastly's caches. Objects invalidated with Soft Purge will be treated as // outdated (stale) while Fastly fetches a new version from origin. diff --git a/middleware/timeout.js b/middleware/timeout.js index d685db210422..0764576330cf 100644 --- a/middleware/timeout.js +++ b/middleware/timeout.js @@ -21,7 +21,7 @@ export default timeout.handler({ // Pass the error to our Express error handler for consolidated processing return next(timeoutError) - } + }, // Can also set an `onDelayedResponse` property IF AND ONLY IF you allow for disabling methods }) diff --git a/middleware/trigger-error.js b/middleware/trigger-error.js index 5f468cf8b088..084b5f0c2f23 100644 --- a/middleware/trigger-error.js +++ b/middleware/trigger-error.js @@ -1,7 +1,7 @@ // This module is for testing our handling of uncaught async rejections on incoming requests // IMPORTANT: Leave this function as `async` even though it doesn't need to be! -export default async function triggerError (req, res, next) { +export default async function triggerError(req, res, next) { // IMPORTANT: // Do NOT wrap this method's contents in the usual `try-catch+next(error)` // pattern used on async middleware! This is an intentional omission! diff --git a/next.config.js b/next.config.js index de7e7780bb55..8644dd711b58 100644 --- a/next.config.js +++ b/next.config.js @@ -15,7 +15,7 @@ module.exports = { webpack: 5, }, typescript: { - ignoreBuildErrors: true + ignoreBuildErrors: true, }, eslint: { ignoreDuringBuilds: true, @@ -23,18 +23,18 @@ module.exports = { i18n: { // locales: Object.values(languages).map(({ code }) => code), locales: ['en', 'cn', 'ja', 'es', 'pt', 'de'], - defaultLocale: 'en' + defaultLocale: 'en', }, sassOptions: { - quietDeps: true + quietDeps: true, }, - async rewrites () { + async rewrites() { const DEFAULT_VERSION = 'free-pro-team@latest' return productIds.map((productId) => { return { source: `/${productId}/:path*`, - destination: `/${DEFAULT_VERSION}/${productId}/:path*` + destination: `/${DEFAULT_VERSION}/${productId}/:path*`, } }) - } + }, } diff --git a/package.json b/package.json index a554bf387294..12ea659be7ee 100644 --- a/package.json +++ b/package.json @@ -201,12 +201,12 @@ "link-check": "start-server-and-test link-check-server 4002 link-check-test", "link-check-server": "cross-env NODE_ENV=development ENABLED_LANGUAGES='en' PORT=4002 node server.mjs", "link-check-test": "cross-env node script/check-internal-links.js", - "lint": "eslint '**/*.{js,ts,tsx}' && prettier -w \"**/*.{yml,yaml}\" && npm run lint-tsc", + "lint": "eslint '**/*.{js,mjs,ts,tsx}'", "lint-translation": "cross-env TEST_TRANSLATION=true jest content/lint-files", - "lint-tsc": "prettier -w \"**/*.{ts,tsx}\"", "pa11y-ci": "pa11y-ci", "pa11y-test": "start-server-and-test browser-test-server 4001 pa11y-ci", "prebrowser-test": "npm run build", + "prettier": "prettier -w \"**/*.{ts,tsx,js,jsx,mjs,scss,yml,yaml}\"", "prevent-pushes-to-main": "node script/prevent-pushes-to-main.js", "rest-dev": "script/rest/update-files.js && npm run dev", "start": "cross-env NODE_ENV=development ENABLED_LANGUAGES='en,ja' nodemon server.mjs", diff --git a/script/anonymize-branch.js b/script/anonymize-branch.js index 6ba4df67ee55..d3ab07883997 100755 --- a/script/anonymize-branch.js +++ b/script/anonymize-branch.js @@ -13,14 +13,19 @@ import path from 'path' // [end-readme] process.env.GIT_AUTHOR_NAME = process.env.GIT_COMMITTER_NAME = 'Octomerger Bot' -process.env.GIT_AUTHOR_EMAIL = process.env.GIT_COMMITTER_EMAIL = '63058869+Octomerger@users.noreply.github.com' +process.env.GIT_AUTHOR_EMAIL = process.env.GIT_COMMITTER_EMAIL = + '63058869+Octomerger@users.noreply.github.com' const args = process.argv.slice(2) const message = args[0] const base = args[1] || 'main' if (!message || !message.length) { - console.error(`Specify a new commit message in quotes. Example:\n\nscript/${path.basename(module.filename)} "new commit"`) + console.error( + `Specify a new commit message in quotes. Example:\n\nscript/${path.basename( + module.filename + )} "new commit"` + ) process.exit() } diff --git a/script/backfill-missing-localizations.js b/script/backfill-missing-localizations.js index 6c501f308af7..5394885013bb 100755 --- a/script/backfill-missing-localizations.js +++ b/script/backfill-missing-localizations.js @@ -18,16 +18,16 @@ const dirs = ['content', 'data'] // // [end-readme] -dirs.forEach(dir => { +dirs.forEach((dir) => { const englishPath = path.join(__dirname, `../${dir}`) - const filenames = walk(englishPath) - .filter(filename => { - return (filename.endsWith('.yml') || filename.endsWith('.md')) && - !filename.endsWith('README.md') - }) + const filenames = walk(englishPath).filter((filename) => { + return ( + (filename.endsWith('.yml') || filename.endsWith('.md')) && !filename.endsWith('README.md') + ) + }) - filenames.forEach(filename => { - Object.values(languages).forEach(language => { + filenames.forEach((filename) => { + Object.values(languages).forEach((language) => { if (language.code === 'en') return const fullPath = path.join(__dirname, '..', language.dir, dir, filename) if (!fs.existsSync(fullPath)) { diff --git a/script/check-english-links.js b/script/check-english-links.js index 653cf81996b9..0c173cb9d419 100755 --- a/script/check-english-links.js +++ b/script/check-english-links.js @@ -34,17 +34,26 @@ const retryStatusCodes = [429, 503, 'Invalid'] program .description('Check all links in the English docs.') - .option('-d, --dry-run', 'Turn off recursion to get a fast minimal report (useful for previewing output).') - .option('-r, --do-not-retry', `Do not retry broken links with status codes ${retryStatusCodes.join(', ')}.`) - .option('-p, --path <PATH>', `Provide an optional path to check. Best used with --dry-run. Default: ${englishRoot}`) + .option( + '-d, --dry-run', + 'Turn off recursion to get a fast minimal report (useful for previewing output).' + ) + .option( + '-r, --do-not-retry', + `Do not retry broken links with status codes ${retryStatusCodes.join(', ')}.` + ) + .option( + '-p, --path <PATH>', + `Provide an optional path to check. Best used with --dry-run. Default: ${englishRoot}` + ) .parse(process.argv) // Skip excluded links defined in separate file. // Skip non-English content. const languagesToSkip = Object.keys(xLanguages) - .filter(code => code !== 'en') - .map(code => `${root}/${code}`) + .filter((code) => code !== 'en') + .map((code) => `${root}/${code}`) // Skip deprecated Enterprise content. // Capture the old format https://docs.github.com/enterprise/2.1/ @@ -58,23 +67,19 @@ const config = { recurse: !program.opts().dryRun, silent: true, // The values in this array are treated as regexes. - linksToSkip: [ - enterpriseReleasesToSkip, - ...languagesToSkip, - ...excludedLinks - ] + linksToSkip: [enterpriseReleasesToSkip, ...languagesToSkip, ...excludedLinks], } main() -async function main () { +async function main() { // Clear and recreate a directory for logs. const logFile = path.join(__dirname, '../.linkinator/full.log') rimraf(path.dirname(logFile)) mkdirp(path.dirname(logFile)) // Update CLI output and append to logfile after each checked link. - checker.on('link', result => { + checker.on('link', (result) => { // We don't need to dump all of the HTTP and HTML details delete result.failureDetails @@ -86,28 +91,31 @@ async function main () { // Scan is complete! Filter the results for broken links. const brokenLinks = result - .filter(link => link.state === 'BROKEN') + .filter((link) => link.state === 'BROKEN') // Coerce undefined status codes into `Invalid` strings so we can display them. // Without this, undefined codes get JSON.stringified as `0`, which is not useful output. - .map(link => { link.status = link.status || 'Invalid'; return link }) + .map((link) => { + link.status = link.status || 'Invalid' + return link + }) if (!program.opts().doNotRetry) { // Links to retry individually. - const linksToRetry = brokenLinks - .filter(link => retryStatusCodes.includes(link.status)) + const linksToRetry = brokenLinks.filter((link) => retryStatusCodes.includes(link.status)) - await Promise.all(linksToRetry - .map(async (link) => { + await Promise.all( + linksToRetry.map(async (link) => { try { // got throws an HTTPError if response code is not 2xx or 3xx. // If got succeeds, we can remove the link from the list. await got(link.url) pull(brokenLinks, link) - // If got fails, do nothing. The link is already in the broken list. + // If got fails, do nothing. The link is already in the broken list. } catch (err) { // noop } - })) + }) + ) } // Exit successfully if no broken links! @@ -124,20 +132,21 @@ async function main () { process.exit(1) } -function displayBrokenLinks (brokenLinks) { +function displayBrokenLinks(brokenLinks) { // Sort results by status code. - const allStatusCodes = uniq(brokenLinks - // Coerce undefined status codes into `Invalid` strings so we can display them. - // Without this, undefined codes get JSON.stringified as `0`, which is not useful output. - .map(link => link.status || 'Invalid') + const allStatusCodes = uniq( + brokenLinks + // Coerce undefined status codes into `Invalid` strings so we can display them. + // Without this, undefined codes get JSON.stringified as `0`, which is not useful output. + .map((link) => link.status || 'Invalid') ) - allStatusCodes.forEach(statusCode => { - const brokenLinksForStatus = brokenLinks.filter(x => x.status === statusCode) + allStatusCodes.forEach((statusCode) => { + const brokenLinksForStatus = brokenLinks.filter((x) => x.status === statusCode) console.log(`## Status ${statusCode}: Found ${brokenLinksForStatus.length} broken links`) console.log('```') - brokenLinksForStatus.forEach(brokenLinkObj => { + brokenLinksForStatus.forEach((brokenLinkObj) => { // We don't need to dump all of the HTTP and HTML details delete brokenLinkObj.failureDetails diff --git a/script/check-internal-links.js b/script/check-internal-links.js index 54eae15b7ee1..f3a85e023ac1 100755 --- a/script/check-internal-links.js +++ b/script/check-internal-links.js @@ -33,14 +33,18 @@ const config = { // Skip dist files '/dist/index.*', // Skip deprecated Enterprise content - `enterprise(-server@|/)(${deprecated.join('|')})(/|$)` - ] + `enterprise(-server@|/)(${deprecated.join('|')})(/|$)`, + ], } // Customize config for specific versions if (process.env.DOCS_VERSION === 'dotcom') { // If Dotcom, skip Enterprise Server and GitHub AE links - config.linksToSkip.push('^.*/enterprise-server@.*$', '^.*/enterprise/.*$', '^.*/github-ae@latest.*$') + config.linksToSkip.push( + '^.*/enterprise-server@.*$', + '^.*/enterprise/.*$', + '^.*/github-ae@latest.*$' + ) } else if (process.env.DOCS_VERSION === 'enterprise-server') { // If Enterprise Server, skip links that are not Enterprise Server links config.path = `${englishRoot}/enterprise-server@${latest}` @@ -53,7 +57,7 @@ if (process.env.DOCS_VERSION === 'dotcom') { main() -async function main () { +async function main() { process.env.DOCS_VERSION && allowedVersions.includes(process.env.DOCS_VERSION) ? console.log(`Checking internal links for version ${process.env.DOCS_VERSION}!\n`) : console.log('Checking internal links for all versions!\n') @@ -63,8 +67,11 @@ async function main () { console.timeEnd('check') const brokenLinks = result - .filter(link => link.state === 'BROKEN') - .map(link => { delete link.failureDetails; return link }) + .filter((link) => link.state === 'BROKEN') + .map((link) => { + delete link.failureDetails + return link + }) if (brokenLinks.length === 1 && brokenLinks[0].url === englishRoot) { console.log(`You must be running ${englishRoot}!\n\nTry instead: npm run link-check`) @@ -78,7 +85,9 @@ async function main () { } console.log('\n==============================') - console.log(`Found ${brokenLinks.length} total broken links: ${JSON.stringify([...brokenLinks], null, 2)}`) + console.log( + `Found ${brokenLinks.length} total broken links: ${JSON.stringify([...brokenLinks], null, 2)}` + ) console.log('==============================\n') // Exit unsuccessfully if broken links are found. diff --git a/script/code/convert-cjs-to-esm.mjs b/script/code/convert-cjs-to-esm.mjs index 7f53316dac17..ea2384865882 100755 --- a/script/code/convert-cjs-to-esm.mjs +++ b/script/code/convert-cjs-to-esm.mjs @@ -21,62 +21,50 @@ import semver from 'semver' import walkSync from 'walk-sync' // https://stackoverflow.com/a/31102605 -function orderKeys (unordered) { - return Object.keys(unordered).sort().reduce( - (obj, key) => { - obj[key] = unordered[key].constructor === {}.constructor - ? orderKeys(unordered[key]) - : unordered[key] +function orderKeys(unordered) { + return Object.keys(unordered) + .sort() + .reduce((obj, key) => { + obj[key] = + unordered[key].constructor === {}.constructor ? orderKeys(unordered[key]) : unordered[key] return obj - }, - {} - ) + }, {}) } -async function readPackageFile () { +async function readPackageFile() { return JSON.parse(await fs.readFile('./package.json', 'utf8')) } -async function writePackageFile (packageFile) { +async function writePackageFile(packageFile) { return fs.writeFile('./package.json', JSON.stringify(orderKeys(packageFile), ' ', 2) + '\n') } -async function readAllJsFiles () { +async function readAllJsFiles() { const paths = walkSync('./', { directories: false, includeBasePath: true, globs: ['**/*.js'], - ignore: ['node_modules', 'dist'] + ignore: ['node_modules', 'dist'], }) - return await Promise.all( - paths.map( - async path => [path, await fs.readFile(path, 'utf8')] - ) - ) + return await Promise.all(paths.map(async (path) => [path, await fs.readFile(path, 'utf8')])) } -function listJsonPaths () { +function listJsonPaths() { const paths = walkSync('./', { directories: false, includeBasePath: true, globs: ['**/*.json'], - ignore: ['node_modules', 'dist'] + ignore: ['node_modules', 'dist'], }) - return paths.map(p => p.replace('.json', '')) + return paths.map((p) => p.replace('.json', '')) } function withAllFiles(jsFiles, fn) { - return jsFiles.map( - ([path, file]) => [path, fn(path, file)] - ) + return jsFiles.map(([path, file]) => [path, fn(path, file)]) } -async function writeAllJsFiles (jsFiles) { - return await Promise.all( - jsFiles.map( - async ([path, file]) => await fs.writeFile(path, file) - ) - ) +async function writeAllJsFiles(jsFiles) { + return await Promise.all(jsFiles.map(async ([path, file]) => await fs.writeFile(path, file))) } // Converts a path to an import name @@ -86,19 +74,19 @@ async function writeAllJsFiles (jsFiles) { */ function nameImport(p2) { const myString = p2.split('/').pop() - const string = myString.replace(/[-\.]([a-z])/g, (g) => g[1].toUpperCase()) + const string = myString.replace(/[-.]([a-z])/g, (g) => g[1].toUpperCase()) return `x${string.charAt(0).toUpperCase()}${string.slice(1)}` } // Add "type": "module" to your package.json. -async function addTypeModule () { +async function addTypeModule() { const packageFile = await readPackageFile() packageFile.type = 'module' return writePackageFile(packageFile) } // Replace "main": "index.js" with "exports": "./index.js" in your package.json. -async function updateMainExport () { +async function updateMainExport() { const packageFile = await readPackageFile() const main = packageFile.main if (!main) return @@ -109,16 +97,16 @@ async function updateMainExport () { // Update the "engines" field in package.json to Node.js 12: "node": "^12.20.0 || ^14.13.1 || >=16.0.0". // If 12 is already required, we will skip this change. -async function checkEngines () { +async function checkEngines() { const packageFile = await readPackageFile() const nodeVersion = packageFile.engines.node if (semver.gt(semver.minVersion(nodeVersion), '12.0.0')) return - packageFile.engines.node = "^12.20.0 || ^14.13.1 || >=16.0.0" + packageFile.engines.node = '^12.20.0 || ^14.13.1 || >=16.0.0' await writePackageFile(packageFile) } // Remove 'use strict'; from all JavaScript files. -function noStrict (path, file) { +function noStrict(path, file) { if (file.includes('use strict')) { throw new Error(`Cannot use strict in ${path}. Please remove and run this script again.`) } @@ -131,15 +119,17 @@ import { promises as fs } from 'fs' const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8')) */ -function noJsonReads (jsonPaths) { +function noJsonReads(jsonPaths) { return (path, file) => { - const found = [...file.matchAll(/require\('[\.\/]+(.*?)'\)/gm)] + const found = [...file.matchAll(/require\('[./]+(.*?)'\)/gm)] if (!found) return file const matchesJsonPath = found - .map(f => f[1]) - .filter(f => jsonPaths.some(p => p.endsWith(f))) + .map((f) => f[1]) + .filter((f) => jsonPaths.some((p) => p.endsWith(f))) if (matchesJsonPath.length) { - throw new Error(`${path} has possible JSON requires: ${matchesJsonPath}. Please fix this manually then run the script again.`) + throw new Error( + `${path} has possible JSON requires: ${matchesJsonPath}. Please fix this manually then run the script again.` + ) } return file } @@ -151,88 +141,83 @@ function noJsonReads (jsonPaths) { // Replace `:` to as // Fix up standard const x = require('x') statements -function updateStandardImport (path, file) { - return file.replaceAll( - /^const\s(.*?)\s*?=\s*?require\('(.*?)'\)$/gm, - (_, p1, p2) => { - // Replace `:` to as - p1 = p1.replace(/\s*:\s*/g, ' as ') - // Add `.js` if path starts with `.` - if (p2.startsWith('.') && !p2.endsWith('.js')) p2 = p2 + '.js' - return `import ${p1} from '${p2}'` - } - ) +function updateStandardImport(path, file) { + return file.replaceAll(/^const\s(.*?)\s*?=\s*?require\('(.*?)'\)$/gm, (_, p1, p2) => { + // Replace `:` to as + p1 = p1.replace(/\s*:\s*/g, ' as ') + // Add `.js` if path starts with `.` + if (p2.startsWith('.') && !p2.endsWith('.js')) p2 = p2 + '.js' + return `import ${p1} from '${p2}'` + }) } // Fix up inlined requires that are still "top-level" -function updateInlineImport (path, file) { - return file.replaceAll( - /^(.*?)require\('(.*?)'\)(.*)$/gm, - (_, p1, p2, p3) => { - // Generate a new import name based on the path - const name = nameImport(p2) - // Add `.js` if starts with `.` - if (p2.startsWith('.') && !p2.endsWith('.js')) p2 = p2 + '.js' - // Fix up unused require('x') statements - if (!p1 && !p3) return `import '${p2}'` - return `import ${name} from '${p2}'\n${p1}${name}${p3}` - } - ) +function updateInlineImport(path, file) { + return file.replaceAll(/^(.*?)require\('(.*?)'\)(.*)$/gm, (_, p1, p2, p3) => { + // Generate a new import name based on the path + const name = nameImport(p2) + // Add `.js` if starts with `.` + if (p2.startsWith('.') && !p2.endsWith('.js')) p2 = p2 + '.js' + // Fix up unused require('x') statements + if (!p1 && !p3) return `import '${p2}'` + return `import ${name} from '${p2}'\n${p1}${name}${p3}` + }) } // Handle module.exports = -function updateDefaultExport (path, file) { - return file.replaceAll( - /^module.exports\s*?=\s*?(\S.*)/gm, - 'export default $1' - ) +function updateDefaultExport(path, file) { + return file.replaceAll(/^module.exports\s*?=\s*?(\S.*)/gm, 'export default $1') } // Handle exports.x = -function updateNamedExport (path, file) { - return file.replaceAll( - /^exports\.(\S+)\s*?=\s*?(\S.*)/gm, - 'export const $1 = $2' - ) +function updateNamedExport(path, file) { + return file.replaceAll(/^exports\.(\S+)\s*?=\s*?(\S.*)/gm, 'export const $1 = $2') } // Replace __filename and __dirname -function updateFileAndDir (path, file) { +function updateFileAndDir(path, file) { if (!file.includes('__filename') && !file.includes('__dirname')) return file return [ - 'import { fileURLToPath } from \'url\'', - 'import path from \'path\'', + "import { fileURLToPath } from 'url'", + "import path from 'path'", file.includes('__filename') && 'const __filename = fileURLToPath(import.meta.url)', file.includes('__dirname') && 'const __dirname = path.dirname(fileURLToPath(import.meta.url))', - file - ].filter(Boolean).join('\n') + file, + ] + .filter(Boolean) + .join('\n') } // lodash => lodash-es -function useEsLodash (path, file) { +function useEsLodash(path, file) { return file.replace("'lodash'", "'lodash-es'") } // Pull all imports to the top of the file to avoid syntax issues -function moveImportsToTop (path, file) { +function moveImportsToTop(path, file) { if (!file.includes('import')) return file - const isTop = line => /^import/gm.test(line) + const isTop = (line) => /^import/gm.test(line) const lineEnd = /\r?\n|\r/g - return file.split(lineEnd).filter(isTop).join('\n') + - '\n' + - file.split(lineEnd).filter(line => !isTop(line)).join('\n') + return ( + file.split(lineEnd).filter(isTop).join('\n') + + '\n' + + file + .split(lineEnd) + .filter((line) => !isTop(line)) + .join('\n') + ) } // Make sure script declarations on the top of the file before imports -function updateScriptDeclaration (path, file) { +function updateScriptDeclaration(path, file) { if (!path.startsWith('./script')) return file file = file.replace('#!/usr/bin/env node\n', '') return '#!/usr/bin/env node\n' + file } // Check there's no `require(` ... anywhere -function checkRequire (path, file) { +function checkRequire(path, file) { if (/require\s*\(/.test(file)) { throw new Error(`"require(" still in ${path}`) } @@ -240,14 +225,14 @@ function checkRequire (path, file) { } // Check there's no `exports` ... anywhere -function checkExports (path, file) { +function checkExports(path, file) { if (file.includes('exports')) { throw new Error(`"exports" still in ${path}`) } return file } -async function main () { +async function main() { await addTypeModule() await updateMainExport() await checkEngines() diff --git a/script/content-migrations/add-early-access-tocs.js b/script/content-migrations/add-early-access-tocs.js index 21497dd28bae..40a96b42433b 100755 --- a/script/content-migrations/add-early-access-tocs.js +++ b/script/content-migrations/add-early-access-tocs.js @@ -10,9 +10,8 @@ updateOrCreateToc(earlyAccessDir) console.log('Updated Early Access TOCs!') -function updateOrCreateToc (directory) { - const children = fs.readdirSync(directory) - .filter(subpath => !subpath.endsWith('index.md')) +function updateOrCreateToc(directory) { + const children = fs.readdirSync(directory).filter((subpath) => !subpath.endsWith('index.md')) if (!children.length) return @@ -29,15 +28,15 @@ function updateOrCreateToc (directory) { data = { title: sentenceCase(path.basename(directory)), versions: '*', - hidden: true + hidden: true, } } - data.children = children.map(child => `/${child.replace('.md', '')}`) + data.children = children.map((child) => `/${child.replace('.md', '')}`) const newContents = readFrontmatter.stringify(content, data, { lineWidth: 10000 }) fs.writeFileSync(tocFile, newContents) - children.forEach(child => { + children.forEach((child) => { if (child.endsWith('.md')) return updateOrCreateToc(path.posix.join(directory, child)) }) diff --git a/script/content-migrations/add-tags-to-articles.js b/script/content-migrations/add-tags-to-articles.js index 6a69b156e0f7..0f1c12f6b34c 100755 --- a/script/content-migrations/add-tags-to-articles.js +++ b/script/content-migrations/add-tags-to-articles.js @@ -9,37 +9,37 @@ const XlsxPopulate = xXlsxPopulate // this is an optional dependency, install wi const START_ROW = 2 // Load an existing workbook -XlsxPopulate.fromFileAsync('./SanitizedInformationArchitecture.xlsx') - .then(workbook => { - const sheet = workbook.sheet('New content architecture') - - for (let row = START_ROW; sheet.row(row).cell(1).value() !== undefined; row++) { - const pageUrl = sheet.row(row).cell(1).hyperlink() - // article, learning path, or category - const contentStructure = sheet.row(row).cell(2).value() - // comma-separated keywords - const topics = sheet.row(row).cell(5).value() - - // The spreadsheet cell sometimes contains the string "null" - if (!topics || topics === 'null') continue - - // enterprise admin article urls will always include enterprise-server@3.0 - let fileName = pageUrl.replace('https://docs.github.com/en', 'content') - .replace('enterprise-server@3.0', '') - - // Only category files use the index.md format - if (contentStructure === 'article' || contentStructure === 'learning path') { - fileName = fileName + '.md' - } else { - fileName = fileName + '/index.md' - } - - const topicsArray = topics.split(',').map(topic => topic.trim()) || [] - updateFrontmatter(path.join(process.cwd(), fileName), topicsArray) +XlsxPopulate.fromFileAsync('./SanitizedInformationArchitecture.xlsx').then((workbook) => { + const sheet = workbook.sheet('New content architecture') + + for (let row = START_ROW; sheet.row(row).cell(1).value() !== undefined; row++) { + const pageUrl = sheet.row(row).cell(1).hyperlink() + // article, learning path, or category + const contentStructure = sheet.row(row).cell(2).value() + // comma-separated keywords + const topics = sheet.row(row).cell(5).value() + + // The spreadsheet cell sometimes contains the string "null" + if (!topics || topics === 'null') continue + + // enterprise admin article urls will always include enterprise-server@3.0 + let fileName = pageUrl + .replace('https://docs.github.com/en', 'content') + .replace('enterprise-server@3.0', '') + + // Only category files use the index.md format + if (contentStructure === 'article' || contentStructure === 'learning path') { + fileName = fileName + '.md' + } else { + fileName = fileName + '/index.md' } - }) -function updateFrontmatter (filePath, newTopics) { + const topicsArray = topics.split(',').map((topic) => topic.trim()) || [] + updateFrontmatter(path.join(process.cwd(), fileName), topicsArray) + } +}) + +function updateFrontmatter(filePath, newTopics) { const articleContents = fs.readFileSync(filePath, 'utf8') const { content, data } = readFrontmatter(articleContents) @@ -50,7 +50,7 @@ function updateFrontmatter (filePath, newTopics) { topics = topics.concat(data.topics) } - newTopics.forEach(topic => { + newTopics.forEach((topic) => { topics.push(topic) }) diff --git a/script/content-migrations/add_mini_toc_frontmatter.js b/script/content-migrations/add_mini_toc_frontmatter.js index e6639c350ba7..42a7a29f2101 100755 --- a/script/content-migrations/add_mini_toc_frontmatter.js +++ b/script/content-migrations/add_mini_toc_frontmatter.js @@ -7,18 +7,22 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) // [start-readme] // -// Run this one time script to add max mini toc +// Run this one time script to add max mini toc // to rest reference documentation // // [end-readme] - const re = /^---\n/gm -async function updateMdHeaders (dir) { +async function updateMdHeaders(dir) { walk(dir, { includeBasePath: true, directories: false }) - .filter(file => !file.endsWith('README.md') && !file.endsWith('index.md') && file.includes('content/rest/reference')) - .forEach(file => { + .filter( + (file) => + !file.endsWith('README.md') && + !file.endsWith('index.md') && + file.includes('content/rest/reference') + ) + .forEach((file) => { fs.readFile(file, 'utf8', (err, data) => { if (err) return console.error(err) const matchHeader = data.match(re)[1] @@ -27,7 +31,7 @@ async function updateMdHeaders (dir) { if (matchHeader) { result = data.replace(re, function (match) { t++ - return (t === 2) ? 'miniTocMaxHeadingLevel: 3\n---\n' : match + return t === 2 ? 'miniTocMaxHeadingLevel: 3\n---\n' : match }) } fs.writeFile(file, result, 'utf8', function (err) { @@ -37,7 +41,7 @@ async function updateMdHeaders (dir) { }) } -async function main () { +async function main() { await updateMdHeaders(path.join(__dirname, '../../content')) } diff --git a/script/content-migrations/comment-on-open-prs.js b/script/content-migrations/comment-on-open-prs.js index e7b2c6da1513..dc79edc02a31 100755 --- a/script/content-migrations/comment-on-open-prs.js +++ b/script/content-migrations/comment-on-open-prs.js @@ -1,7 +1,6 @@ #!/usr/bin/env node import { listPulls, createIssueComment } from '../helpers/git-utils.js' - // [start-readme] // // This script finds all open PRs from active branches that touch content files, and adds a comment @@ -18,7 +17,7 @@ if (!process.env.GITHUB_TOKEN) { const options = { owner: 'github', - repo: 'docs-internal' + repo: 'docs-internal', } const comment = ` @@ -38,17 +37,19 @@ For a 5min demo of what the scripts do and why they're needed, check out [this s main() -async function main () { +async function main() { const allPulls = await listPulls(options.owner, options.repo) // get the number of open PRs only const openPullNumbers = allPulls - .filter(pull => pull.state === 'open') - .map(pull => pull.number) + .filter((pull) => pull.state === 'open') + .map((pull) => pull.number) // for every open PR, create a review comment - await Promise.all(openPullNumbers.map(async (pullNumber) => { - await createIssueComment(options.owner, options.repo, pullNumber, comment) - console.log(`Added a comment to PR #${pullNumber}`) - })) + await Promise.all( + openPullNumbers.map(async (pullNumber) => { + await createIssueComment(options.owner, options.repo, pullNumber, comment) + console.log(`Added a comment to PR #${pullNumber}`) + }) + ) } diff --git a/script/content-migrations/create-csv-of-short-titles.js b/script/content-migrations/create-csv-of-short-titles.js index 486ba7128de2..073e300a940f 100755 --- a/script/content-migrations/create-csv-of-short-titles.js +++ b/script/content-migrations/create-csv-of-short-titles.js @@ -7,15 +7,18 @@ import readFrontmatter from '../../lib/read-frontmatter.js' const csvFile = path.join(process.cwd(), 'shortTitles.csv') fs.writeFileSync(csvFile, 'Product,Article Title,Short title,Relative path\n') -const files = walk(path.join(process.cwd(), 'content'), { includeBasePath: true, directories: false }) -files.forEach(file => { +const files = walk(path.join(process.cwd(), 'content'), { + includeBasePath: true, + directories: false, +}) +files.forEach((file) => { const relativeFilePath = file.replace(process.cwd(), '') const productName = relativeFilePath.split('/')[2] const fileContent = fs.readFileSync(file, 'utf8') const { data } = readFrontmatter(fileContent) const { title, shortTitle } = data - + if (title && !shortTitle && title.length > 25) { fs.appendFileSync(csvFile, `"${productName}","${title}",,${relativeFilePath}\n`) } diff --git a/script/content-migrations/deduplicate-enterprise-assets.js b/script/content-migrations/deduplicate-enterprise-assets.js index d278c0244b3c..456cc783c32e 100755 --- a/script/content-migrations/deduplicate-enterprise-assets.js +++ b/script/content-migrations/deduplicate-enterprise-assets.js @@ -16,15 +16,15 @@ const enterpriseAssetDirectories = [ '/assets/enterprise/github-ae', '/assets/enterprise/2.22', '/assets/enterprise/2.21', - '/assets/enterprise/2.20' + '/assets/enterprise/2.20', ] -async function main () { +async function main() { for (const directory of enterpriseAssetDirectories) { const fullDirectoryPath = path.join(process.cwd(), directory) const files = walk(fullDirectoryPath, { includeBasePath: true, - directories: false + directories: false, }) for (const file of files) { @@ -54,8 +54,10 @@ async function main () { } else { const existingImageToCompare = await fs.readFileSync(existingFileToCompare) const enterpriseImage = await fs.readFileSync(file) - compareResult = Buffer.compare(Buffer.from(existingImageToCompare), - Buffer.from(enterpriseImage)) + compareResult = Buffer.compare( + Buffer.from(existingImageToCompare), + Buffer.from(enterpriseImage) + ) } } catch (err) { console.log(file) diff --git a/script/content-migrations/extended-markdown-tags.js b/script/content-migrations/extended-markdown-tags.js index 25b6b06ad7fb..b47c681c89cf 100755 --- a/script/content-migrations/extended-markdown-tags.js +++ b/script/content-migrations/extended-markdown-tags.js @@ -5,10 +5,9 @@ import walk from 'walk-sync' import replace from 'replace' const __dirname = path.dirname(fileURLToPath(import.meta.url)) - const FINDER = /{{\s?([#/])([a-z-]+)?\s?}}/g -async function rewriteFiles (dir) { +async function rewriteFiles(dir) { const files = walk(dir, { includeBasePath: true }) replace({ regex: FINDER, @@ -24,15 +23,15 @@ async function rewriteFiles (dir) { } }, paths: files, - recursive: true + recursive: true, }) } -async function main () { +async function main() { const dirs = [ path.join(__dirname, '../../content'), path.join(__dirname, '../../data'), - path.join(__dirname, '../../translations') + path.join(__dirname, '../../translations'), ] for (const dir of dirs) { diff --git a/script/content-migrations/move-unique-image-assets.js b/script/content-migrations/move-unique-image-assets.js index 1695e74010c0..80d08cc5897c 100755 --- a/script/content-migrations/move-unique-image-assets.js +++ b/script/content-migrations/move-unique-image-assets.js @@ -3,23 +3,25 @@ import fs from 'fs' import path from 'path' import walk from 'walk-sync' - // iterate through enterprise images from most recent to oldest // for each asset and move any images from /assets/enterprise, // with file paths that don't already exist, to the /assets/images // directory. Then the existing Markdown will just work. -async function main () { +async function main() { const directories = [ path.join('assets/enterprise/3.0'), path.join('assets/enterprise/github-ae'), path.join('assets/enterprise/2.22'), path.join('assets/enterprise/2.21'), - path.join('assets/enterprise/2.20') + path.join('assets/enterprise/2.20'), ] for (const directory of directories) { - const files = walk(path.join(process.cwd(), directory), { includeBasePath: true, directories: false }) + const files = walk(path.join(process.cwd(), directory), { + includeBasePath: true, + directories: false, + }) for (const file of files) { // get the /assets/images path from the enterprise asset path diff --git a/script/content-migrations/octicon-tag.js b/script/content-migrations/octicon-tag.js index bed49eb59ade..d5867eea8c6a 100755 --- a/script/content-migrations/octicon-tag.js +++ b/script/content-migrations/octicon-tag.js @@ -5,10 +5,9 @@ import walk from 'walk-sync' import replace from 'replace' const __dirname = path.dirname(fileURLToPath(import.meta.url)) - const FINDER = /{{\s?octicon-([a-z-]+)(\s[\w\s\d-]+)?\s?}}/g -async function rewriteFiles (dir) { +async function rewriteFiles(dir) { const files = walk(dir, { includeBasePath: true }) replace({ regex: FINDER, @@ -20,15 +19,15 @@ async function rewriteFiles (dir) { } }, paths: files, - recursive: true + recursive: true, }) } -async function main () { +async function main() { const dirs = [ path.join(__dirname, '../../content'), path.join(__dirname, '../../data'), - path.join(__dirname, '../../translations') + path.join(__dirname, '../../translations'), ] for (const dir of dirs) { diff --git a/script/content-migrations/remove-html-comments-from-index-files.js b/script/content-migrations/remove-html-comments-from-index-files.js index af0b62d6ac75..5083bea2a91c 100755 --- a/script/content-migrations/remove-html-comments-from-index-files.js +++ b/script/content-migrations/remove-html-comments-from-index-files.js @@ -7,9 +7,8 @@ const contentDir = path.join(process.cwd(), 'content') // remove legacy commented out conditionals in index.md files walk(contentDir, { includeBasePath: true, directories: false }) - .filter(file => file.endsWith('index.md')) - .forEach(file => { - const newContents = fs.readFileSync(file, 'utf8') - .replace(/\n<!-- (if|endif) .*?-->/g, '') + .filter((file) => file.endsWith('index.md')) + .forEach((file) => { + const newContents = fs.readFileSync(file, 'utf8').replace(/\n<!-- (if|endif) .*?-->/g, '') fs.writeFileSync(file, newContents) }) diff --git a/script/content-migrations/remove-map-topics.js b/script/content-migrations/remove-map-topics.js index 70f5af139d5b..ffd4cc56655b 100755 --- a/script/content-migrations/remove-map-topics.js +++ b/script/content-migrations/remove-map-topics.js @@ -7,24 +7,27 @@ import languages from '../../lib/languages.js' import frontmatter from '../../lib/read-frontmatter.js' import addRedirectToFrontmatter from '../helpers/add-redirect-to-frontmatter.js' - const relativeRefRegex = /\/[a-zA-Z0-9-]+/g const linkString = /{% [^}]*?link.*? \/(.*?) ?%}/m const linksArray = new RegExp(linkString.source, 'gm') const walkOpts = { includeBasePath: true, - directories: false + directories: false, } // We only want category TOC files, not product TOCs. const categoryFileRegex = /content\/[^/]+?\/[^/]+?\/index.md/ -const fullDirectoryPaths = Object.values(languages).map(langObj => path.join(process.cwd(), langObj.dir, 'content')) -const categoryIndexFiles = fullDirectoryPaths.map(fullDirectoryPath => walk(fullDirectoryPath, walkOpts)).flat() - .filter(file => categoryFileRegex.test(file)) +const fullDirectoryPaths = Object.values(languages).map((langObj) => + path.join(process.cwd(), langObj.dir, 'content') +) +const categoryIndexFiles = fullDirectoryPaths + .map((fullDirectoryPath) => walk(fullDirectoryPath, walkOpts)) + .flat() + .filter((file) => categoryFileRegex.test(file)) -categoryIndexFiles.forEach(categoryIndexFile => { +categoryIndexFiles.forEach((categoryIndexFile) => { let categoryIndexContent = fs.readFileSync(categoryIndexFile, 'utf8') if (categoryIndexFile.endsWith('github/getting-started-with-github/index.md')) { @@ -39,7 +42,7 @@ categoryIndexFiles.forEach(categoryIndexFile => { let currentTopic = '' // Create an object of topics and articles - rawItems.forEach(tocItem => { + rawItems.forEach((tocItem) => { const relativePath = tocItem.match(relativeRefRegex).pop().replace('/', '') if (tocItem.includes('topic_link_in_list')) { currentTopic = relativePath @@ -68,12 +71,15 @@ categoryIndexFiles.forEach(categoryIndexFile => { const articles = pageToc[topic] - articles.forEach(article => { + articles.forEach((article) => { // Update the new map topic index file content topicContent = topicContent + `{% link_with_intro /${article} %}\n` // Update the category index file content - categoryIndexContent = categoryIndexContent.replace(`{% link_in_list /${article}`, `{% link_in_list /${topic}/${article}`) + categoryIndexContent = categoryIndexContent.replace( + `{% link_in_list /${article}`, + `{% link_in_list /${topic}/${article}` + ) // Early return if the article doesn't exist (some translated category TOCs may be outdated and contain incorrect links) if (!fs.existsSync(`${oldTopicDirectory}/${article}.md`)) return @@ -86,14 +92,25 @@ categoryIndexFiles.forEach(categoryIndexFile => { const articleContents = frontmatter(fs.readFileSync(newArticlePath, 'utf8')) if (!articleContents.data.redirect_from) articleContents.data.redirect_from = [] - addRedirectToFrontmatter(articleContents.data.redirect_from, `${oldTopicDirectory.replace(/^.*?\/content\//, '/')}/${article}`) + addRedirectToFrontmatter( + articleContents.data.redirect_from, + `${oldTopicDirectory.replace(/^.*?\/content\//, '/')}/${article}` + ) // Write the article with updated frontmatter - fs.writeFileSync(newArticlePath, frontmatter.stringify(articleContents.content.trim(), articleContents.data, { lineWidth: 10000 })) + fs.writeFileSync( + newArticlePath, + frontmatter.stringify(articleContents.content.trim(), articleContents.data, { + lineWidth: 10000, + }) + ) }) // Write the map topic index file - fs.writeFileSync(`${newTopicDirectory}/index.md`, frontmatter.stringify(topicContent.trim(), data, { lineWidth: 10000 })) + fs.writeFileSync( + `${newTopicDirectory}/index.md`, + frontmatter.stringify(topicContent.trim(), data, { lineWidth: 10000 }) + ) // Write the category index file fs.writeFileSync(categoryIndexFile, categoryIndexContent) diff --git a/script/content-migrations/remove-unused-assets.js b/script/content-migrations/remove-unused-assets.js index 3d89986e8da8..b0a8423723d2 100755 --- a/script/content-migrations/remove-unused-assets.js +++ b/script/content-migrations/remove-unused-assets.js @@ -10,13 +10,12 @@ import { supported } from '../../lib/enterprise-server-releases.js' import semver from 'semver' const __dirname = path.dirname(fileURLToPath(import.meta.url)) - const imagesPath = [ '/assets/enterprise/3.0', '/assets/enterprise/github-ae', '/assets/enterprise/2.22', '/assets/enterprise/2.21', - '/assets/enterprise/2.20' + '/assets/enterprise/2.20', ] // these paths should remain in the repo even if they are not referenced directly @@ -24,21 +23,14 @@ const ignoreList = [ '/assets/images/help/site-policy', '/assets/images/site', '/assets/images/octicons', - '/assets/fonts' + '/assets/fonts', ] // search these dirs for images or data references // content files are handled separately -const dirsToGrep = [ - 'includes', - 'layouts', - 'javascripts', - 'stylesheets', - 'README.md', - 'data' -] +const dirsToGrep = ['includes', 'layouts', 'javascripts', 'stylesheets', 'README.md', 'data'] -async function main () { +async function main() { const pages = await getEnglishPages() // step 1. find assets referenced in content by searching page markdown @@ -54,13 +46,22 @@ async function main () { // and remove assets that are referenced in any files for (const directory of imagesPath) { const allImagesInDir = await getAllAssetsInDirectory(directory) - await removeEnterpriseImages(markdownImageData, allImagesInDir, assetsReferencedInNonContentDirs, directory) + await removeEnterpriseImages( + markdownImageData, + allImagesInDir, + assetsReferencedInNonContentDirs, + directory + ) } // step 5. find all assets that exist in the /assets/images directory // and remove assets that are referenced in any files const allDotcomImagesInDir = await getAllAssetsInDirectory('/assets/images') - await removeUnusedDotcomImages(markdownImageData, allDotcomImagesInDir, assetsReferencedInNonContentDirs) + await removeUnusedDotcomImages( + markdownImageData, + allDotcomImagesInDir, + assetsReferencedInNonContentDirs + ) } // Returns an object of all the images referenced in Markdown @@ -73,7 +74,7 @@ async function main () { // '/assets/images/foo/bar.png': { 'enterprise-server': '<=2.22'}, // '/assets/images/bar/foo/png': { 'github-ae': '*'} // } -async function getMarkdownImageData (pages) { +async function getMarkdownImageData(pages) { const imageData = {} // loop through each page and get all /assets/images references from Markdown @@ -94,7 +95,10 @@ async function getMarkdownImageData (pages) { // or values need to be added or updated for (const pageVersion in page.versions) { const imageVersions = imageData[imagePath] - const versionAlreadyExists = Object.prototype.hasOwnProperty.call(imageVersions, pageVersion) + const versionAlreadyExists = Object.prototype.hasOwnProperty.call( + imageVersions, + pageVersion + ) const existingVersionRangeIsAll = imageVersions[pageVersion] === '*' if (!versionAlreadyExists) { @@ -123,42 +127,45 @@ async function getMarkdownImageData (pages) { return imageData } -async function getEnglishPages () { +async function getEnglishPages() { const pages = await loadPages() - return pages.filter(page => page.languageCode === 'en') + return pages.filter((page) => page.languageCode === 'en') } -async function getAllAssetsInDirectory (directory) { - return walk(path.join(process.cwd(), directory), { directories: false }) - .map(relPath => path.join(directory, relPath)) +async function getAllAssetsInDirectory(directory) { + return walk(path.join(process.cwd(), directory), { directories: false }).map((relPath) => + path.join(directory, relPath) + ) } -async function getAssetsReferencedInNonContentDirs () { +async function getAssetsReferencedInNonContentDirs() { const regex = patterns.imagePath.source const grepCmd = `egrep -rh '${regex}' ${dirsToGrep.join(' ')}` const grepResults = execSync(grepCmd).toString() return await getImageReferencesOnPage(grepResults) } -async function getImageReferencesOnPage (text) { - return (text.match(patterns.imagePath) || []) - .map(ref => { - return ref - .replace(/\.\.\//g, '') - .trim() - }) +async function getImageReferencesOnPage(text) { + return (text.match(patterns.imagePath) || []).map((ref) => { + return ref.replace(/\.\.\//g, '').trim() + }) } // loop through images referenced in Markdown and check whether the image // is only referenced in pages versioned for free-pro-team. If the image // is only used on free-pro-team pages, then it shouldn't exist in the // assets/enterprise directory. -function removeDotcomOnlyImagesFromEnterprise (markdownImageData) { +function removeDotcomOnlyImagesFromEnterprise(markdownImageData) { for (const image in markdownImageData) { const imageVersions = markdownImageData[image] if (!Object.prototype.hasOwnProperty.call(imageVersions, 'enterprise-server')) { - supported.forEach(enterpriseReleaseNumber => { - const imagePath = path.join(__dirname, '../..', `/assets/enterprise/${enterpriseReleaseNumber}`, image) + supported.forEach((enterpriseReleaseNumber) => { + const imagePath = path.join( + __dirname, + '../..', + `/assets/enterprise/${enterpriseReleaseNumber}`, + image + ) if (fs.existsSync(imagePath)) fs.unlinkSync(imagePath) }) } @@ -172,7 +179,12 @@ function removeDotcomOnlyImagesFromEnterprise (markdownImageData) { // loop through each image in a directory under /assets/enterprise // and check the image's version to determine if the image should be // removed from the directory -async function removeEnterpriseImages (markdownImageData, directoryImageList, assetsReferencedInNonContentDirs, directory) { +async function removeEnterpriseImages( + markdownImageData, + directoryImageList, + assetsReferencedInNonContentDirs, + directory +) { const directoryVersion = directory.split('/').pop() for (const directoryImage of directoryImageList) { // get the asset's format that is stored in the markdownImageData object @@ -192,30 +204,42 @@ async function removeEnterpriseImages (markdownImageData, directoryImageList, as } // if the asset is in Markdown but is not used on GitHub AE pages, remove it - if (directoryVersion === 'github-ae' && - !Object.prototype.hasOwnProperty.call(imageVersions, 'github-ae')) { + if ( + directoryVersion === 'github-ae' && + !Object.prototype.hasOwnProperty.call(imageVersions, 'github-ae') + ) { fs.unlinkSync(imageFullPath) continue - // if the asset is in Markdown but is not used on a page versioned for the - // directoryVersion (i.e., GHES release number), remove it + // if the asset is in Markdown but is not used on a page versioned for the + // directoryVersion (i.e., GHES release number), remove it } - if (directoryVersion !== 'github-ae' && - !Object.prototype.hasOwnProperty.call(imageVersions, 'enterprise-server')) { + if ( + directoryVersion !== 'github-ae' && + !Object.prototype.hasOwnProperty.call(imageVersions, 'enterprise-server') + ) { fs.unlinkSync(imageFullPath) continue } - if (directoryVersion !== 'github-ae' && semver.lt( - semver.coerce(directoryVersion), - semver.coerce(imageVersions['enterprise-server'].replace('*', 0.0)))) { + if ( + directoryVersion !== 'github-ae' && + semver.lt( + semver.coerce(directoryVersion), + semver.coerce(imageVersions['enterprise-server'].replace('*', 0.0)) + ) + ) { fs.unlinkSync(imageFullPath) } } } // loop through each file in /assets/images and check if -async function removeUnusedDotcomImages (markdownImageData, directoryImageList, assetsReferencedInNonContentDirs) { +async function removeUnusedDotcomImages( + markdownImageData, + directoryImageList, + assetsReferencedInNonContentDirs +) { for (const directoryImage of directoryImageList) { - if (ignoreList.find(ignored => directoryImage.startsWith(ignored))) continue + if (ignoreList.find((ignored) => directoryImage.startsWith(ignored))) continue // if the image is in a non content file (i.e., javascript or data file) // we don't have the page version info so assume it's used in all versions diff --git a/script/content-migrations/site-data-tag.js b/script/content-migrations/site-data-tag.js index 51a3b4de26d4..3cc9aaed7681 100755 --- a/script/content-migrations/site-data-tag.js +++ b/script/content-migrations/site-data-tag.js @@ -5,27 +5,26 @@ import walk from 'walk-sync' import replace from 'replace' const __dirname = path.dirname(fileURLToPath(import.meta.url)) - const FINDER = /{{\s?site\.data\.([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)+)\s*}}/g const REPLACER = '{% data $1 %}' -async function rewriteFiles (dir) { +async function rewriteFiles(dir) { const files = walk(dir, { includeBasePath: true }) replace({ regex: FINDER, replacement: REPLACER, paths: files, - recursive: true + recursive: true, }) } -async function main () { +async function main() { const dirs = [ path.join(__dirname, '../../content'), path.join(__dirname, '../../data'), path.join(__dirname, '../../translations'), path.join(__dirname, '../../includes'), - path.join(__dirname, '../../layouts') + path.join(__dirname, '../../layouts'), ] for (const dir of dirs) { diff --git a/script/content-migrations/topics-upcase.js b/script/content-migrations/topics-upcase.js index 640f65d0b3f4..ad43a1495248 100755 --- a/script/content-migrations/topics-upcase.js +++ b/script/content-migrations/topics-upcase.js @@ -5,25 +5,27 @@ import walk from 'walk-sync' import readFrontmatter from '../../lib/read-frontmatter.js' import allowTopics from '../../data/allowed-topics.js' - // key is the downcased valued for comparison // value is the display value with correct casing const topicLookupObject = {} -allowTopics.forEach(topic => { +allowTopics.forEach((topic) => { const lowerCaseTopic = topic.toLowerCase() topicLookupObject[lowerCaseTopic] = topic }) -const files = walk(path.join(process.cwd(), 'content'), { includeBasePath: true, directories: false }) -files.forEach(file => { +const files = walk(path.join(process.cwd(), 'content'), { + includeBasePath: true, + directories: false, +}) +files.forEach((file) => { const fileContent = fs.readFileSync(file, 'utf8') const { content, data } = readFrontmatter(fileContent) if (data.topics === undefined) return - const topics = data.topics.map(elem => elem.toLowerCase()) + const topics = data.topics.map((elem) => elem.toLowerCase()) const newTopics = [] - topics.forEach(topic => { + topics.forEach((topic) => { // for each topic in the markdown file, lookup the display value // and add it to a new array newTopics.push(topicLookupObject[topic]) diff --git a/script/content-migrations/update-developer-site-links.js b/script/content-migrations/update-developer-site-links.js index eeff5579ba83..7e2dcab14b34 100755 --- a/script/content-migrations/update-developer-site-links.js +++ b/script/content-migrations/update-developer-site-links.js @@ -11,19 +11,23 @@ import xAllVersions from '../../lib/all-versions.js' const allVersions = Object.keys(xAllVersions) // get all content and data files -const files = ['content', 'data'].map(dir => { - return walk(path.join(process.cwd(), dir), { includeBasePath: true, directories: false }) - .filter(file => file.endsWith('.md') && !file.endsWith('README.md')) -}).flat() +const files = ['content', 'data'] + .map((dir) => { + return walk(path.join(process.cwd(), dir), { + includeBasePath: true, + directories: false, + }).filter((file) => file.endsWith('.md') && !file.endsWith('README.md')) + }) + .flat() // match [foo](/v3) and [bar](/v4) Markdown links const linkRegex = /\(\/v[34].*?\)/g main() -async function main () { +async function main() { // we need to load the pages so we can get the redirects - const englishPages = (await loadPages()).filter(p => p.languageCode === 'en') + const englishPages = (await loadPages()).filter((p) => p.languageCode === 'en') const englishPageMap = await loadPageMap(englishPages) const redirects = await loadRedirects(englishPages, englishPageMap) @@ -35,8 +39,7 @@ async function main () { // remove parentheses: (/v3) -> /v3 // also remove trailing slash before closing parens if there is one - const devLinks = links - .map(link => link.replace('(', '').replace(/\/?\)/, '')) + const devLinks = links.map((link) => link.replace('(', '').replace(/\/?\)/, '')) let newContent = content @@ -60,14 +63,11 @@ async function main () { // re-add the fragment after removing any fragment added via the redirect // otherwise /v3/git/refs/#create-a-reference will become /rest/reference/git#refs#create-a-reference // we want to preserve the #create-a-reference fragment, not #refs - const newLink = fragment - ? redirect.replace(/#.+?$/, '') + '#' + fragment - : redirect + const newLink = fragment ? redirect.replace(/#.+?$/, '') + '#' + fragment : redirect // first replace the old link with the new link // then remove any trailing slashes - newContent = newContent - .replace(new RegExp(`${devLink}/?(?=\\))`), newLink) + newContent = newContent.replace(new RegExp(`${devLink}/?(?=\\))`), newLink) } fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) diff --git a/script/content-migrations/update-headers.js b/script/content-migrations/update-headers.js index c82d3a7a2e8c..13bdd3784e64 100755 --- a/script/content-migrations/update-headers.js +++ b/script/content-migrations/update-headers.js @@ -12,17 +12,16 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) // // [end-readme] - const re = /^#.*\n/gm -async function updateMdHeaders (dir) { +async function updateMdHeaders(dir) { walk(dir, { includeBasePath: true, directories: false }) - .filter(file => !file.endsWith('README.md') && !file.includes('content/rest/reference')) - .forEach(file => { + .filter((file) => !file.endsWith('README.md') && !file.includes('content/rest/reference')) + .forEach((file) => { fs.readFile(file, 'utf8', (err, data) => { if (err) return console.error(err) const matchHeader = data.match(re) - let firstHeader = (matchHeader) ? matchHeader[0].split(' ')[0] : null + let firstHeader = matchHeader ? matchHeader[0].split(' ')[0] : null if (firstHeader) { for (let index = 1; index < matchHeader.length; index++) { const nextHeader = matchHeader[index].split(' ')[0] @@ -33,7 +32,10 @@ async function updateMdHeaders (dir) { } } if (file.includes('data/reusables/')) { - if (!file.endsWith('data/reusables/actions/actions-group-concurrency.md') && !file.endsWith('data/reusables/github-actions/actions-on-examples.md')) { + if ( + !file.endsWith('data/reusables/actions/actions-group-concurrency.md') && + !file.endsWith('data/reusables/github-actions/actions-on-examples.md') + ) { firstHeader = 'reusable-' + firstHeader } } @@ -58,18 +60,13 @@ async function updateMdHeaders (dir) { .replace(/^###### /gm, '#### ') break case 'reusable-####': - result = data - .replace(/^#### /gm, '### ') - .replace(/^##### /gm, '#### ') + result = data.replace(/^#### /gm, '### ').replace(/^##### /gm, '#### ') break case 'reusable-#####': - result = data - .replace(/^##### /gm, '#### ') + result = data.replace(/^##### /gm, '#### ') break case '#####': - result = data - .replace(/^##### /gm, '### ') - .replace(/^###### /gm, '#### ') + result = data.replace(/^##### /gm, '### ').replace(/^###### /gm, '#### ') break default: return @@ -81,10 +78,10 @@ async function updateMdHeaders (dir) { }) } -async function main () { +async function main() { const mdDirPaths = [ path.join(__dirname, '../../content'), - path.join(__dirname, '../../data/reusables') + path.join(__dirname, '../../data/reusables'), ] for (const dir of mdDirPaths) { diff --git a/script/content-migrations/update-short-titles-from-csv.js b/script/content-migrations/update-short-titles-from-csv.js index 62ba2927454e..f05ebccff81b 100755 --- a/script/content-migrations/update-short-titles-from-csv.js +++ b/script/content-migrations/update-short-titles-from-csv.js @@ -5,7 +5,6 @@ import readFrontmatter from '../../lib/read-frontmatter.js' import csv from 'csv-parse' import { exit } from 'process' - main() async function main() { @@ -14,7 +13,7 @@ async function main() { const csvFileName = 'shortTitles.csv' const filePath = path.join(process.cwd(), csvFileName) const reader = fs.createReadStream(filePath) - + // Parse each row of the csv reader .pipe(csv()) @@ -29,38 +28,41 @@ async function main() { } }) .on('end', () => { - console.log(`โญ Completed updating the shortTitle frontmatter.\nUpdated ${fileCounter} files.`) + console.log( + `โญ Completed updating the shortTitle frontmatter.\nUpdated ${fileCounter} files.` + ) }) } async function updateFrontmatter(csvData) { - const filePath = path.join(process.cwd(), csvData[4]) const fileContent = fs.readFileSync(filePath, 'utf8') const { content, data } = readFrontmatter(fileContent) - data.shortTitle = csvData[3] + data.shortTitle = csvData[3] const newContents = readFrontmatter.stringify(content, data, { lineWidth: 10000 }) fs.writeFileSync(filePath, newContents) - } -// Ensure the columns being read out are in the location expected +// Ensure the columns being read out are in the location expected async function verifyHeader(csvData) { - const csvHeader = [] - csvData.forEach(element => { + csvData.forEach((element) => { csvHeader.push(element) }) if (csvHeader[3] !== 'Short title') { - console.log(`The CSV headers are malformed. Expected to see column 3 contain the header 'Short title'`) + console.log( + `The CSV headers are malformed. Expected to see column 3 contain the header 'Short title'` + ) exit(1) } if (csvHeader[4] !== 'Relative path') { - console.log(`The CSV headers are malformed. Expected to see column 4 contain the header 'Relative path'`) + console.log( + `The CSV headers are malformed. Expected to see column 4 contain the header 'Relative path'` + ) exit(1) } return csvHeader -} \ No newline at end of file +} diff --git a/script/content-migrations/update-tocs.js b/script/content-migrations/update-tocs.js index a8da2b440bce..b12ca15f0226 100755 --- a/script/content-migrations/update-tocs.js +++ b/script/content-migrations/update-tocs.js @@ -21,80 +21,92 @@ const linksArray = new RegExp(linkString.source, 'gm') const walkOpts = { includeBasePath: true, - directories: false + directories: false, } -const fullDirectoryPaths = Object.values(languages).map(langObj => path.join(process.cwd(), langObj.dir, 'content')) -const indexFiles = fullDirectoryPaths.map(fullDirectoryPath => walk(fullDirectoryPath, walkOpts)).flat() - .filter(file => file.endsWith('index.md')) +const fullDirectoryPaths = Object.values(languages).map((langObj) => + path.join(process.cwd(), langObj.dir, 'content') +) +const indexFiles = fullDirectoryPaths + .map((fullDirectoryPath) => walk(fullDirectoryPath, walkOpts)) + .flat() + .filter((file) => file.endsWith('index.md')) const englishHomepageData = { children: '', - externalProducts: '' + externalProducts: '', } -indexFiles - .forEach(indexFile => { - const relativePath = indexFile.replace(/^.+\/content\//, '') - const documentType = getDocumentType(relativePath) - - const { data, content } = frontmatter(fs.readFileSync(indexFile, 'utf8')) - - // Save the English homepage frontmatter props... - if (documentType === 'homepage' && !indexFile.includes('/translations/')) { - englishHomepageData.children = data.children - englishHomepageData.externalProducts = data.externalProducts - } - - // ...and reuse them in the translated homepages, in case the translated files are out of date - if (documentType === 'homepage' && indexFile.includes('/translations/')) { - data.children = englishHomepageData.children - data.externalProducts = englishHomepageData.externalProducts - } - - const linkItems = content.match(linksArray) - if (!linkItems) return - - // Turn the `{% link /<link> %}` list into an array of /<link> items - if (documentType === 'product' || documentType === 'mapTopic') { - data.children = getLinks(linkItems) - } - - if (documentType === 'category') { - const childMapTopics = linkItems.filter(item => item.includes('topic_')) - - data.children = childMapTopics.length ? getLinks(childMapTopics) : getLinks(linkItems) - } - - // Fix this one weird file - if (relativePath === 'discussions/guides/index.md') { - data.children = [ - '/best-practices-for-community-conversations-on-github', - '/finding-discussions-across-multiple-repositories', - '/granting-higher-permissions-to-top-contributors' - ] - } - - // Remove the Table of Contents section and leave any body text before it. - let newContent = content - .replace(/^#*? Table of contents[\s\S]*/im, '') - .replace('<div hidden>', '') - .replace(linksArray, '') - - const linesArray = newContent - .split('\n') - - const newLinesArray = linesArray - .filter((line, index) => /\S/.test(line) || (extendedMarkdownTags.find(tag => (linesArray[index - 1] && linesArray[index - 1].includes(tag)) || (linesArray[index + 1] && linesArray[index + 1].includes(tag))))) - .filter(line => !/^<!--\s+?-->$/m.test(line)) - - newContent = newLinesArray.join('\n') - - // Index files should no longer have body content, so we write an empty string - fs.writeFileSync(indexFile, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) - }) - -function getLinks (linkItemArray) { +indexFiles.forEach((indexFile) => { + const relativePath = indexFile.replace(/^.+\/content\//, '') + const documentType = getDocumentType(relativePath) + + const { data, content } = frontmatter(fs.readFileSync(indexFile, 'utf8')) + + // Save the English homepage frontmatter props... + if (documentType === 'homepage' && !indexFile.includes('/translations/')) { + englishHomepageData.children = data.children + englishHomepageData.externalProducts = data.externalProducts + } + + // ...and reuse them in the translated homepages, in case the translated files are out of date + if (documentType === 'homepage' && indexFile.includes('/translations/')) { + data.children = englishHomepageData.children + data.externalProducts = englishHomepageData.externalProducts + } + + const linkItems = content.match(linksArray) + if (!linkItems) return + + // Turn the `{% link /<link> %}` list into an array of /<link> items + if (documentType === 'product' || documentType === 'mapTopic') { + data.children = getLinks(linkItems) + } + + if (documentType === 'category') { + const childMapTopics = linkItems.filter((item) => item.includes('topic_')) + + data.children = childMapTopics.length ? getLinks(childMapTopics) : getLinks(linkItems) + } + + // Fix this one weird file + if (relativePath === 'discussions/guides/index.md') { + data.children = [ + '/best-practices-for-community-conversations-on-github', + '/finding-discussions-across-multiple-repositories', + '/granting-higher-permissions-to-top-contributors', + ] + } + + // Remove the Table of Contents section and leave any body text before it. + let newContent = content + .replace(/^#*? Table of contents[\s\S]*/im, '') + .replace('<div hidden>', '') + .replace(linksArray, '') + + const linesArray = newContent.split('\n') + + const newLinesArray = linesArray + .filter( + (line, index) => + /\S/.test(line) || + extendedMarkdownTags.find( + (tag) => + (linesArray[index - 1] && linesArray[index - 1].includes(tag)) || + (linesArray[index + 1] && linesArray[index + 1].includes(tag)) + ) + ) + .filter((line) => !/^<!--\s+?-->$/m.test(line)) + + newContent = newLinesArray.join('\n') + + // Index files should no longer have body content, so we write an empty string + fs.writeFileSync(indexFile, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) +}) + +function getLinks(linkItemArray) { // do a oneoff replacement while mapping - return linkItemArray.map(item => item.match(linkString)[1].replace('/discussions-guides', '/guides')) + return linkItemArray.map((item) => + item.match(linkString)[1].replace('/discussions-guides', '/guides') + ) } diff --git a/script/content-migrations/use-short-versions.js b/script/content-migrations/use-short-versions.js index 03e999a8cd42..08fa7fae3ad5 100755 --- a/script/content-migrations/use-short-versions.js +++ b/script/content-migrations/use-short-versions.js @@ -12,8 +12,10 @@ const allVersions = Object.values(xAllVersions) const dryRun = ['-d', '--dry-run'].includes(process.argv[2]) const walkFiles = (pathToWalk, ext) => { - return walk(path.posix.join(process.cwd(), pathToWalk), { includeBasePath: true, directories: false }) - .filter(file => file.endsWith(ext) && !file.endsWith('README.md')) + return walk(path.posix.join(process.cwd(), pathToWalk), { + includeBasePath: true, + directories: false, + }).filter((file) => file.endsWith(ext) && !file.endsWith('README.md')) } const markdownFiles = walkFiles('content', '.md').concat(walkFiles('data', '.md')) @@ -22,26 +24,27 @@ const yamlFiles = walkFiles('data', '.yml') const operatorsMap = { // old: new '==': '=', - 'ver_gt': '>', - 'ver_lt': '<', - '!=': '!=' // noop + ver_gt: '>', + ver_lt: '<', + '!=': '!=', // noop } // [start-readme] // -// Run this script to convert long form Liquid conditionals (e.g., {% if currentVersion == "free-pro-team" %}) to +// Run this script to convert long form Liquid conditionals (e.g., {% if currentVersion == "free-pro-team" %}) to // the new custom tag (e.g., {% ifversion fpt %}) and also use the short names in versions frontmatter. // // [end-readme] -async function main () { - if (dryRun) console.log('This is a dry run! The script will not write any files. Use for debugging.\n') +async function main() { + if (dryRun) + console.log('This is a dry run! The script will not write any files. Use for debugging.\n') // 1. UPDATE MARKDOWN FILES (CONTENT AND REUSABLES) console.log('Updating Liquid conditionals and versions frontmatter in Markdown files...\n') for (const file of markdownFiles) { // A. UPDATE LIQUID CONDITIONALS IN CONTENT - // Create an { old: new } conditionals object so we can get the replacements and + // Create an { old: new } conditionals object so we can get the replacements and // make the replacements separately and not do both in nested loops. const content = fs.readFileSync(file, 'utf8') const contentReplacements = getLiquidReplacements(content, file) @@ -59,7 +62,9 @@ async function main () { .replace(/>=?2\.19/, '*') // Find the relevant version from the master list so we can access the short name. - const versionObj = allVersions.find(version => version.plan === plan || version.shortName === plan) + const versionObj = allVersions.find( + (version) => version.plan === plan || version.shortName === plan + ) if (!versionObj) { console.error(`can't find supported version for ${plan}`) process.exit(1) @@ -95,24 +100,25 @@ async function main () { } } -main() - .then( - () => { console.log('Done!') }, - (err) => { - console.error(err) - process.exit(1) - } - ) +main().then( + () => { + console.log('Done!') + }, + (err) => { + console.error(err) + process.exit(1) + } +) // Convenience function to help with readability by removing this large but unneded property. -function removeInputProps (arrayOfObjects) { - return arrayOfObjects.map(obj => { +function removeInputProps(arrayOfObjects) { + return arrayOfObjects.map((obj) => { delete obj.input || delete obj.token.input return obj }) } -function makeLiquidReplacements (replacementsObj, text) { +function makeLiquidReplacements(replacementsObj, text) { let newText = text Object.entries(replacementsObj).forEach(([oldCond, newCond]) => { const oldCondRegex = new RegExp(`({%-?)\\s*?${escapeRegExp(oldCond)}\\s*?(-?%})`, 'g') @@ -135,22 +141,25 @@ function makeLiquidReplacements (replacementsObj, text) { // if currentVersion ver_gt "myVersion@myRelease -> ifversion myVersionShort > myRelease // if currentVersion ver_lt "myVersion@myRelease -> ifversion myVersionShort < myRelease // if enterpriseServerVersions contains currentVersion -> ifversion ghes -function getLiquidReplacements (content, file) { +function getLiquidReplacements(content, file) { const replacements = {} const tokenizer = new Tokenizer(content) const tokens = removeInputProps(tokenizer.readTopLevelTokens()) - + tokens - .filter(token => (token.name === 'if' || token.name === 'elsif') && token.content.includes('currentVersion')) - .map(token => token.content) - .forEach(token => { + .filter( + (token) => + (token.name === 'if' || token.name === 'elsif') && token.content.includes('currentVersion') + ) + .map((token) => token.content) + .forEach((token) => { const newToken = token.startsWith('if') ? ['ifversion'] : ['elsif'] // Everything from here on pushes to the `newToken` array to construct the new conditional. token .replace(/(if|elsif) /, '') .split(/ (or|and) /) - .forEach(op => { + .forEach((op) => { if (op === 'or' || op === 'and') { newToken.push(op) return @@ -177,7 +186,7 @@ function getLiquidReplacements (content, file) { const [plan, release] = opParts[2].slice(1, -1).split('@') // Find the relevant version from the master list so we can access the short name. - const versionObj = allVersions.find(version => version.plan === plan) + const versionObj = allVersions.find((version) => version.plan === plan) if (!versionObj) { console.error(`Couldn't find a version for ${plan} in "${token}" in ${file}`) @@ -188,7 +197,9 @@ function getLiquidReplacements (content, file) { if (versionObj.hasNumberedReleases) { const newOperator = operatorsMap[operator] if (!newOperator) { - console.error(`Couldn't find an operator that corresponds to ${operator} in "${token} in "${file}`) + console.error( + `Couldn't find an operator that corresponds to ${operator} in "${token} in "${file}` + ) process.exit(1) } @@ -211,7 +222,8 @@ function getLiquidReplacements (content, file) { const lessThanOldestSupported = release === oldestSupported && newOperator === '<' // E.g., ghes = 2.20 const equalsDeprecated = deprecated.includes(release) && newOperator === '=' - const hasDeprecatedContent = lessThanDeprecated || lessThanOldestSupported || equalsDeprecated + const hasDeprecatedContent = + lessThanDeprecated || lessThanOldestSupported || equalsDeprecated // Remove these by hand. if (hasDeprecatedContent) { @@ -247,7 +259,7 @@ function getLiquidReplacements (content, file) { // Handle all other non-standard releases, like github-ae@next and github-ae@issue-12345 newToken.push(`${versionObj.shortName}-${release}`) }) - + replacements[token] = newToken.join(' ') }) diff --git a/script/create-glossary-from-spreadsheet.js b/script/create-glossary-from-spreadsheet.js index 4b432328d94c..597f6a85cb43 100755 --- a/script/create-glossary-from-spreadsheet.js +++ b/script/create-glossary-from-spreadsheet.js @@ -18,7 +18,7 @@ const glossary = yaml.load(fs.readFileSync(inputFile, 'utf8')) console.log(glossary) const external = [] const internal = [] -glossary.forEach(term => { +glossary.forEach((term) => { if (term.internal) { delete term.internal internal.push(term) @@ -27,12 +27,6 @@ glossary.forEach(term => { } }) -fs.writeFileSync( - path.join(__dirname, '../data/glossaries/internal.yml'), - yaml.dump(internal) -) +fs.writeFileSync(path.join(__dirname, '../data/glossaries/internal.yml'), yaml.dump(internal)) -fs.writeFileSync( - path.join(__dirname, '../data/glossaries/external.yml'), - yaml.dump(external) -) +fs.writeFileSync(path.join(__dirname, '../data/glossaries/external.yml'), yaml.dump(external)) diff --git a/script/deployment/create-staging-app-name.js b/script/deployment/create-staging-app-name.js index 2dafcaca95ce..ad9ef5feb1e9 100644 --- a/script/deployment/create-staging-app-name.js +++ b/script/deployment/create-staging-app-name.js @@ -5,13 +5,15 @@ const slugify = xGithubSlugger.slug const APP_NAME_MAX_LENGTH = 30 export default function ({ repo, pullNumber, branch }) { - return `${repo}-${pullNumber}--${slugify(branch)}` - // Shorten the string to the max allowed length - .slice(0, APP_NAME_MAX_LENGTH) - // Convert underscores to dashes - .replace(/_/g, '-') - // Remove trailing dashes - .replace(/-+$/, '') - // Make it all lowercase - .toLowerCase() + return ( + `${repo}-${pullNumber}--${slugify(branch)}` + // Shorten the string to the max allowed length + .slice(0, APP_NAME_MAX_LENGTH) + // Convert underscores to dashes + .replace(/_/g, '-') + // Remove trailing dashes + .replace(/-+$/, '') + // Make it all lowercase + .toLowerCase() + ) } diff --git a/script/deployment/deploy-to-staging.js b/script/deployment/deploy-to-staging.js index fb58ddc05761..326aba860af5 100644 --- a/script/deployment/deploy-to-staging.js +++ b/script/deployment/deploy-to-staging.js @@ -7,12 +7,12 @@ import createStagingAppName from './create-staging-app-name.js' const SLEEP_INTERVAL = 5000 const HEROKU_LOG_LINES_TO_SHOW = 25 -export default async function deployToStaging ({ +export default async function deployToStaging({ herokuToken, octokit, pullRequest, forceRebuild = false, - runId = null + runId = null, }) { // Start a timer so we can report how long the deployment takes const startTime = Date.now() @@ -23,15 +23,12 @@ export default async function deployToStaging ({ base: { repo: { name: repo, - owner: { login: owner } - } + owner: { login: owner }, + }, }, state, - head: { - ref: branch, - sha - }, - user: author + head: { ref: branch, sha }, + user: author, } = pullRequest // Verify the PR is still open @@ -78,7 +75,7 @@ export default async function deployToStaging ({ required_contexts: [], // Do not try to merge the base branch into the feature branch - auto_merge: false + auto_merge: false, }) console.log('GitHub Deployment created', deployment) @@ -95,21 +92,23 @@ export default async function deployToStaging ({ // the use of the `log_url`, `environment_url`, and `auto_inactive` parameters. // The 'flash' preview is required for `state` values of 'in_progress' and 'queued'. mediaType: { - previews: ['ant-man', 'flash'] - } + previews: ['ant-man', 'flash'], + }, }) console.log('๐Ÿš€ Deployment status: in_progress - Preparing to deploy the app...') // Get a URL for the tarballed source code bundle - const { headers: { location: tarballUrl } } = await octokit.repos.downloadTarballArchive({ + const { + headers: { location: tarballUrl }, + } = await octokit.repos.downloadTarballArchive({ owner, repo, ref: sha, // Override the underlying `node-fetch` module's `redirect` option // configuration to prevent automatically following redirects. request: { - redirect: 'manual' - } + redirect: 'manual', + }, }) // Time to talk to Heroku... @@ -135,7 +134,9 @@ export default async function deployToStaging ({ console.log(`Heroku app '${appName}' deleted for forced rebuild`) } catch (error) { - throw new Error(`Failed to delete Heroku app '${appName}' for forced rebuild. Error: ${error}`) + throw new Error( + `Failed to delete Heroku app '${appName}' for forced rebuild. Error: ${error}` + ) } } @@ -154,18 +155,18 @@ export default async function deployToStaging ({ const { DOCUBOT_REPO_PAT, HYDRO_ENDPOINT, HYDRO_SECRET } = process.env const secretEnvVars = { // This is required for cloning the `docs-early-access` repo - ...DOCUBOT_REPO_PAT && { DOCUBOT_REPO_PAT }, + ...(DOCUBOT_REPO_PAT && { DOCUBOT_REPO_PAT }), // These are required for Hydro event tracking - ...(HYDRO_ENDPOINT && HYDRO_SECRET) && { HYDRO_ENDPOINT, HYDRO_SECRET } + ...(HYDRO_ENDPOINT && HYDRO_SECRET && { HYDRO_ENDPOINT, HYDRO_SECRET }), } appSetup = await heroku.post('/app-setups', { body: { app: { - name: appName + name: appName, }, source_blob: { - url: tarballUrl + url: tarballUrl, }, // Pass some secret environment variables to staging apps via Heroku @@ -173,10 +174,10 @@ export default async function deployToStaging ({ overrides: { env: { ...secretEnvVars, - GIT_BRANCH: branch - } - } - } + GIT_BRANCH: branch, + }, + }, + }, }) console.log('Heroku AppSetup created', appSetup) @@ -193,14 +194,16 @@ export default async function deployToStaging ({ body: { user: `${author.login}@github.com`, // We don't want an email invitation for every new staging app - silent: true - } + silent: true, + }, }) console.log(`Added PR author @${author.login} as a Heroku app collaborator`) } } catch (error) { // It's fine if this fails, it shouldn't block the app from deploying! - console.warn(`Warning: failed to add PR author as a Heroku app collaborator. Error: ${error}`) + console.warn( + `Warning: failed to add PR author as a Heroku app collaborator. Error: ${error}` + ) } // A new Build is created as a by-product of creating an AppSetup. @@ -210,7 +213,11 @@ export default async function deployToStaging ({ appSetup = await heroku.get(`/app-setups/${appSetup.id}`) build = appSetup.build - console.log(`AppSetup status: ${appSetup.status} (after ${Math.round((Date.now() - appSetupStartTime) / 1000)} seconds)`) + console.log( + `AppSetup status: ${appSetup.status} (after ${Math.round( + (Date.now() - appSetupStartTime) / 1000 + )} seconds)` + ) } console.log('Heroku build detected', build) @@ -222,9 +229,9 @@ export default async function deployToStaging ({ build = await heroku.post(`/apps/${appName}/builds`, { body: { source_blob: { - url: tarballUrl - } - } + url: tarballUrl, + }, + }, }) } catch (error) { throw new Error(`Failed to create Heroku build. Error: ${error}`) @@ -247,14 +254,25 @@ export default async function deployToStaging ({ } catch (error) { throw new Error(`Failed to get build status. Error: ${error}`) } - console.log(`Heroku build status: ${(build || {}).status} (after ${Math.round((Date.now() - buildStartTime) / 1000)} seconds)`) + console.log( + `Heroku build status: ${(build || {}).status} (after ${Math.round( + (Date.now() - buildStartTime) / 1000 + )} seconds)` + ) } if (build.status !== 'succeeded') { - throw new Error(`Failed to build after ${Math.round((Date.now() - buildStartTime) / 1000)} seconds. See Heroku logs for more information:\n${logUrl}`) + throw new Error( + `Failed to build after ${Math.round( + (Date.now() - buildStartTime) / 1000 + )} seconds. See Heroku logs for more information:\n${logUrl}` + ) } - console.log(`Finished Heroku build after ${Math.round((Date.now() - buildStartTime) / 1000)} seconds.`, build) + console.log( + `Finished Heroku build after ${Math.round((Date.now() - buildStartTime) / 1000)} seconds.`, + build + ) const releaseStartTime = Date.now() // Close enough... let releaseId = build.release.id @@ -280,14 +298,27 @@ export default async function deployToStaging ({ throw new Error(`Failed to get release status. Error: ${error}`) } - console.log(`Release status: ${(release || {}).status} (after ${Math.round((Date.now() - releaseStartTime) / 1000)} seconds)`) + console.log( + `Release status: ${(release || {}).status} (after ${Math.round( + (Date.now() - releaseStartTime) / 1000 + )} seconds)` + ) } if (release.status !== 'succeeded') { - throw new Error(`Failed to release after ${Math.round((Date.now() - releaseStartTime) / 1000)} seconds. See Heroku logs for more information:\n${logUrl}`) + throw new Error( + `Failed to release after ${Math.round( + (Date.now() - releaseStartTime) / 1000 + )} seconds. See Heroku logs for more information:\n${logUrl}` + ) } - console.log(`Finished Heroku release after ${Math.round((Date.now() - releaseStartTime) / 1000)} seconds.`, release) + console.log( + `Finished Heroku release after ${Math.round( + (Date.now() - releaseStartTime) / 1000 + )} seconds.`, + release + ) // Monitor dyno state for this release to ensure it reaches "up" rather than crashing. // This will help us catch issues with faulty startup code and/or the package manifest. @@ -299,11 +330,11 @@ export default async function deployToStaging ({ // Keep checking while there are still dynos in non-terminal states let newDynos = [] - while (newDynos.length === 0 || newDynos.some(dyno => dyno.state === 'starting')) { + while (newDynos.length === 0 || newDynos.some((dyno) => dyno.state === 'starting')) { await sleep(SLEEP_INTERVAL) try { const dynoList = await heroku.get(`/apps/${appName}/dynos`) - const dynosForThisRelease = dynoList.filter(dyno => dyno.release.id === releaseId) + const dynosForThisRelease = dynoList.filter((dyno) => dyno.release.id === releaseId) // If this Heroku app was just newly created, often a secondary release // is requested to enable automatically managed SSL certificates. The @@ -323,7 +354,9 @@ export default async function deployToStaging ({ try { nextRelease = await heroku.get(`/apps/${appName}/releases/${release.version + 1}`) } catch (error) { - throw new Error(`Could not find a secondary release to explain the disappearing dynos. Error: ${error}`) + throw new Error( + `Could not find a secondary release to explain the disappearing dynos. Error: ${error}` + ) } if (nextRelease) { @@ -338,21 +371,27 @@ export default async function deployToStaging ({ } else { // Otherwise, assume another release replaced this one but it // PROBABLY would've succeeded...? - newDynos.forEach(dyno => { dyno.state = 'up' }) + newDynos.forEach((dyno) => { + dyno.state = 'up' + }) } } // else just keep monitoring and hope for the best } newDynos = dynosForThisRelease - console.log(`Dyno states: ${JSON.stringify(newDynos.map(dyno => dyno.state))} (after ${Math.round((Date.now() - dynoBootStartTime) / 1000)} seconds)`) + console.log( + `Dyno states: ${JSON.stringify(newDynos.map((dyno) => dyno.state))} (after ${Math.round( + (Date.now() - dynoBootStartTime) / 1000 + )} seconds)` + ) } catch (error) { throw new Error(`Failed to find dynos for this release. Error: ${error}`) } } - const crashedDynos = newDynos.filter(dyno => ['crashed', 'restarting'].includes(dyno.state)) - const runningDynos = newDynos.filter(dyno => dyno.state === 'up') + const crashedDynos = newDynos.filter((dyno) => ['crashed', 'restarting'].includes(dyno.state)) + const runningDynos = newDynos.filter((dyno) => dyno.state === 'up') // If any dynos crashed on start-up, fail the deployment if (crashedDynos.length > 0) { @@ -366,14 +405,16 @@ export default async function deployToStaging ({ body: { dyno: crashedDynos[0].name, lines: HEROKU_LOG_LINES_TO_SHOW, - tail: false - } + tail: false, + }, }) logUrl = logSession.logplex_url const logText = await got(logUrl).text() - console.error(`Here are the last ${HEROKU_LOG_LINES_TO_SHOW} lines of the Heroku log:\n\n${logText}`) + console.error( + `Here are the last ${HEROKU_LOG_LINES_TO_SHOW} lines of the Heroku log:\n\n${logText}` + ) } catch (error) { // Don't fail because of this error console.error(`Failed to retrieve the Heroku logs for the crashed dynos. Error: ${error}`) @@ -382,7 +423,11 @@ export default async function deployToStaging ({ throw new Error(errorMessage) } - console.log(`At least ${runningDynos.length} Heroku dyno(s) are ready after ${Math.round((Date.now() - dynoBootStartTime) / 1000)} seconds.`) + console.log( + `At least ${runningDynos.length} Heroku dyno(s) are ready after ${Math.round( + (Date.now() - dynoBootStartTime) / 1000 + )} seconds.` + ) // Send a series of requests to trigger the server warmup routines console.log('๐Ÿš€ Deployment status: in_progress - Triggering server warmup routines...') @@ -397,18 +442,30 @@ export default async function deployToStaging ({ beforeRetry: [ (options, error = {}, retryCount = '?') => { const statusCode = error.statusCode || (error.response || {}).statusCode || -1 - console.log(`Retrying after warmup request attempt #${retryCount} (${statusCode}) after ${Math.round((Date.now() - warmupStartTime) / 1000)} seconds...`) - } - ] - } + console.log( + `Retrying after warmup request attempt #${retryCount} (${statusCode}) after ${Math.round( + (Date.now() - warmupStartTime) / 1000 + )} seconds...` + ) + }, + ], + }, }) - console.log(`Warmup requests passed after ${Math.round((Date.now() - warmupStartTime) / 1000)} seconds`) + console.log( + `Warmup requests passed after ${Math.round((Date.now() - warmupStartTime) / 1000)} seconds` + ) } catch (error) { - throw new Error(`Warmup requests failed after ${Math.round((Date.now() - warmupStartTime) / 1000)} seconds. Error: ${error}`) + throw new Error( + `Warmup requests failed after ${Math.round( + (Date.now() - warmupStartTime) / 1000 + )} seconds. Error: ${error}` + ) } // Report success! - const successMessage = `Deployment succeeded after ${Math.round((Date.now() - startTime) / 1000)} seconds.` + const successMessage = `Deployment succeeded after ${Math.round( + (Date.now() - startTime) / 1000 + )} seconds.` console.log(successMessage) await octokit.repos.createDeploymentStatus({ @@ -417,21 +474,23 @@ export default async function deployToStaging ({ deployment_id: deploymentId, state: 'success', description: successMessage, - ...logUrl && { log_url: logUrl }, + ...(logUrl && { log_url: logUrl }), environment_url: homepageUrl, // The 'ant-man' preview is required for `state` values of 'inactive', as well as // the use of the `log_url`, `environment_url`, and `auto_inactive` parameters. // The 'flash' preview is required for `state` values of 'in_progress' and 'queued'. mediaType: { - previews: ['ant-man', 'flash'] - } + previews: ['ant-man', 'flash'], + }, }) console.log(`๐Ÿš€ Deployment status: success - ${successMessage}`) console.log(`Visit the newly deployed app at: ${homepageUrl}`) } catch (error) { // Report failure! - const failureMessage = `Deployment failed after ${Math.round((Date.now() - startTime) / 1000)} seconds. See logs for more information.` + const failureMessage = `Deployment failed after ${Math.round( + (Date.now() - startTime) / 1000 + )} seconds. See logs for more information.` console.error(failureMessage) try { @@ -442,19 +501,18 @@ export default async function deployToStaging ({ deployment_id: deploymentId, state: 'error', description: failureMessage, - ...logUrl && { log_url: logUrl }, + ...(logUrl && { log_url: logUrl }), environment_url: homepageUrl, // The 'ant-man' preview is required for `state` values of 'inactive', as well as // the use of the `log_url`, `environment_url`, and `auto_inactive` parameters. // The 'flash' preview is required for `state` values of 'in_progress' and 'queued'. mediaType: { - previews: ['ant-man', 'flash'] - } + previews: ['ant-man', 'flash'], + }, }) console.log( - `๐Ÿš€ Deployment status: error - ${failureMessage}` + - (logUrl ? ` Logs: ${logUrl}` : '') + `๐Ÿš€ Deployment status: error - ${failureMessage}` + (logUrl ? ` Logs: ${logUrl}` : '') ) } } catch (error) { diff --git a/script/deployment/parse-pr-url.js b/script/deployment/parse-pr-url.js index 646b284fcfd8..af7f876dffa6 100644 --- a/script/deployment/parse-pr-url.js +++ b/script/deployment/parse-pr-url.js @@ -5,17 +5,17 @@ const PR_NUMBER_FORMAT = '(\\d+)' const ALLOWED_PR_URL_FORMAT = new RegExp( '^' + - '[\'"]?' + - `https://github\\.com/${USERNAME_FORMAT}/${REPO_NAME_FORMAT}/pull/${PR_NUMBER_FORMAT}` + - '[\'"]?' + - '$' + '[\'"]?' + + `https://github\\.com/${USERNAME_FORMAT}/${REPO_NAME_FORMAT}/pull/${PR_NUMBER_FORMAT}` + + '[\'"]?' + + '$' ) -export default function parsePullRequestUrl (prUrl) { - const [/* fullMatch */, owner, repo, pr] = ((prUrl || '').match(ALLOWED_PR_URL_FORMAT) || []) +export default function parsePullRequestUrl(prUrl) { + const [, /* fullMatch */ owner, repo, pr] = (prUrl || '').match(ALLOWED_PR_URL_FORMAT) || [] return { owner, repo, - pullNumber: parseInt(pr, 10) || undefined + pullNumber: parseInt(pr, 10) || undefined, } } diff --git a/script/deployment/undeploy-from-staging.js b/script/deployment/undeploy-from-staging.js index 4bb772510983..9984db11e786 100644 --- a/script/deployment/undeploy-from-staging.js +++ b/script/deployment/undeploy-from-staging.js @@ -2,11 +2,11 @@ import Heroku from 'heroku-client' import createStagingAppName from './create-staging-app-name.js' -export default async function undeployFromStaging ({ +export default async function undeployFromStaging({ herokuToken, octokit, pullRequest, - runId = null + runId = null, }) { // Start a timer so we can report how long the deployment takes const startTime = Date.now() @@ -17,12 +17,10 @@ export default async function undeployFromStaging ({ base: { repo: { name: repo, - owner: { login: owner } - } + owner: { login: owner }, + }, }, - head: { - ref: branch - } + head: { ref: branch }, } = pullRequest const workflowRunLog = runId ? `https://github.com/${owner}/${repo}/actions/runs/${runId}` : null @@ -64,12 +62,14 @@ export default async function undeployFromStaging ({ // In the GitHub API, there can only be one active deployment per environment. // For our many staging apps, we must use the unique appName as the environment. - environment: appName + environment: appName, }) if (deployments.length === 0) { console.log('๐Ÿš€ No deployments to deactivate!') - console.log(`Finished undeploying after ${Math.round((Date.now() - startTime) / 1000)} seconds`) + console.log( + `Finished undeploying after ${Math.round((Date.now() - startTime) / 1000)} seconds` + ) return } @@ -83,21 +83,25 @@ export default async function undeployFromStaging ({ deployment_id: deployment.id, state: 'inactive', description: 'The app was undeployed', - ...logUrl && { log_url: logUrl }, + ...(logUrl && { log_url: logUrl }), // The 'ant-man' preview is required for `state` values of 'inactive', as well as // the use of the `log_url`, `environment_url`, and `auto_inactive` parameters. // The 'flash' preview is required for `state` values of 'in_progress' and 'queued'. mediaType: { - previews: ['ant-man', 'flash'] - } + previews: ['ant-man', 'flash'], + }, }) - console.log(`๐Ÿš€ Deployment status (ID: ${deployment.id}): ${deploymentStatus.state} - ${deploymentStatus.description}`) + console.log( + `๐Ÿš€ Deployment status (ID: ${deployment.id}): ${deploymentStatus.state} - ${deploymentStatus.description}` + ) } console.log(`Finished undeploying after ${Math.round((Date.now() - startTime) / 1000)} seconds`) } catch (error) { // Report failure! - const failureMessage = `Undeployment failed after ${Math.round((Date.now() - startTime) / 1000)} seconds. See logs for more information.` + const failureMessage = `Undeployment failed after ${Math.round( + (Date.now() - startTime) / 1000 + )} seconds. See logs for more information.` console.error(failureMessage) // Re-throw the error to bubble up diff --git a/script/early-access/clone-for-build.js b/script/early-access/clone-for-build.js index a6739751bedb..a62da3618a5b 100755 --- a/script/early-access/clone-for-build.js +++ b/script/early-access/clone-for-build.js @@ -18,7 +18,7 @@ xDotenv.config() const { DOCUBOT_REPO_PAT, HEROKU_PRODUCTION_APP, - GIT_BRANCH // Set by Actions and/or the deployer with the name of the docs-internal branch + GIT_BRANCH, // Set by Actions and/or the deployer with the name of the docs-internal branch } = process.env // Exit if PAT is not found @@ -56,14 +56,10 @@ const earlyAccessCloningParentDir = process.env.CI ? os.homedir() : os.tmpdir() const earlyAccessCloningDir = path.join(earlyAccessCloningParentDir, earlyAccessRepoName) const destinationDirNames = ['content', 'data', 'assets/images'] -const destinationDirsMap = destinationDirNames - .reduce( - (map, dirName) => { - map[dirName] = path.join(process.cwd(), dirName, earlyAccessDirName) - return map - }, - {} - ) +const destinationDirsMap = destinationDirNames.reduce((map, dirName) => { + map[dirName] = path.join(process.cwd(), dirName, earlyAccessDirName) + return map +}, {}) // Production vs. staging environment // TODO test that this works as expected @@ -73,20 +69,28 @@ const environment = HEROKU_PRODUCTION_APP ? 'production' : 'staging' let earlyAccessBranch = HEROKU_PRODUCTION_APP ? EA_PRODUCTION_BRANCH : currentBranch // Confirm that the branch exists in the remote -let branchExists = execSync(`git ls-remote --heads ${earlyAccessFullRepo} ${earlyAccessBranch}`).toString() +let branchExists = execSync( + `git ls-remote --heads ${earlyAccessFullRepo} ${earlyAccessBranch}` +).toString() // If the branch did NOT exist, try checking for the default branch instead if (!branchExists && earlyAccessBranch !== EA_PRODUCTION_BRANCH) { - console.warn(`The branch '${earlyAccessBranch}' was not found in ${earlyAccessOwner}/${earlyAccessRepoName}!`) + console.warn( + `The branch '${earlyAccessBranch}' was not found in ${earlyAccessOwner}/${earlyAccessRepoName}!` + ) console.warn(`Attempting the default branch ${EA_PRODUCTION_BRANCH} instead...`) earlyAccessBranch = EA_PRODUCTION_BRANCH - branchExists = execSync(`git ls-remote --heads ${earlyAccessFullRepo} ${earlyAccessBranch}`).toString() + branchExists = execSync( + `git ls-remote --heads ${earlyAccessFullRepo} ${earlyAccessBranch}` + ).toString() } // If no suitable branch was found, bail out now if (!branchExists) { - console.error(`The branch '${earlyAccessBranch}' was not found in ${earlyAccessOwner}/${earlyAccessRepoName}!`) + console.error( + `The branch '${earlyAccessBranch}' was not found in ${earlyAccessOwner}/${earlyAccessRepoName}!` + ) console.error('Exiting!') process.exit(1) } @@ -99,13 +103,13 @@ console.log(`Setting up: ${earlyAccessCloningDir}`) execSync( `git clone --single-branch --branch ${earlyAccessBranch} ${earlyAccessFullRepo} ${earlyAccessRepoName}`, { - cwd: earlyAccessCloningParentDir + cwd: earlyAccessCloningParentDir, } ) console.log(`Using early-access ${environment} branch: '${earlyAccessBranch}'`) // Remove all existing early access directories from this repo -destinationDirNames.forEach(key => rimraf(destinationDirsMap[key])) +destinationDirNames.forEach((key) => rimraf(destinationDirsMap[key])) // Move the latest early access source directories into this repo destinationDirNames.forEach((dirName) => { diff --git a/script/early-access/symlink-from-local-repo.js b/script/early-access/symlink-from-local-repo.js index a7e61b3b4049..ff4f8fa25075 100755 --- a/script/early-access/symlink-from-local-repo.js +++ b/script/early-access/symlink-from-local-repo.js @@ -21,7 +21,10 @@ const earlyAccessRepoUrl = `https://github.com/github/${earlyAccessRepo}` program .description(`Create or destroy symlinks to your local "${earlyAccessRepo}" repository.`) - .option('-p, --path-to-early-access-repo <PATH>', `path to a local checkout of ${earlyAccessRepoUrl}`) + .option( + '-p, --path-to-early-access-repo <PATH>', + `path to a local checkout of ${earlyAccessRepoUrl}` + ) .option('-u, --unlink', 'remove the symlinks') .parse(process.argv) @@ -45,22 +48,24 @@ if (!unlink && pathToEarlyAccessRepo) { } if (!dirStats) { - throw new Error(`The local "${earlyAccessRepo}" repo directory does not exist:`, earlyAccessLocalRepoDir) + throw new Error( + `The local "${earlyAccessRepo}" repo directory does not exist:`, + earlyAccessLocalRepoDir + ) } if (dirStats && !dirStats.isDirectory()) { - throw new Error(`A non-directory entry exists at the local "${earlyAccessRepo}" repo directory location:`, earlyAccessLocalRepoDir) + throw new Error( + `A non-directory entry exists at the local "${earlyAccessRepo}" repo directory location:`, + earlyAccessLocalRepoDir + ) } } const destinationDirNames = ['content', 'data', 'assets/images'] -const destinationDirsMap = destinationDirNames - .reduce( - (map, dirName) => { - map[dirName] = path.join(process.cwd(), dirName, earlyAccessDirName) - return map - }, - {} - ) +const destinationDirsMap = destinationDirNames.reduce((map, dirName) => { + map[dirName] = path.join(process.cwd(), dirName, earlyAccessDirName) + return map +}, {}) // Remove all existing early access directories from this repo destinationDirNames.forEach((dirName) => { @@ -100,7 +105,9 @@ destinationDirNames.forEach((dirName) => { throw new Error(`The early access directory '${dirName}' entry is not a symbolic link!`) } if (!fs.statSync(destDir).isDirectory()) { - throw new Error(`The early access directory '${dirName}' entry's symbolic link does not refer to a directory!`) + throw new Error( + `The early access directory '${dirName}' entry's symbolic link does not refer to a directory!` + ) } console.log(`+ Added symlink for early access directory '${dirName}' into this repo`) diff --git a/script/early-access/update-data-and-image-paths.js b/script/early-access/update-data-and-image-paths.js index 23d252275f52..f552868400da 100755 --- a/script/early-access/update-data-and-image-paths.js +++ b/script/early-access/update-data-and-image-paths.js @@ -20,7 +20,10 @@ const earlyAccessImages = path.posix.join(process.cwd(), 'assets/images/early-ac program .description('Update data and image paths.') - .option('-p, --path-to-early-access-content-file <PATH>', 'Path to a specific content file. Defaults to all Early Access content files if not provided.') + .option( + '-p, --path-to-early-access-content-file <PATH>', + 'Path to a specific content file. Defaults to all Early Access content files if not provided.' + ) .option('-a, --add', 'Add "early-access" to data and image paths.') .option('-r, --remove', 'Remove "early-access" from data and image paths.') .parse(process.argv) @@ -37,96 +40,104 @@ if (pathToEarlyAccessContentFile) { earlyAccessContentAndDataFiles = path.posix.join(process.cwd(), pathToEarlyAccessContentFile) if (!fs.existsSync(earlyAccessContentAndDataFiles)) { - console.error(`Error! ${pathToEarlyAccessContentFile} can't be found. Make sure the path starts with 'content/early-access'.`) + console.error( + `Error! ${pathToEarlyAccessContentFile} can't be found. Make sure the path starts with 'content/early-access'.` + ) process.exit(1) } earlyAccessContentAndDataFiles = [earlyAccessContentAndDataFiles] } else { // Gather the EA content and data files - earlyAccessContentAndDataFiles = walk(earlyAccessContent, { includeBasePath: true, directories: false }) - .concat(walk(earlyAccessData, { includeBasePath: true, directories: false })) + earlyAccessContentAndDataFiles = walk(earlyAccessContent, { + includeBasePath: true, + directories: false, + }).concat(walk(earlyAccessData, { includeBasePath: true, directories: false })) } // Update the EA content and data files -earlyAccessContentAndDataFiles - .forEach(file => { - const oldContents = fs.readFileSync(file, 'utf8') - - // Get all the data references in each file that exist in data/early-access - const dataRefs = (oldContents.match(patterns.dataReference) || []) - .filter(dataRef => dataRef.includes('variables') ? checkVariable(dataRef) : checkReusable(dataRef)) - - // Get all the image references in each file that exist in assets/images/early-access - const imageRefs = (oldContents.match(patterns.imagePath) || []) - .filter(imageRef => checkImage(imageRef)) - - const replacements = {} - - if (add) { - dataRefs - // Since we're adding early-access to the path, filter for those that do not already include it - .filter(dataRef => !dataRef.includes('data early-access.')) - // Add to the { oldRef: newRef } replacements object - .forEach(dataRef => { - replacements[dataRef] = dataRef.replace(/({% data )(.*)/, '$1early-access.$2') - }) - - imageRefs - // Since we're adding early-access to the path, filter for those that do not already include it - .filter(imageRef => !imageRef.split('/').includes('early-access')) - // Add to the { oldRef: newRef } replacements object - .forEach(imageRef => { - replacements[imageRef] = imageRef.replace('/assets/images/', '/assets/images/early-access/') - }) - } - - if (remove) { - dataRefs - // Since we're removing early-access from the path, filter for those that include it - .filter(dataRef => dataRef.includes('{% data early-access.')) - // Add to the { oldRef: newRef } replacements object - .forEach(dataRef => { - replacements[dataRef] = dataRef.replace('early-access.', '') - }) - - imageRefs - // Since we're removing early-access from the path, filter for those that include it - .filter(imageRef => imageRef.split('/').includes('early-access')) - // Add to the { oldRef: newRef } replacements object - .forEach(imageRef => { - replacements[imageRef] = imageRef.replace('/assets/images/early-access/', '/assets/images/') - }) - } - - // Return early if nothing to replace - if (!Object.keys(replacements).length) { - return - } - - // Make the replacement in the content - let newContents = oldContents - Object.entries(replacements).forEach(([oldRef, newRef]) => { - newContents = newContents.replace(new RegExp(escapeRegExp(oldRef), 'g'), newRef) - }) - - // Write the updated content - fs.writeFileSync(file, newContents) +earlyAccessContentAndDataFiles.forEach((file) => { + const oldContents = fs.readFileSync(file, 'utf8') + + // Get all the data references in each file that exist in data/early-access + const dataRefs = (oldContents.match(patterns.dataReference) || []).filter((dataRef) => + dataRef.includes('variables') ? checkVariable(dataRef) : checkReusable(dataRef) + ) + + // Get all the image references in each file that exist in assets/images/early-access + const imageRefs = (oldContents.match(patterns.imagePath) || []).filter((imageRef) => + checkImage(imageRef) + ) + + const replacements = {} + + if (add) { + dataRefs + // Since we're adding early-access to the path, filter for those that do not already include it + .filter((dataRef) => !dataRef.includes('data early-access.')) + // Add to the { oldRef: newRef } replacements object + .forEach((dataRef) => { + replacements[dataRef] = dataRef.replace(/({% data )(.*)/, '$1early-access.$2') + }) + + imageRefs + // Since we're adding early-access to the path, filter for those that do not already include it + .filter((imageRef) => !imageRef.split('/').includes('early-access')) + // Add to the { oldRef: newRef } replacements object + .forEach((imageRef) => { + replacements[imageRef] = imageRef.replace('/assets/images/', '/assets/images/early-access/') + }) + } + + if (remove) { + dataRefs + // Since we're removing early-access from the path, filter for those that include it + .filter((dataRef) => dataRef.includes('{% data early-access.')) + // Add to the { oldRef: newRef } replacements object + .forEach((dataRef) => { + replacements[dataRef] = dataRef.replace('early-access.', '') + }) + + imageRefs + // Since we're removing early-access from the path, filter for those that include it + .filter((imageRef) => imageRef.split('/').includes('early-access')) + // Add to the { oldRef: newRef } replacements object + .forEach((imageRef) => { + replacements[imageRef] = imageRef.replace('/assets/images/early-access/', '/assets/images/') + }) + } + + // Return early if nothing to replace + if (!Object.keys(replacements).length) { + return + } + + // Make the replacement in the content + let newContents = oldContents + Object.entries(replacements).forEach(([oldRef, newRef]) => { + newContents = newContents.replace(new RegExp(escapeRegExp(oldRef), 'g'), newRef) }) + // Write the updated content + fs.writeFileSync(file, newContents) +}) + console.log('Done! Run "git status" in your docs-early-access checkout to see the changes.\n') -function checkVariable (dataRef) { +function checkVariable(dataRef) { // Get the data filepath from the data reference, // where the data reference looks like: {% data variables.foo.bar %} // and the data filepath looks like: data/variables/foo.yml with key of 'bar'. - const variablePathArray = dataRef.match(/{% data (.*?) %}/)[1].split('.') - // If early access is part of the path, remove it (since the path below already includes it) - .filter(n => n !== 'early-access') + const variablePathArray = dataRef + .match(/{% data (.*?) %}/)[1] + .split('.') + // 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`. - const variableKey = last(variablePathArray); variablePathArray.pop() + const variableKey = last(variablePathArray) + variablePathArray.pop() const variablePath = path.posix.join(earlyAccessData, `${variablePathArray.join('/')}.yml`) // If the variable file doesn't exist in data/early-access, exclude it @@ -137,20 +148,22 @@ function checkVariable (dataRef) { return variableFileContent[variableKey] } -function checkReusable (dataRef) { +function checkReusable(dataRef) { // Get the data filepath from the data reference, // where the data reference looks like: {% data reusables.foo.bar %} // and the data filepath looks like: data/reusables/foo/bar.md. - const reusablePath = dataRef.match(/{% data (.*?) %}/)[1].split('.') + const reusablePath = dataRef + .match(/{% data (.*?) %}/)[1] + .split('.') // If early access is part of the path, remove it (since the path below already includes it) - .filter(n => n !== 'early-access') + .filter((n) => n !== 'early-access') .join('/') // If the reusable file doesn't exist in data/early-access, exclude it return fs.existsSync(`${path.posix.join(earlyAccessData, reusablePath)}.md`) } -function checkImage (imageRef) { +function checkImage(imageRef) { const imagePath = imageRef .replace('/assets/images/', '') // If early access is part of the path, remove it (since the path below already includes it) diff --git a/script/enterprise-server-deprecations/archive-version.js b/script/enterprise-server-deprecations/archive-version.js index 4b4a48b0b41d..2477cfd520a8 100755 --- a/script/enterprise-server-deprecations/archive-version.js +++ b/script/enterprise-server-deprecations/archive-version.js @@ -30,7 +30,9 @@ const remoteImageStoreBaseURL = 'https://githubdocs.azureedge.net/github-images' // [end-readme] program - .description('Scrape HTML of the oldest supported Enterprise version and add it to the archival repository.') + .description( + 'Scrape HTML of the oldest supported Enterprise version and add it to the archival repository.' + ) .option('-p, --path-to-archival-repo <PATH>', `path to a local checkout of ${archivalRepoUrl}`) .option('-d, --dry-run', 'only scrape the first 10 pages for testing purposes') .parse(process.argv) @@ -41,12 +43,12 @@ const dryRun = program.opts().dryRun main() class RewriteAssetPathsPlugin { - constructor (version, tempDirectory) { + constructor(version, tempDirectory) { this.version = version this.tempDirectory = tempDirectory } - apply (registerAction) { + apply(registerAction) { registerAction('onResourceSaved', async ({ resource }) => { // Show some activity process.stdout.write('.') @@ -82,7 +84,11 @@ class RewriteAssetPathsPlugin { newBody = text.replace( /(?<attribute>url)\("(?:\.\.\/)*(?<basepath>assets\/fonts|assets\/images)/g, (match, attribute, basepath) => { - const replaced = path.join(`${remoteImageStoreBaseURL}/enterprise`, this.version, basepath) + const replaced = path.join( + `${remoteImageStoreBaseURL}/enterprise`, + this.version, + basepath + ) const returnValue = `${attribute}("${replaced}` return returnValue } @@ -91,14 +97,12 @@ class RewriteAssetPathsPlugin { const filePath = path.join(this.tempDirectory, resource.getFilename()) - await fs - .promises - .writeFile(filePath, newBody, 'binary') + await fs.promises.writeFile(filePath, newBody, 'binary') }) } } -async function main () { +async function main() { if (!pathToArchivalRepo) { console.log(`Please specify a path to a local checkout of ${archivalRepoUrl}`) const scriptPath = path.relative(process.cwd(), __filename) @@ -107,7 +111,9 @@ async function main () { } if (dryRun) { - console.log('This is a dry run! Creating HTML for redirects and scraping the first 10 pages only.\n') + console.log( + 'This is a dry run! Creating HTML for redirects and scraping the first 10 pages only.\n' + ) } // Build the production assets, to simulate a production deployment @@ -124,12 +130,13 @@ async function main () { console.log(`Enterprise version to archive: ${version}`) const pageMap = await loadPageMap() - const permalinksPerVersion = Object.keys(pageMap) - .filter(key => key.includes(`/enterprise-server@${version}`)) + const permalinksPerVersion = Object.keys(pageMap).filter((key) => + key.includes(`/enterprise-server@${version}`) + ) const urls = dryRun - ? permalinksPerVersion.slice(0, 10).map(href => `${host}${href}`) - : permalinksPerVersion.map(href => `${host}${href}`) + ? permalinksPerVersion.slice(0, 10).map((href) => `${host}${href}`) + : permalinksPerVersion.map((href) => `${host}${href}`) console.log(`found ${urls.length} pages for version ${version}`) @@ -157,24 +164,23 @@ async function main () { directory: tempDirectory, filenameGenerator: 'bySiteStructure', requestConcurrency: 6, - plugins: [new RewriteAssetPathsPlugin(version, tempDirectory)] + plugins: [new RewriteAssetPathsPlugin(version, tempDirectory)], } createApp().listen(port, async () => { console.log(`started server on ${host}`) - await scrape(scraperOptions).catch(err => { + await scrape(scraperOptions).catch((err) => { console.error('scraping error') console.error(err) }) - fs.renameSync( - path.join(tempDirectory, `/localhost_${port}`), - path.join(finalDirectory) - ) + fs.renameSync(path.join(tempDirectory, `/localhost_${port}`), path.join(finalDirectory)) rimraf(tempDirectory) - console.log(`\n\ndone scraping! added files to ${path.relative(process.cwd(), finalDirectory)}\n`) + console.log( + `\n\ndone scraping! added files to ${path.relative(process.cwd(), finalDirectory)}\n` + ) // create redirect html files to preserve frontmatter redirects await createRedirectsFile(permalinksPerVersion, pageMap, finalDirectory) @@ -185,22 +191,29 @@ async function main () { }) } -async function createRedirectsFile (permalinks, pageMap, finalDirectory) { - const pagesPerVersion = permalinks.map(permalink => pageMap[permalink]) +async function createRedirectsFile(permalinks, pageMap, finalDirectory) { + const pagesPerVersion = permalinks.map((permalink) => pageMap[permalink]) const redirects = await loadRedirects(pagesPerVersion, pageMap) const redirectsPerVersion = {} Object.entries(redirects).forEach(([oldPath, newPath]) => { // remove any liquid variables that sneak in - oldPath = oldPath - .replace('/{{ page.version }}', '') - .replace('/{{ currentVersion }}', '') + oldPath = oldPath.replace('/{{ page.version }}', '').replace('/{{ currentVersion }}', '') // ignore any old paths that are not in this version - if (!(oldPath.includes(`/enterprise-server@${version}`) || oldPath.includes(`/enterprise/${version}`))) return + if ( + !( + oldPath.includes(`/enterprise-server@${version}`) || + oldPath.includes(`/enterprise/${version}`) + ) + ) + return redirectsPerVersion[oldPath] = newPath }) - fs.writeFileSync(path.posix.join(finalDirectory, 'redirects.json'), JSON.stringify(redirectsPerVersion, null, 2)) + fs.writeFileSync( + path.posix.join(finalDirectory, 'redirects.json'), + JSON.stringify(redirectsPerVersion, null, 2) + ) } diff --git a/script/enterprise-server-deprecations/remove-static-files.js b/script/enterprise-server-deprecations/remove-static-files.js index 570f367727ba..3c315cf4ea49 100755 --- a/script/enterprise-server-deprecations/remove-static-files.js +++ b/script/enterprise-server-deprecations/remove-static-files.js @@ -18,31 +18,33 @@ const restDereferencedDir = path.join(process.cwd(), 'lib/rest/static/dereferenc // // [end-readme] -const supportedEnterpriseVersions = Object.values(allVersions).filter(v => v.plan === 'enterprise-server') +const supportedEnterpriseVersions = Object.values(allVersions).filter( + (v) => v.plan === 'enterprise-server' +) // webhooks and GraphQL -const supportedMiscVersions = supportedEnterpriseVersions.map(v => v.miscVersionName) +const supportedMiscVersions = supportedEnterpriseVersions.map((v) => v.miscVersionName) // The miscBaseName is the same for all GHES versions (currently `ghes-`), so we can just grab the first one -const miscBaseName = supportedEnterpriseVersions.map(v => v.miscBaseName)[0] +const miscBaseName = supportedEnterpriseVersions.map((v) => v.miscBaseName)[0] -;[graphqlDataDir, graphqlStaticDir, webhooksStaticDir].forEach(dir => { +;[graphqlDataDir, graphqlStaticDir, webhooksStaticDir].forEach((dir) => { removeFiles(dir, miscBaseName, supportedMiscVersions) }) // REST -const supportedOpenApiVersions = supportedEnterpriseVersions.map(v => v.openApiVersionName) +const supportedOpenApiVersions = supportedEnterpriseVersions.map((v) => v.openApiVersionName) // The openApiBaseName is the same for all GHES versions (currently `ghes-`), so we can just grab the first one -const openApiBaseName = supportedEnterpriseVersions.map(v => v.openApiBaseName)[0] +const openApiBaseName = supportedEnterpriseVersions.map((v) => v.openApiBaseName)[0] -;[restDecoratedDir, restDereferencedDir].forEach(dir => { +;[restDecoratedDir, restDereferencedDir].forEach((dir) => { removeFiles(dir, openApiBaseName, supportedOpenApiVersions) }) -function removeFiles (dir, baseName, supportedVersions) { +function removeFiles(dir, baseName, supportedVersions) { fs.readdirSync(dir) - .filter(file => file.includes(baseName)) - .filter(file => supportedVersions.every(version => !file.includes(version))) - .forEach(file => { + .filter((file) => file.includes(baseName)) + .filter((file) => supportedVersions.every((version) => !file.includes(version))) + .forEach((file) => { const fullPath = path.join(dir, file) console.log(`removing ${fullPath}`) rimraf(fullPath) diff --git a/script/enterprise-server-deprecations/remove-version-markup.js b/script/enterprise-server-deprecations/remove-version-markup.js index 668dc47a5ce5..836d0008de57 100755 --- a/script/enterprise-server-deprecations/remove-version-markup.js +++ b/script/enterprise-server-deprecations/remove-version-markup.js @@ -25,7 +25,9 @@ const elseifRegex = /{-?% elsif/ // [end-readme] program - .description('Remove Liquid conditionals and update versions frontmatter for a given Enterprise Server release.') + .description( + 'Remove Liquid conditionals and update versions frontmatter for a given Enterprise Server release.' + ) .option('-r, --release <NUMBER>', 'Enterprise Server release number. Example: 2.19') .parse(process.argv) @@ -34,7 +36,7 @@ const release = program.opts().release // verify CLI options if (!release) { console.log(program.description() + '\n') - program.options.forEach(opt => { + program.options.forEach((opt) => { console.log(opt.flags) console.log(opt.description + '\n') }) @@ -42,7 +44,9 @@ if (!release) { } if (!enterpriseServerReleases.all.includes(release)) { - console.log(`You specified ${release}! Please specify a supported or deprecated release number from lib/enterprise-server-releases.js`) + console.log( + `You specified ${release}! Please specify a supported or deprecated release number from lib/enterprise-server-releases.js` + ) process.exit(1) } @@ -56,12 +60,12 @@ console.log(`Next oldest version: ${nextOldestVersion}\n`) // gather content and data files const contentFiles = walk(contentPath, { includeBasePath: true, directories: false }) - .filter(file => file.endsWith('.md')) - .filter(file => !(file.endsWith('README.md') || file === 'LICENSE' || file === 'LICENSE-CODE')) + .filter((file) => file.endsWith('.md')) + .filter((file) => !(file.endsWith('README.md') || file === 'LICENSE' || file === 'LICENSE-CODE')) const dataFiles = walk(dataPath, { includeBasePath: true, directories: false }) - .filter(file => file.includes('data/reusables') || file.includes('data/variables')) - .filter(file => !file.endsWith('README.md')) + .filter((file) => file.includes('data/reusables') || file.includes('data/variables')) + .filter((file) => !file.endsWith('README.md')) const allFiles = contentFiles.concat(dataFiles) @@ -69,12 +73,14 @@ main() console.log(`\nRunning ${removeUnusedAssetsScript}...`) runRemoveUnusedAssetsScript() -function printElseIfFoundWarning (location) { - console.log(`${location} has an 'elsif' condition! Resolve all elsifs by hand, then rerun the script.`) +function printElseIfFoundWarning(location) { + console.log( + `${location} has an 'elsif' condition! Resolve all elsifs by hand, then rerun the script.` + ) } -function main () { - allFiles.forEach(file => { +function main() { + allFiles.forEach((file) => { const oldContents = fs.readFileSync(file, 'utf8') const { content, data } = matter(oldContents) @@ -84,7 +90,7 @@ function main () { process.exit() } - Object.keys(data).forEach(key => { + Object.keys(data).forEach((key) => { if (elseifRegex.test(data[key])) { printElseIfFoundWarning(`frontmatter '${key}' in ${file}`) process.exit() @@ -108,5 +114,7 @@ function main () { fs.writeFileSync(file, newContents) }) - console.log(`Removed ${versionToDeprecate} markup from content and data files! Review and run script/test.`) + console.log( + `Removed ${versionToDeprecate} markup from content and data files! Review and run script/test.` + ) } diff --git a/script/enterprise-server-releases/add-ghec-to-fpt.js b/script/enterprise-server-releases/add-ghec-to-fpt.js index 814da1f534fa..117ae80ffe78 100755 --- a/script/enterprise-server-releases/add-ghec-to-fpt.js +++ b/script/enterprise-server-releases/add-ghec-to-fpt.js @@ -19,8 +19,13 @@ const contentPath = path.join(process.cwd(), 'content') const dataPath = path.join(process.cwd(), 'data') program - .description('Add versions frontmatter and Liquid conditionals for GitHub EC based on FPT. Runs on all content by default.') - .option('-p, --products [OPTIONAL PRODUCT_IDS...]', 'Optional list of space-separated product IDs. Example: admin github developers') + .description( + 'Add versions frontmatter and Liquid conditionals for GitHub EC based on FPT. Runs on all content by default.' + ) + .option( + '-p, --products [OPTIONAL PRODUCT_IDS...]', + 'Optional list of space-separated product IDs. Example: admin github developers' + ) .parse(process.argv) const { products } = program.opts() @@ -40,78 +45,78 @@ console.log('Working...\n') const englishContentFiles = walkContent(contentPath) const englishDataFiles = walkData(dataPath) -function walkContent (dirPath) { +function walkContent(dirPath) { const productArray = products || [''] - return productArray.map(product => { - dirPath = path.join(contentPath, product) - return walk(dirPath, { includeBasePath: true, directories: false }) - .filter(file => file.includes('/content/')) - .filter(file => file.endsWith('.md')) - .filter(file => !file.endsWith('README.md')) - }).flat() + return productArray + .map((product) => { + dirPath = path.join(contentPath, product) + return walk(dirPath, { includeBasePath: true, directories: false }) + .filter((file) => file.includes('/content/')) + .filter((file) => file.endsWith('.md')) + .filter((file) => !file.endsWith('README.md')) + }) + .flat() } -function walkData (dirPath) { +function walkData(dirPath) { return walk(dirPath, { includeBasePath: true, directories: false }) - .filter(file => file.includes('/data/reusables') || file.includes('/data/variables')) - .filter(file => !file.endsWith('README.md')) + .filter((file) => file.includes('/data/reusables') || file.includes('/data/variables')) + .filter((file) => !file.endsWith('README.md')) } const allContentFiles = englishContentFiles const allDataFiles = englishDataFiles // Update the data files -allDataFiles - .forEach(file => { - const dataContent = fs.readFileSync(file, 'utf8') +allDataFiles.forEach((file) => { + const dataContent = fs.readFileSync(file, 'utf8') - const conditionalsToUpdate = getConditionalsToUpdate(dataContent) - if (!conditionalsToUpdate.length) return + const conditionalsToUpdate = getConditionalsToUpdate(dataContent) + if (!conditionalsToUpdate.length) return - // Update Liquid in data files - const newDataContent = updateLiquid(conditionalsToUpdate, dataContent) + // Update Liquid in data files + const newDataContent = updateLiquid(conditionalsToUpdate, dataContent) - fs.writeFileSync(file, newDataContent) - }) + fs.writeFileSync(file, newDataContent) +}) // Update the content files -allContentFiles - .forEach(file => { - const { data, content } = frontmatter(fs.readFileSync(file, 'utf8')) +allContentFiles.forEach((file) => { + const { data, content } = frontmatter(fs.readFileSync(file, 'utf8')) - // Return early if the current page frontmatter does not apply to either GHEC or the given fpt release - if (!data.versions.fpt) return + // Return early if the current page frontmatter does not apply to either GHEC or the given fpt release + if (!data.versions.fpt) return - const conditionalsToUpdate = getConditionalsToUpdate(content) - if (!conditionalsToUpdate.length) return + const conditionalsToUpdate = getConditionalsToUpdate(content) + if (!conditionalsToUpdate.length) return - // Update Liquid in content files - const newContent = updateLiquid(conditionalsToUpdate, content) + // Update Liquid in content files + const newContent = updateLiquid(conditionalsToUpdate, content) - // Add frontmatter version - data.versions.ghec = '*' + // Add frontmatter version + data.versions.ghec = '*' - // Update Liquid in frontmatter props - Object.keys(data) - .filter(key => typeof data[key] === 'string') - .forEach(key => { - const conditionalsToUpdate = getConditionalsToUpdate(data[key]) - if (!conditionalsToUpdate.length) return - data[key] = updateLiquid(conditionalsToUpdate, data[key]) - }) + // Update Liquid in frontmatter props + Object.keys(data) + .filter((key) => typeof data[key] === 'string') + .forEach((key) => { + const conditionalsToUpdate = getConditionalsToUpdate(data[key]) + if (!conditionalsToUpdate.length) return + data[key] = updateLiquid(conditionalsToUpdate, data[key]) + }) - fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) - }) + fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) +}) -function getConditionalsToUpdate (content) { +function getConditionalsToUpdate(content) { return getLiquidConditionals(content, 'ifversion') - .filter(c => c.includes('fpt')) - .filter(c => !c.includes('ghec')) + .filter((c) => c.includes('fpt')) + .filter((c) => !c.includes('ghec')) } -function updateLiquid (conditionalsToUpdate, content) { +function updateLiquid(conditionalsToUpdate, content) { let newContent = content - conditionalsToUpdate.forEach(cond => { + conditionalsToUpdate.forEach((cond) => { const oldConditional = `{% ifversion ${cond} %}` const newConditional = `{% ifversion ${cond.concat(' or ghec')} %}` const oldConditionalRegex = new RegExp(escapeRegExp(oldConditional), 'g') diff --git a/script/enterprise-server-releases/create-graphql-files.js b/script/enterprise-server-releases/create-graphql-files.js index dc66883ed921..0aba65a41175 100755 --- a/script/enterprise-server-releases/create-graphql-files.js +++ b/script/enterprise-server-releases/create-graphql-files.js @@ -17,8 +17,14 @@ const graphqlDataDir = path.join(process.cwd(), 'data/graphql') program .description('Create GraphQL files in lib/graphql/static based on an existing version.') - .option('-n, --newVersion <version>', 'The version to copy the files to. Must be in <plan@release> format.') - .option('-o, --oldVersion <version>', 'The version to copy the files from. Must be in <plan@release> format.') + .option( + '-n, --newVersion <version>', + 'The version to copy the files to. Must be in <plan@release> format.' + ) + .option( + '-o, --oldVersion <version>', + 'The version to copy the files from. Must be in <plan@release> format.' + ) .parse(process.argv) const newVersion = program.opts().newVersion @@ -29,8 +35,12 @@ if (!(newVersion && oldVersion)) { process.exit(1) } -if (!(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).includes(oldVersion))) { - console.log('Error! You must provide the full name of a currently supported version, e.g., enterprise-server@2.22.') +if ( + !(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).includes(oldVersion)) +) { + console.log( + 'Error! You must provide the full name of a currently supported version, e.g., enterprise-server@2.22.' + ) process.exit(1) } @@ -61,10 +71,11 @@ const inputObjects = JSON.parse(fs.readFileSync(inputObjectsFile)) // The prerendered objects file for the "old version" contains hardcoded links with the old version number. // We need to update those links to include the new version to prevent a test from failing. const regexOldVersion = new RegExp(oldVersion, 'gi') -const stringifiedObject = JSON.stringify(objects[oldVersionId]) - .replace(regexOldVersion, newVersion) -const stringifiedInputObject = JSON.stringify(inputObjects[oldVersionId]) - .replace(regexOldVersion, newVersion) +const stringifiedObject = JSON.stringify(objects[oldVersionId]).replace(regexOldVersion, newVersion) +const stringifiedInputObject = JSON.stringify(inputObjects[oldVersionId]).replace( + regexOldVersion, + newVersion +) previews[newVersionId] = previews[oldVersionId] changes[newVersionId] = changes[oldVersionId] @@ -104,7 +115,7 @@ const destDir = path.join(graphqlDataDir, newVersionId) mkdirp(destDir) // copy the files -fs.readdirSync(srcDir).forEach(file => { +fs.readdirSync(srcDir).forEach((file) => { const srcFile = path.join(srcDir, file) const destFile = path.join(destDir, file) fs.copyFileSync(srcFile, destFile) diff --git a/script/enterprise-server-releases/create-rest-files.js b/script/enterprise-server-releases/create-rest-files.js index 04a2f55793ac..cfa03611027b 100755 --- a/script/enterprise-server-releases/create-rest-files.js +++ b/script/enterprise-server-releases/create-rest-files.js @@ -17,9 +17,17 @@ const decoratedDir = 'lib/rest/static/decorated' // [end-readme] program - .description('Create new openAPI files in lib/rest/static/decorated and lib/rest/static/dereferenced based on an existing version.') - .option('-n, --newVersion <version>', 'The new version to copy the REST files to. Must be in <plan@release> format.') - .option('-o, --oldVersion <version>', 'The existing version to copy the REST files from. Must be in <plan@release> format.') + .description( + 'Create new openAPI files in lib/rest/static/decorated and lib/rest/static/dereferenced based on an existing version.' + ) + .option( + '-n, --newVersion <version>', + 'The new version to copy the REST files to. Must be in <plan@release> format.' + ) + .option( + '-o, --oldVersion <version>', + 'The existing version to copy the REST files from. Must be in <plan@release> format.' + ) .parse(process.argv) const newVersion = program.opts().newVersion @@ -30,14 +38,18 @@ if (!(newVersion && oldVersion)) { process.exit(1) } -if (!(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).includes(oldVersion))) { - console.log('Error! You must provide the full name of a currently supported version, e.g., enterprise-server@2.22.') +if ( + !(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).includes(oldVersion)) +) { + console.log( + 'Error! You must provide the full name of a currently supported version, e.g., enterprise-server@2.22.' + ) process.exit(1) } main() -async function main () { +async function main() { const oldDereferencedFilename = `${allVersions[oldVersion].openApiVersionName}.deref.json` const newDereferencedFilename = `${allVersions[newVersion].openApiVersionName}.deref.json` const newDecoratedFilename = `${allVersions[newVersion].openApiVersionName}.json` @@ -63,13 +75,15 @@ async function main () { fs.writeFileSync(newDereferencedFile, newDereferenceContent) console.log(`Created ${newDereferencedFile}.`) - const dereferencedSchema = JSON.parse(fs.readFileSync(path.join(process.cwd(), newDereferencedFile))) + const dereferencedSchema = JSON.parse( + fs.readFileSync(path.join(process.cwd(), newDereferencedFile)) + ) // Store all operations in an array of operation objects const operations = await getOperations(dereferencedSchema) // Process each operation asynchronously - await Promise.all(operations.map(operation => operation.process())) + await Promise.all(operations.map((operation) => operation.process())) // Write processed operations to disk fs.writeFileSync(newDecoratedFile, JSON.stringify(operations, null, 2)) diff --git a/script/enterprise-server-releases/create-webhook-files.js b/script/enterprise-server-releases/create-webhook-files.js index 155e12e43a4e..9068f115ccd3 100755 --- a/script/enterprise-server-releases/create-webhook-files.js +++ b/script/enterprise-server-releases/create-webhook-files.js @@ -15,9 +15,17 @@ const payloadsDir = 'lib/webhooks/static' // [end-readme] program - .description('Create new payload files in lib/webhooks/static/<new_version> based on an existing version.') - .option('-n, --newVersion <version>', 'The version to copy the payloads to. Must be in <plan@release> format.') - .option('-o, --oldVersion <version>', 'The version to copy the payloads from. Must be in <plan@release> format.') + .description( + 'Create new payload files in lib/webhooks/static/<new_version> based on an existing version.' + ) + .option( + '-n, --newVersion <version>', + 'The version to copy the payloads to. Must be in <plan@release> format.' + ) + .option( + '-o, --oldVersion <version>', + 'The version to copy the payloads from. Must be in <plan@release> format.' + ) .parse(process.argv) const newVersion = program.opts().newVersion @@ -28,8 +36,12 @@ if (!(newVersion && oldVersion)) { process.exit(1) } -if (!(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).includes(oldVersion))) { - console.log('Error! You must provide the full name of a currently supported version, e.g., enterprise-server@2.22.') +if ( + !(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).includes(oldVersion)) +) { + console.log( + 'Error! You must provide the full name of a currently supported version, e.g., enterprise-server@2.22.' + ) process.exit(1) } @@ -43,7 +55,7 @@ const destDir = path.join(payloadsDir, newVersionDirName) mkdirp(destDir) // copy the files -fs.readdirSync(srcDir).forEach(file => { +fs.readdirSync(srcDir).forEach((file) => { const srcFile = path.join(srcDir, file) const destFile = path.join(destDir, file) fs.copyFileSync(srcFile, destFile) diff --git a/script/enterprise-server-releases/ghes-to-ghae-versioning.js b/script/enterprise-server-releases/ghes-to-ghae-versioning.js index 175e2b78dfec..68436c83e859 100755 --- a/script/enterprise-server-releases/ghes-to-ghae-versioning.js +++ b/script/enterprise-server-releases/ghes-to-ghae-versioning.js @@ -22,9 +22,17 @@ const translationsPath = path.join(process.cwd(), 'translations') // [end-readme] program - .description('Add versions frontmatter and Liquid conditionals for GitHub AE based on a given Enterprise Server release. Runs on all content by default.') - .option('-r, --ghes-release <RELEASE>', 'The Enterprise Server release to base AE versioning on. Example: 2.23') - .option('-p, --products [OPTIONAL PRODUCT_IDS...]', 'Optional list of space-separated product IDs. Example: admin github developers') + .description( + 'Add versions frontmatter and Liquid conditionals for GitHub AE based on a given Enterprise Server release. Runs on all content by default.' + ) + .option( + '-r, --ghes-release <RELEASE>', + 'The Enterprise Server release to base AE versioning on. Example: 2.23' + ) + .option( + '-p, --products [OPTIONAL PRODUCT_IDS...]', + 'Optional list of space-separated product IDs. Example: admin github developers' + ) .option('-t, --translations', 'Run the script on content and data in translations too.') .parse(process.argv) @@ -58,21 +66,23 @@ console.log('Working...\n') const englishContentFiles = walkContent(contentPath) const englishDataFiles = walkData(dataPath) -function walkContent (dirPath) { +function walkContent(dirPath) { const productArray = products || [''] - return productArray.map(product => { - dirPath = path.join(contentPath, product) - return walk(dirPath, { includeBasePath: true, directories: false }) - .filter(file => file.includes('/content/')) - .filter(file => file.endsWith('.md')) - .filter(file => !file.endsWith('README.md')) - }).flat() + return productArray + .map((product) => { + dirPath = path.join(contentPath, product) + return walk(dirPath, { includeBasePath: true, directories: false }) + .filter((file) => file.includes('/content/')) + .filter((file) => file.endsWith('.md')) + .filter((file) => !file.endsWith('README.md')) + }) + .flat() } -function walkData (dirPath) { +function walkData(dirPath) { return walk(dirPath, { includeBasePath: true, directories: false }) - .filter(file => file.includes('/data/reusables') || file.includes('/data/variables')) - .filter(file => !file.endsWith('README.md')) + .filter((file) => file.includes('/data/reusables') || file.includes('/data/variables')) + .filter((file) => !file.endsWith('README.md')) } let allContentFiles, allDataFiles @@ -87,57 +97,61 @@ if (translations) { } // Update the data files -allDataFiles - .forEach(file => { - const dataContent = fs.readFileSync(file, 'utf8') +allDataFiles.forEach((file) => { + const dataContent = fs.readFileSync(file, 'utf8') - const conditionalsToUpdate = getConditionalsToUpdate(dataContent) - if (!conditionalsToUpdate.length) return + const conditionalsToUpdate = getConditionalsToUpdate(dataContent) + if (!conditionalsToUpdate.length) return - // Update Liquid in data files - const newDataContent = updateLiquid(conditionalsToUpdate, dataContent) + // Update Liquid in data files + const newDataContent = updateLiquid(conditionalsToUpdate, dataContent) - fs.writeFileSync(file, newDataContent) - }) + fs.writeFileSync(file, newDataContent) +}) // Update the content files -allContentFiles - .forEach(file => { - const { data, content } = frontmatter(fs.readFileSync(file, 'utf8')) - - // Return early if the current page frontmatter does not apply to either GHAE or the given GHES release - if (!(data.versions['github-ae'] || versionSatisfiesRange(ghesRelease, data.versions['enterprise-server']))) return - - const conditionalsToUpdate = getConditionalsToUpdate(content) - if (!conditionalsToUpdate.length) return - - // Update Liquid in content files - const newContent = updateLiquid(conditionalsToUpdate, content) - - // Add frontmatter version - data.versions['github-ae'] = '*' - - // Update Liquid in frontmatter props - Object.keys(data) - .filter(key => typeof data[key] === 'string') - .forEach(key => { - const conditionalsToUpdate = getConditionalsToUpdate(data[key]) - if (!conditionalsToUpdate.length) return - data[key] = updateLiquid(conditionalsToUpdate, data[key]) - }) - - fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) - }) - -function getConditionalsToUpdate (content) { +allContentFiles.forEach((file) => { + const { data, content } = frontmatter(fs.readFileSync(file, 'utf8')) + + // Return early if the current page frontmatter does not apply to either GHAE or the given GHES release + if ( + !( + data.versions['github-ae'] || + versionSatisfiesRange(ghesRelease, data.versions['enterprise-server']) + ) + ) + return + + const conditionalsToUpdate = getConditionalsToUpdate(content) + if (!conditionalsToUpdate.length) return + + // Update Liquid in content files + const newContent = updateLiquid(conditionalsToUpdate, content) + + // Add frontmatter version + data.versions['github-ae'] = '*' + + // Update Liquid in frontmatter props + Object.keys(data) + .filter((key) => typeof data[key] === 'string') + .forEach((key) => { + const conditionalsToUpdate = getConditionalsToUpdate(data[key]) + if (!conditionalsToUpdate.length) return + data[key] = updateLiquid(conditionalsToUpdate, data[key]) + }) + + fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) +}) + +function getConditionalsToUpdate(content) { return getLiquidConditionals(content, 'ifversion') - .filter(c => !c.includes('ghae')) - .filter(c => doesReleaseSatisfyConditional(c.match(ghesRegex))) + .filter((c) => !c.includes('ghae')) + .filter((c) => doesReleaseSatisfyConditional(c.match(ghesRegex))) } -function updateLiquid (conditionalsToUpdate, content) { +function updateLiquid(conditionalsToUpdate, content) { let newContent = content - conditionalsToUpdate.forEach(cond => { + conditionalsToUpdate.forEach((cond) => { const oldConditional = `{% ifversion ${cond} %}` const newConditional = `{% ifversion ${cond.concat(' or ghae')} %}` const oldConditionalRegex = new RegExp(escapeRegExp(oldConditional), 'g') @@ -151,7 +165,7 @@ function updateLiquid (conditionalsToUpdate, content) { console.log('Done!') -function doesReleaseSatisfyConditional (ghesMatch) { +function doesReleaseSatisfyConditional(ghesMatch) { if (!ghesMatch) return false // Operator (e.g., <) diff --git a/script/enterprise-server-releases/release-banner.js b/script/enterprise-server-releases/release-banner.js index 7e0bb3422242..b178e91fd432 100755 --- a/script/enterprise-server-releases/release-banner.js +++ b/script/enterprise-server-releases/release-banner.js @@ -22,7 +22,10 @@ program .storeOptionsAsProperties(false) .passCommandToAction(false) .option(`-a, --action <${allowedActions.join(' or ')}>`, 'Create or remove the banner.') - .option('-v, --version <version>', 'The version the banner applies to. Must be in <plan@release> format.') + .option( + '-v, --version <version>', + 'The version the banner applies to. Must be in <plan@release> format.' + ) .parse(process.argv) const options = program.opts() @@ -32,8 +35,10 @@ if (!allowedActions.includes(options.action)) { process.exit(1) } -if (!(Object.keys(allVersions).includes(options.version))) { - console.log('Error! You must specify --version with the full name of a supported version, e.g., enterprise-server@2.22.') +if (!Object.keys(allVersions).includes(options.version)) { + console.log( + 'Error! You must specify --version with the full name of a supported version, e.g., enterprise-server@2.22.' + ) process.exit(1) } diff --git a/script/fix-translation-errors.js b/script/fix-translation-errors.js index fc9a0ab34293..1464c5ecf16f 100755 --- a/script/fix-translation-errors.js +++ b/script/fix-translation-errors.js @@ -21,12 +21,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) // // [end-readme] - main() -async function main () { +async function main() { const fixableFmProps = Object.keys(fm.schema.properties) - .filter(property => !fm.schema.properties[property].translatable) + .filter((property) => !fm.schema.properties[property].translatable) .sort() const fixableYmlProps = ['date'] @@ -40,12 +39,13 @@ async function main () { } if (path.endsWith('yml')) { - let data; let errors = [] + let data + let errors = [] try { data = yaml.load(fileContents) } catch {} if (data && schema) { - ({ errors } = revalidator.validate(data, schema)) + ;({ errors } = revalidator.validate(data, schema)) } return { data, errors, content: null } } else { @@ -53,7 +53,8 @@ async function main () { } } - const cmd = 'git -c diff.renameLimit=10000 diff --name-only origin/main | egrep "^translations/.*/(content/.+.md|data/release-notes/.*.yml)$"' + const cmd = + 'git -c diff.renameLimit=10000 diff --name-only origin/main | egrep "^translations/.*/(content/.+.md|data/release-notes/.*.yml)$"' const changedFilesRelPaths = execSync(cmd).toString().split('\n') for (const relPath of changedFilesRelPaths) { diff --git a/script/get-new-version-path.js b/script/get-new-version-path.js index 24f041a33c90..6726b79c96eb 100755 --- a/script/get-new-version-path.js +++ b/script/get-new-version-path.js @@ -5,7 +5,8 @@ import patterns from '../lib/patterns.js' import { deprecated } from '../lib/enterprise-server-releases.js' import { getNewVersionedPath, getOldVersionFromOldPath } from '../lib/old-versions-utils.js' -const usage = 'must provide a path like "/github/getting-started" or "/enterprise/2.20/user/github/getting-started", with or without language code' +const usage = + 'must provide a path like "/github/getting-started" or "/enterprise/2.20/user/github/getting-started", with or without language code' // [start-readme] // diff --git a/script/graphql/build-changelog.js b/script/graphql/build-changelog.js index b6c7f617d70c..830c9e7e38fc 100644 --- a/script/graphql/build-changelog.js +++ b/script/graphql/build-changelog.js @@ -10,7 +10,7 @@ import fs from 'fs' * @param {string} targetPath * @return {void} */ -export function prependDatedEntry (changelogEntry, targetPath) { +export function prependDatedEntry(changelogEntry, targetPath) { // Build a `yyyy-mm-dd`-formatted date string // and tag the changelog entry with it const todayString = new Date().toISOString().slice(0, 10) @@ -36,7 +36,13 @@ export function prependDatedEntry (changelogEntry, targetPath) { * @param {Array<object>} [newUpcomingChanges] * @return {object?} */ -export async function createChangelogEntry (oldSchemaString, newSchemaString, previews, oldUpcomingChanges, newUpcomingChanges) { +export async function createChangelogEntry( + oldSchemaString, + newSchemaString, + previews, + oldUpcomingChanges, + newUpcomingChanges +) { // Create schema objects out of the strings const oldSchema = await loadSchema(oldSchemaString) const newSchema = await loadSchema(newSchemaString) @@ -50,17 +56,23 @@ export async function createChangelogEntry (oldSchemaString, newSchemaString, pr } else if (CHANGES_TO_IGNORE.includes(change.type)) { // Do nothing } else { - throw new Error('This change type should be added to CHANGES_TO_REPORT or CHANGES_TO_IGNORE: ' + change.type) + throw new Error( + 'This change type should be added to CHANGES_TO_REPORT or CHANGES_TO_IGNORE: ' + change.type + ) } }) - const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges(changesToReport, previews) + const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges( + changesToReport, + previews + ) const addedUpcomingChanges = newUpcomingChanges.filter(function (change) { // Manually check each of `newUpcomingChanges` for an equivalent entry // in `oldUpcomingChanges`. return !oldUpcomingChanges.find(function (oldChange) { - return (oldChange.location === change.location && + return ( + oldChange.location === change.location && oldChange.date === change.date && oldChange.description === change.description ) @@ -68,27 +80,36 @@ export async function createChangelogEntry (oldSchemaString, newSchemaString, pr }) // If there were any changes, create a changelog entry - if (schemaChangesToReport.length > 0 || previewChangesToReport.length > 0 || addedUpcomingChanges.length > 0) { + if ( + schemaChangesToReport.length > 0 || + previewChangesToReport.length > 0 || + addedUpcomingChanges.length > 0 + ) { const changelogEntry = { schemaChanges: [], previewChanges: [], - upcomingChanges: [] + upcomingChanges: [], } const schemaChange = { title: 'The GraphQL schema includes these changes:', // Replace single quotes which wrap field/argument/type names with backticks - changes: cleanMessagesFromChanges(schemaChangesToReport) + changes: cleanMessagesFromChanges(schemaChangesToReport), } changelogEntry.schemaChanges.push(schemaChange) for (const previewTitle in previewChangesToReport) { const previewChanges = previewChangesToReport[previewTitle] const cleanTitle = cleanPreviewTitle(previewTitle) - const entryTitle = 'The [' + cleanTitle + '](/graphql/overview/schema-previews#' + previewAnchor(cleanTitle) + ') includes these changes:' + const entryTitle = + 'The [' + + cleanTitle + + '](/graphql/overview/schema-previews#' + + previewAnchor(cleanTitle) + + ') includes these changes:' changelogEntry.previewChanges.push({ title: entryTitle, - changes: cleanMessagesFromChanges(previewChanges.changes) + changes: cleanMessagesFromChanges(previewChanges.changes), }) } @@ -100,7 +121,7 @@ export async function createChangelogEntry (oldSchemaString, newSchemaString, pr const description = change.description const date = change.date.split('T')[0] return 'On member `' + location + '`:' + description + ' **Effective ' + date + '**.' - }) + }), }) } @@ -115,7 +136,7 @@ export async function createChangelogEntry (oldSchemaString, newSchemaString, pr * @param {string} title * @return {string} */ -export function cleanPreviewTitle (title) { +export function cleanPreviewTitle(title) { if (title === 'UpdateRefsPreview') { title = 'Update refs preview' } else if (title === 'MergeInfoPreview') { @@ -131,8 +152,8 @@ export function cleanPreviewTitle (title) { * (ported from graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L281) * @param {string} [previewTitle] * @return {string} -*/ -export function previewAnchor (previewTitle) { + */ +export function previewAnchor(previewTitle) { return previewTitle .toLowerCase() .replace(/ /g, '-') @@ -144,7 +165,7 @@ export function previewAnchor (previewTitle) { * @param {Array<object>} changes * @return {Array<string>} */ -export function cleanMessagesFromChanges (changes) { +export function cleanMessagesFromChanges(changes) { return changes.map(function (change) { // replace single quotes around graphql names with backticks, // to match previous behavior from graphql-schema-comparator @@ -161,7 +182,7 @@ export function cleanMessagesFromChanges (changes) { * @param {object} previews * @return {object} */ -export function segmentPreviewChanges (changesToReport, previews) { +export function segmentPreviewChanges(changesToReport, previews) { // Build a map of `{ path => previewTitle` } // for easier lookup of change to preview const pathToPreview = {} @@ -188,10 +209,12 @@ export function segmentPreviewChanges (changesToReport, previews) { pathParts.pop() } if (previewTitle) { - previewChanges = changesByPreview[previewTitle] || (changesByPreview[previewTitle] = { - title: previewTitle, - changes: [] - }) + previewChanges = + changesByPreview[previewTitle] || + (changesByPreview[previewTitle] = { + title: previewTitle, + changes: [], + }) previewChanges.changes.push(change) } else { schemaChanges.push(change) @@ -228,7 +251,7 @@ const CHANGES_TO_REPORT = [ ChangeType.UnionMemberAdded, ChangeType.SchemaQueryTypeChanged, ChangeType.SchemaMutationTypeChanged, - ChangeType.SchemaSubscriptionTypeChanged + ChangeType.SchemaSubscriptionTypeChanged, ] const CHANGES_TO_IGNORE = [ @@ -260,7 +283,7 @@ const CHANGES_TO_IGNORE = [ ChangeType.InputFieldDescriptionChanged, ChangeType.TypeDescriptionChanged, ChangeType.TypeDescriptionRemoved, - ChangeType.TypeDescriptionAdded + ChangeType.TypeDescriptionAdded, ] export default { createChangelogEntry, cleanPreviewTitle, previewAnchor, prependDatedEntry } diff --git a/script/graphql/update-files.js b/script/graphql/update-files.js index 024faa82245a..5a9f2297601b 100755 --- a/script/graphql/update-files.js +++ b/script/graphql/update-files.js @@ -17,7 +17,9 @@ import loadData from '../../lib/site-data.js' const mkdirp = xMkdirp.sync const graphqlDataDir = path.join(process.cwd(), 'data/graphql') const graphqlStaticDir = path.join(process.cwd(), 'lib/graphql/static') -const dataFilenames = JSON.parse(fs.readFileSync(path.join(process.cwd(), './script/graphql/utils/data-filenames.json'))) +const dataFilenames = JSON.parse( + fs.readFileSync(path.join(process.cwd(), './script/graphql/utils/data-filenames.json')) +) // check for required PAT if (!process.env.GITHUB_TOKEN) { @@ -31,7 +33,7 @@ const currentLanguage = 'en' main() -async function main () { +async function main() { try { const previewsJson = {} const upcomingChangesJson = {} @@ -43,7 +45,7 @@ async function main () { // create a bare minimum context for rendering the graphql-object.html layout const context = { currentLanguage, - site: siteData[currentLanguage].site + site: siteData[currentLanguage].site, } for (const version of versionsToBuild) { @@ -55,7 +57,9 @@ async function main () { // 1. UPDATE PREVIEWS const previewsPath = getDataFilepath('previews', graphqlVersion) - const safeForPublicPreviews = yaml.load(await getRemoteRawContent(previewsPath, graphqlVersion)) + const safeForPublicPreviews = yaml.load( + await getRemoteRawContent(previewsPath, graphqlVersion) + ) updateFile(previewsPath, yaml.dump(safeForPublicPreviews)) previewsJson[graphqlVersion] = processPreviews(safeForPublicPreviews) @@ -73,7 +77,10 @@ async function main () { const latestSchema = await getRemoteRawContent(schemaPath, graphqlVersion) updateFile(schemaPath, latestSchema) const schemaJsonPerVersion = await processSchemas(latestSchema, safeForPublicPreviews) - updateStaticFile(schemaJsonPerVersion, path.join(graphqlStaticDir, `schema-${graphqlVersion}.json`)) + updateStaticFile( + schemaJsonPerVersion, + path.join(graphqlStaticDir, `schema-${graphqlVersion}.json`) + ) // Add some version specific data to the context context.graphql = { schemaForCurrentVersion: schemaJsonPerVersion } @@ -98,7 +105,10 @@ async function main () { yaml.load(safeForPublicChanges).upcoming_changes ) if (changelogEntry) { - prependDatedEntry(changelogEntry, path.join(process.cwd(), 'lib/graphql/static/changelog.json')) + prependDatedEntry( + changelogEntry, + path.join(process.cwd(), 'lib/graphql/static/changelog.json') + ) } } } @@ -106,7 +116,10 @@ async function main () { updateStaticFile(previewsJson, path.join(graphqlStaticDir, 'previews.json')) updateStaticFile(upcomingChangesJson, path.join(graphqlStaticDir, 'upcoming-changes.json')) updateStaticFile(prerenderedObjects, path.join(graphqlStaticDir, 'prerendered-objects.json')) - updateStaticFile(prerenderedInputObjects, path.join(graphqlStaticDir, 'prerendered-input-objects.json')) + updateStaticFile( + prerenderedInputObjects, + path.join(graphqlStaticDir, 'prerendered-input-objects.json') + ) // Ensure the YAML linter runs before checkinging in files execSync('npx prettier -w "**/*.{yml,yaml}"') @@ -117,10 +130,10 @@ async function main () { } // get latest from github/github -async function getRemoteRawContent (filepath, graphqlVersion) { +async function getRemoteRawContent(filepath, graphqlVersion) { const options = { owner: 'github', - repo: 'github' + repo: 'github', } // find the relevant branch in github/github and set it as options.ref @@ -133,7 +146,7 @@ async function getRemoteRawContent (filepath, graphqlVersion) { } // find the relevant filepath in script/graphql/utils/data-filenames.json -function getDataFilepath (id, graphqlVersion) { +function getDataFilepath(id, graphqlVersion) { const versionType = getVersionType(graphqlVersion) // for example, dataFilenames['schema']['ghes'] = schema.docs-enterprise.graphql @@ -146,7 +159,7 @@ function getDataFilepath (id, graphqlVersion) { return path.join(graphqlDataDir, dataSubdir, filename) } -async function setBranchAsRef (options, graphqlVersion, branch = false) { +async function setBranchAsRef(options, graphqlVersion, branch = false) { const versionType = getVersionType(graphqlVersion) const defaultBranch = 'master' @@ -154,7 +167,7 @@ async function setBranchAsRef (options, graphqlVersion, branch = false) { dotcom: defaultBranch, ghes: `enterprise-${graphqlVersion.replace('ghes-', '')}-release`, // TODO confirm the below is accurate after the release branch is created - ghae: 'github-ae-release' + ghae: 'github-ae-release', } // the first time this runs, it uses the branch found for the version above @@ -175,17 +188,17 @@ async function setBranchAsRef (options, graphqlVersion, branch = false) { // given a GraphQL version like `ghes-2.22`, return `ghes`; // given a GraphQL version like `ghae` or `dotcom`, return as is -function getVersionType (graphqlVersion) { +function getVersionType(graphqlVersion) { return graphqlVersion.split('-')[0] } -function updateFile (filepath, content) { +function updateFile(filepath, content) { console.log(`fetching latest data to ${filepath}`) mkdirp(path.dirname(filepath)) fs.writeFileSync(filepath, content, 'utf8') } -function updateStaticFile (json, filepath) { +function updateStaticFile(json, filepath) { const jsonString = JSON.stringify(json, null, 2) updateFile(filepath, jsonString) } diff --git a/script/graphql/utils/prerender-input-objects.js b/script/graphql/utils/prerender-input-objects.js index 34b55cba504a..ee1adb275b9f 100644 --- a/script/graphql/utils/prerender-input-objects.js +++ b/script/graphql/utils/prerender-input-objects.js @@ -6,9 +6,12 @@ import { liquid } from '../../../lib/render-content/index.js' import getMiniTocItems from '../../../lib/get-mini-toc-items.js' import rewriteLocalLinks from '../../../lib/rewrite-local-links.js' const includes = path.join(process.cwd(), 'includes') -const inputObjectIncludeFile = fs.readFileSync(path.join(includes, 'graphql-input-object.html'), 'utf8') +const inputObjectIncludeFile = fs.readFileSync( + path.join(includes, 'graphql-input-object.html'), + 'utf8' +) -export default async function prerenderInputObjects (context) { +export default async function prerenderInputObjects(context) { const inputObjectsArray = [] // render the graphql-object.html layout for every object @@ -25,6 +28,6 @@ export default async function prerenderInputObjects (context) { return { html: inputObjectsHtml, - miniToc: getMiniTocItems(inputObjectsHtml) + miniToc: getMiniTocItems(inputObjectsHtml), } } diff --git a/script/graphql/utils/prerender-objects.js b/script/graphql/utils/prerender-objects.js index 56ac8233bc13..95974947cef4 100644 --- a/script/graphql/utils/prerender-objects.js +++ b/script/graphql/utils/prerender-objects.js @@ -8,7 +8,7 @@ import rewriteLocalLinks from '../../../lib/rewrite-local-links.js' const includes = path.join(process.cwd(), 'includes') const objectIncludeFile = fs.readFileSync(path.join(includes, 'graphql-object.html'), 'utf8') -export default async function prerenderObjects (context) { +export default async function prerenderObjects(context) { const objectsArray = [] // render the graphql-object.html layout for every object @@ -25,6 +25,6 @@ export default async function prerenderObjects (context) { return { html: objectsHtml, - miniToc: getMiniTocItems(objectsHtml) + miniToc: getMiniTocItems(objectsHtml), } } diff --git a/script/graphql/utils/process-previews.js b/script/graphql/utils/process-previews.js index 1c9bc986654d..96df6d8b6dbf 100644 --- a/script/graphql/utils/process-previews.js +++ b/script/graphql/utils/process-previews.js @@ -4,20 +4,19 @@ import GithubSlugger from 'github-slugger' const slugger = new GithubSlugger() const inputOrPayload = /(Input|Payload)$/m -export default function processPreviews (previews) { +export default function processPreviews(previews) { // clean up raw yml data - previews.forEach(preview => { + previews.forEach((preview) => { // remove any extra info that follows a hyphen - preview.title = sentenceCase(preview.title.replace(/ -.+/, '')) - .replace('it hub', 'itHub') // fix overcorrected `git hub` from sentenceCasing + preview.title = sentenceCase(preview.title.replace(/ -.+/, '')).replace('it hub', 'itHub') // fix overcorrected `git hub` from sentenceCasing // Add `preview` to the end of titles if needed - preview.title = preview.title.endsWith('preview') - ? preview.title - : `${preview.title} preview` + preview.title = preview.title.endsWith('preview') ? preview.title : `${preview.title} preview` // filter out schema members that end in `Input` or `Payload` - preview.toggled_on = preview.toggled_on.filter(schemaMember => !inputOrPayload.test(schemaMember)) + preview.toggled_on = preview.toggled_on.filter( + (schemaMember) => !inputOrPayload.test(schemaMember) + ) // remove unnecessary leading colon preview.toggled_by = preview.toggled_by.replace(':', '') diff --git a/script/graphql/utils/process-schemas.js b/script/graphql/utils/process-schemas.js index d971f9faa070..3e0fb4fda7c9 100755 --- a/script/graphql/utils/process-schemas.js +++ b/script/graphql/utils/process-schemas.js @@ -5,27 +5,28 @@ import helpers from './schema-helpers.js' import fs from 'fs' import path from 'path' -const externalScalars = JSON.parse(fs.readFileSync(path.join(process.cwd(), './lib/graphql/non-schema-scalars.json'))) - .map(scalar => { - scalar.id = helpers.getId(scalar.name) - scalar.href = helpers.getFullLink('scalars', scalar.id) - return scalar - }) +const externalScalars = JSON.parse( + fs.readFileSync(path.join(process.cwd(), './lib/graphql/non-schema-scalars.json')) +).map((scalar) => { + scalar.id = helpers.getId(scalar.name) + scalar.href = helpers.getFullLink('scalars', scalar.id) + return scalar +}) // select and format all the data from the schema that we need for the docs // used in the build step by script/graphql/build-static-files.js -export default async function processSchemas (idl, previewsPerVersion) { +export default async function processSchemas(idl, previewsPerVersion) { const schemaAST = parse(idl.toString()) const schema = buildASTSchema(schemaAST) // list of objects is used when processing mutations - const objectsInSchema = schemaAST.definitions.filter(def => def.kind === 'ObjectTypeDefinition') + const objectsInSchema = schemaAST.definitions.filter((def) => def.kind === 'ObjectTypeDefinition') const data = {} data.queries = { connections: [], - fields: [] + fields: [], } data.mutations = [] data.objects = [] @@ -35,318 +36,410 @@ export default async function processSchemas (idl, previewsPerVersion) { data.inputObjects = [] data.scalars = [] - await Promise.all(schemaAST.definitions.map(async (def) => { - // QUERIES - if (def.name.value === 'Query') { - await Promise.all(def.fields.map(async (field) => { - const query = {} - const queryArgs = [] - - query.name = field.name.value - query.type = helpers.getType(field) - query.kind = helpers.getTypeKind(query.type, schema) - query.id = helpers.getId(query.type) - query.href = helpers.getFullLink(query.kind, query.id) - query.description = await helpers.getDescription(field.description.value) - query.isDeprecated = helpers.getDeprecationStatus(field.directives, query.name) - query.deprecationReason = await helpers.getDeprecationReason(field.directives, query) - query.preview = await helpers.getPreview(field.directives, query, previewsPerVersion) - - await Promise.all(field.arguments.map(async (arg) => { - const queryArg = {} - queryArg.name = arg.name.value - queryArg.defaultValue = arg.defaultValue ? arg.defaultValue.value : undefined - queryArg.type = helpers.getType(arg) - queryArg.id = helpers.getId(queryArg.type) - queryArg.kind = helpers.getTypeKind(queryArg.type, schema) - queryArg.href = helpers.getFullLink(queryArg.kind, queryArg.id) - queryArg.description = await helpers.getDescription(arg.description.value) - queryArg.isDeprecated = helpers.getDeprecationStatus(arg.directives, queryArg.name) - queryArg.deprecationReason = await helpers.getDeprecationReason(arg.directives, queryArg) - queryArg.preview = await helpers.getPreview(arg.directives, queryArg, previewsPerVersion) - queryArgs.push(queryArg) - })) - - query.args = sortBy(queryArgs, 'name') - - // QUERY CONNECTIONS - // QUERY FIELDS - query.id.endsWith('connection') - ? data.queries.connections.push(query) - : data.queries.fields.push(query) - })) - - return - } - - // MUTATIONS - if (def.name.value === 'Mutation') { - await Promise.all(def.fields.map(async (field) => { - const mutation = {} - const inputFields = [] - const returnFields = [] - - mutation.name = field.name.value - mutation.kind = helpers.getKind(def.name.value) - mutation.id = helpers.getId(mutation.name) - mutation.href = helpers.getFullLink('mutations', mutation.id) - mutation.description = await helpers.getDescription(field.description.value) - mutation.isDeprecated = helpers.getDeprecationStatus(field.directives, mutation.name) - mutation.deprecationReason = await helpers.getDeprecationReason(field.directives, mutation) - mutation.preview = await helpers.getPreview(field.directives, mutation, previewsPerVersion) - - // there is only ever one input field argument, but loop anyway - await Promise.all(field.arguments.map(async (field) => { - const inputField = {} - inputField.name = field.name.value - inputField.type = helpers.getType(field) - inputField.id = helpers.getId(inputField.type) - inputField.kind = helpers.getTypeKind(inputField.type, schema) - inputField.href = helpers.getFullLink(inputField.kind, inputField.id) - inputFields.push(inputField) - })) - - mutation.inputFields = sortBy(inputFields, 'name') - - // get return fields - // first get the payload, then find payload object's fields. these are the mutation's return fields. - const returnType = helpers.getType(field) - const mutationReturnFields = objectsInSchema.find(obj => obj.name.value === returnType) - - if (!mutationReturnFields) console.log(`no return fields found for ${returnType}`) - - await Promise.all(mutationReturnFields.fields.map(async (field) => { - const returnField = {} - returnField.name = field.name.value - returnField.type = helpers.getType(field) - returnField.id = helpers.getId(returnField.type) - returnField.kind = helpers.getTypeKind(returnField.type, schema) - returnField.href = helpers.getFullLink(returnField.kind, returnField.id) - returnField.description = await helpers.getDescription(field.description.value) - returnField.isDeprecated = helpers.getDeprecationStatus(field.directives, returnField.name) - returnField.deprecationReason = await helpers.getDeprecationReason(field.directives, returnField) - returnField.preview = await helpers.getPreview(field.directives, returnField, previewsPerVersion) - returnFields.push(returnField) - })) - - mutation.returnFields = sortBy(returnFields, 'name') - - data.mutations.push(mutation) - })) - return - } - - // OBJECTS - if (def.kind === 'ObjectTypeDefinition') { - // objects ending with 'Payload' are only used to derive mutation values - // they are not included in the objects docs - if (def.name.value.endsWith('Payload')) return - - const object = {} - const objectImplements = [] - const objectFields = [] - - object.name = def.name.value - object.kind = helpers.getKind(def.kind) - object.id = helpers.getId(object.name) - object.href = helpers.getFullLink('objects', object.id) - object.description = await helpers.getDescription(def.description.value) - object.isDeprecated = helpers.getDeprecationStatus(def.directives, object.name) - object.deprecationReason = await helpers.getDeprecationReason(def.directives, object) - object.preview = await helpers.getPreview(def.directives, object, previewsPerVersion) - - // an object's interfaces render in the `Implements` section - // interfaces do not have directives so they cannot be under preview/deprecated - if (def.interfaces.length) { - await Promise.all(def.interfaces.map(async (graphqlInterface) => { - const objectInterface = {} - objectInterface.name = graphqlInterface.name.value - objectInterface.id = helpers.getId(objectInterface.name) - objectInterface.href = helpers.getFullLink('interfaces', objectInterface.id) - objectImplements.push(objectInterface) - })) + await Promise.all( + schemaAST.definitions.map(async (def) => { + // QUERIES + if (def.name.value === 'Query') { + await Promise.all( + def.fields.map(async (field) => { + const query = {} + const queryArgs = [] + + query.name = field.name.value + query.type = helpers.getType(field) + query.kind = helpers.getTypeKind(query.type, schema) + query.id = helpers.getId(query.type) + query.href = helpers.getFullLink(query.kind, query.id) + query.description = await helpers.getDescription(field.description.value) + query.isDeprecated = helpers.getDeprecationStatus(field.directives, query.name) + query.deprecationReason = await helpers.getDeprecationReason(field.directives, query) + query.preview = await helpers.getPreview(field.directives, query, previewsPerVersion) + + await Promise.all( + field.arguments.map(async (arg) => { + const queryArg = {} + queryArg.name = arg.name.value + queryArg.defaultValue = arg.defaultValue ? arg.defaultValue.value : undefined + queryArg.type = helpers.getType(arg) + queryArg.id = helpers.getId(queryArg.type) + queryArg.kind = helpers.getTypeKind(queryArg.type, schema) + queryArg.href = helpers.getFullLink(queryArg.kind, queryArg.id) + queryArg.description = await helpers.getDescription(arg.description.value) + queryArg.isDeprecated = helpers.getDeprecationStatus(arg.directives, queryArg.name) + queryArg.deprecationReason = await helpers.getDeprecationReason( + arg.directives, + queryArg + ) + queryArg.preview = await helpers.getPreview( + arg.directives, + queryArg, + previewsPerVersion + ) + queryArgs.push(queryArg) + }) + ) + + query.args = sortBy(queryArgs, 'name') + + // QUERY CONNECTIONS + // QUERY FIELDS + query.id.endsWith('connection') + ? data.queries.connections.push(query) + : data.queries.fields.push(query) + }) + ) + + return + } + + // MUTATIONS + if (def.name.value === 'Mutation') { + await Promise.all( + def.fields.map(async (field) => { + const mutation = {} + const inputFields = [] + const returnFields = [] + + mutation.name = field.name.value + mutation.kind = helpers.getKind(def.name.value) + mutation.id = helpers.getId(mutation.name) + mutation.href = helpers.getFullLink('mutations', mutation.id) + mutation.description = await helpers.getDescription(field.description.value) + mutation.isDeprecated = helpers.getDeprecationStatus(field.directives, mutation.name) + mutation.deprecationReason = await helpers.getDeprecationReason( + field.directives, + mutation + ) + mutation.preview = await helpers.getPreview( + field.directives, + mutation, + previewsPerVersion + ) + + // there is only ever one input field argument, but loop anyway + await Promise.all( + field.arguments.map(async (field) => { + const inputField = {} + inputField.name = field.name.value + inputField.type = helpers.getType(field) + inputField.id = helpers.getId(inputField.type) + inputField.kind = helpers.getTypeKind(inputField.type, schema) + inputField.href = helpers.getFullLink(inputField.kind, inputField.id) + inputFields.push(inputField) + }) + ) + + mutation.inputFields = sortBy(inputFields, 'name') + + // get return fields + // first get the payload, then find payload object's fields. these are the mutation's return fields. + const returnType = helpers.getType(field) + const mutationReturnFields = objectsInSchema.find( + (obj) => obj.name.value === returnType + ) + + if (!mutationReturnFields) console.log(`no return fields found for ${returnType}`) + + await Promise.all( + mutationReturnFields.fields.map(async (field) => { + const returnField = {} + returnField.name = field.name.value + returnField.type = helpers.getType(field) + returnField.id = helpers.getId(returnField.type) + returnField.kind = helpers.getTypeKind(returnField.type, schema) + returnField.href = helpers.getFullLink(returnField.kind, returnField.id) + returnField.description = await helpers.getDescription(field.description.value) + returnField.isDeprecated = helpers.getDeprecationStatus( + field.directives, + returnField.name + ) + returnField.deprecationReason = await helpers.getDeprecationReason( + field.directives, + returnField + ) + returnField.preview = await helpers.getPreview( + field.directives, + returnField, + previewsPerVersion + ) + returnFields.push(returnField) + }) + ) + + mutation.returnFields = sortBy(returnFields, 'name') + + data.mutations.push(mutation) + }) + ) + return + } + + // OBJECTS + if (def.kind === 'ObjectTypeDefinition') { + // objects ending with 'Payload' are only used to derive mutation values + // they are not included in the objects docs + if (def.name.value.endsWith('Payload')) return + + const object = {} + const objectImplements = [] + const objectFields = [] + + object.name = def.name.value + object.kind = helpers.getKind(def.kind) + object.id = helpers.getId(object.name) + object.href = helpers.getFullLink('objects', object.id) + object.description = await helpers.getDescription(def.description.value) + object.isDeprecated = helpers.getDeprecationStatus(def.directives, object.name) + object.deprecationReason = await helpers.getDeprecationReason(def.directives, object) + object.preview = await helpers.getPreview(def.directives, object, previewsPerVersion) + + // an object's interfaces render in the `Implements` section + // interfaces do not have directives so they cannot be under preview/deprecated + if (def.interfaces.length) { + await Promise.all( + def.interfaces.map(async (graphqlInterface) => { + const objectInterface = {} + objectInterface.name = graphqlInterface.name.value + objectInterface.id = helpers.getId(objectInterface.name) + objectInterface.href = helpers.getFullLink('interfaces', objectInterface.id) + objectImplements.push(objectInterface) + }) + ) + } + + // an object's fields render in the `Fields` section + if (def.fields.length) { + await Promise.all( + def.fields.map(async (field) => { + if (!field.description) return + const objectField = {} + + objectField.name = field.name.value + objectField.description = await helpers.getDescription(field.description.value) + objectField.type = helpers.getType(field) + objectField.id = helpers.getId(objectField.type) + objectField.kind = helpers.getTypeKind(objectField.type, schema) + objectField.href = helpers.getFullLink(objectField.kind, objectField.id) + objectField.arguments = await helpers.getArguments(field.arguments, schema) + objectField.isDeprecated = helpers.getDeprecationStatus(field.directives) + objectField.deprecationReason = await helpers.getDeprecationReason( + field.directives, + objectField + ) + objectField.preview = await helpers.getPreview( + field.directives, + objectField, + previewsPerVersion + ) + + objectFields.push(objectField) + }) + ) + } + + if (objectImplements.length) object.implements = sortBy(objectImplements, 'name') + if (objectFields.length) object.fields = sortBy(objectFields, 'name') + + data.objects.push(object) + return } - // an object's fields render in the `Fields` section - if (def.fields.length) { - await Promise.all(def.fields.map(async (field) => { - if (!field.description) return - const objectField = {} - - objectField.name = field.name.value - objectField.description = await helpers.getDescription(field.description.value) - objectField.type = helpers.getType(field) - objectField.id = helpers.getId(objectField.type) - objectField.kind = helpers.getTypeKind(objectField.type, schema) - objectField.href = helpers.getFullLink(objectField.kind, objectField.id) - objectField.arguments = await helpers.getArguments(field.arguments, schema) - objectField.isDeprecated = helpers.getDeprecationStatus(field.directives) - objectField.deprecationReason = await helpers.getDeprecationReason(field.directives, objectField) - objectField.preview = await helpers.getPreview(field.directives, objectField, previewsPerVersion) - - objectFields.push(objectField) - })) + // INTERFACES + if (def.kind === 'InterfaceTypeDefinition') { + const graphqlInterface = {} + const interfaceFields = [] + + graphqlInterface.name = def.name.value + graphqlInterface.kind = helpers.getKind(def.kind) + graphqlInterface.id = helpers.getId(graphqlInterface.name) + graphqlInterface.href = helpers.getFullLink('interfaces', graphqlInterface.id) + graphqlInterface.description = await helpers.getDescription(def.description.value) + graphqlInterface.isDeprecated = helpers.getDeprecationStatus(def.directives) + graphqlInterface.deprecationReason = await helpers.getDeprecationReason( + def.directives, + graphqlInterface + ) + graphqlInterface.preview = await helpers.getPreview( + def.directives, + graphqlInterface, + previewsPerVersion + ) + + // an interface's fields render in the "Fields" section + if (def.fields.length) { + await Promise.all( + def.fields.map(async (field) => { + if (!field.description) return + const interfaceField = {} + + interfaceField.name = field.name.value + interfaceField.description = await helpers.getDescription(field.description.value) + interfaceField.type = helpers.getType(field) + interfaceField.id = helpers.getId(interfaceField.type) + interfaceField.kind = helpers.getTypeKind(interfaceField.type, schema) + interfaceField.href = helpers.getFullLink(interfaceField.kind, interfaceField.id) + interfaceField.arguments = await helpers.getArguments(field.arguments, schema) + interfaceField.isDeprecated = helpers.getDeprecationStatus(field.directives) + interfaceField.deprecationReason = await helpers.getDeprecationReason( + field.directives, + interfaceField + ) + interfaceField.preview = await helpers.getPreview( + field.directives, + interfaceField, + previewsPerVersion + ) + + interfaceFields.push(interfaceField) + }) + ) + } + + graphqlInterface.fields = sortBy(interfaceFields, 'name') + + data.interfaces.push(graphqlInterface) + return } - if (objectImplements.length) object.implements = sortBy(objectImplements, 'name') - if (objectFields.length) object.fields = sortBy(objectFields, 'name') - - data.objects.push(object) - return - } - - // INTERFACES - if (def.kind === 'InterfaceTypeDefinition') { - const graphqlInterface = {} - const interfaceFields = [] - - graphqlInterface.name = def.name.value - graphqlInterface.kind = helpers.getKind(def.kind) - graphqlInterface.id = helpers.getId(graphqlInterface.name) - graphqlInterface.href = helpers.getFullLink('interfaces', graphqlInterface.id) - graphqlInterface.description = await helpers.getDescription(def.description.value) - graphqlInterface.isDeprecated = helpers.getDeprecationStatus(def.directives) - graphqlInterface.deprecationReason = await helpers.getDeprecationReason(def.directives, graphqlInterface) - graphqlInterface.preview = await helpers.getPreview(def.directives, graphqlInterface, previewsPerVersion) - - // an interface's fields render in the "Fields" section - if (def.fields.length) { - await Promise.all(def.fields.map(async (field) => { - if (!field.description) return - const interfaceField = {} - - interfaceField.name = field.name.value - interfaceField.description = await helpers.getDescription(field.description.value) - interfaceField.type = helpers.getType(field) - interfaceField.id = helpers.getId(interfaceField.type) - interfaceField.kind = helpers.getTypeKind(interfaceField.type, schema) - interfaceField.href = helpers.getFullLink(interfaceField.kind, interfaceField.id) - interfaceField.arguments = await helpers.getArguments(field.arguments, schema) - interfaceField.isDeprecated = helpers.getDeprecationStatus(field.directives) - interfaceField.deprecationReason = await helpers.getDeprecationReason(field.directives, interfaceField) - interfaceField.preview = await helpers.getPreview(field.directives, interfaceField, previewsPerVersion) - - interfaceFields.push(interfaceField) - })) + // ENUMS + if (def.kind === 'EnumTypeDefinition') { + const graphqlEnum = {} + const enumValues = [] + + graphqlEnum.name = def.name.value + graphqlEnum.kind = helpers.getKind(def.kind) + graphqlEnum.id = helpers.getId(graphqlEnum.name) + graphqlEnum.href = helpers.getFullLink('enums', graphqlEnum.id) + graphqlEnum.description = await helpers.getDescription(def.description.value) + graphqlEnum.isDeprecated = helpers.getDeprecationStatus(def.directives) + graphqlEnum.deprecationReason = await helpers.getDeprecationReason( + def.directives, + graphqlEnum + ) + graphqlEnum.preview = await helpers.getPreview( + def.directives, + graphqlEnum, + previewsPerVersion + ) + + await Promise.all( + def.values.map(async (value) => { + const enumValue = {} + enumValue.name = value.name.value + enumValue.description = await helpers.getDescription(value.description.value) + enumValues.push(enumValue) + }) + ) + + graphqlEnum.values = sortBy(enumValues, 'name') + + data.enums.push(graphqlEnum) + return } - graphqlInterface.fields = sortBy(interfaceFields, 'name') - - data.interfaces.push(graphqlInterface) - return - } - - // ENUMS - if (def.kind === 'EnumTypeDefinition') { - const graphqlEnum = {} - const enumValues = [] - - graphqlEnum.name = def.name.value - graphqlEnum.kind = helpers.getKind(def.kind) - graphqlEnum.id = helpers.getId(graphqlEnum.name) - graphqlEnum.href = helpers.getFullLink('enums', graphqlEnum.id) - graphqlEnum.description = await helpers.getDescription(def.description.value) - graphqlEnum.isDeprecated = helpers.getDeprecationStatus(def.directives) - graphqlEnum.deprecationReason = await helpers.getDeprecationReason(def.directives, graphqlEnum) - graphqlEnum.preview = await helpers.getPreview(def.directives, graphqlEnum, previewsPerVersion) - - await Promise.all(def.values.map(async (value) => { - const enumValue = {} - enumValue.name = value.name.value - enumValue.description = await helpers.getDescription(value.description.value) - enumValues.push(enumValue) - })) - - graphqlEnum.values = sortBy(enumValues, 'name') - - data.enums.push(graphqlEnum) - return - } - - // UNIONS - if (def.kind === 'UnionTypeDefinition') { - const union = {} - const possibleTypes = [] - - union.name = def.name.value - union.kind = helpers.getKind(def.kind) - union.id = helpers.getId(union.name) - union.href = helpers.getFullLink('unions', union.id) - union.description = await helpers.getDescription(def.description.value) - union.isDeprecated = helpers.getDeprecationStatus(def.directives) - union.deprecationReason = await helpers.getDeprecationReason(def.directives, union) - union.preview = await helpers.getPreview(def.directives, union, previewsPerVersion) - - // union types do not have directives so cannot be under preview/deprecated - await Promise.all(def.types.map(async (type) => { - const possibleType = {} - possibleType.name = type.name.value - possibleType.id = helpers.getId(possibleType.name) - possibleType.href = helpers.getFullLink('objects', possibleType.id) - possibleTypes.push(possibleType) - })) - - union.possibleTypes = sortBy(possibleTypes, 'name') - - data.unions.push(union) - return - } - - // INPUT OBJECTS - // NOTE: input objects ending with `Input` are NOT included in the v4 input objects sidebar - // but they are still present in the docs (e.g., https://developer.github.com/v4/input_object/acceptenterpriseadministratorinvitationinput/) - // so we will include them here - if (def.kind === 'InputObjectTypeDefinition') { - const inputObject = {} - const inputFields = [] - - inputObject.name = def.name.value - inputObject.kind = helpers.getKind(def.kind) - inputObject.id = helpers.getId(inputObject.name) - inputObject.href = helpers.getFullLink('input-objects', inputObject.id) - inputObject.description = await helpers.getDescription(def.description.value) - inputObject.isDeprecated = helpers.getDeprecationStatus(def.directives) - inputObject.deprecationReason = await helpers.getDeprecationReason(def.directives, inputObject) - inputObject.preview = await helpers.getPreview(def.directives, inputObject, previewsPerVersion) - - if (def.fields.length) { - await Promise.all(def.fields.map(async (field) => { - const inputField = {} - - inputField.name = field.name.value - inputField.description = await helpers.getDescription(field.description.value) - inputField.type = helpers.getType(field) - inputField.id = helpers.getId(inputField.type) - inputField.kind = helpers.getTypeKind(inputField.type, schema) - inputField.href = helpers.getFullLink(inputField.kind, inputField.id) - inputField.isDeprecated = helpers.getDeprecationStatus(field.directives) - inputField.deprecationReason = await helpers.getDeprecationReason(field.directives, inputField) - inputField.preview = await helpers.getPreview(field.directives, inputField, previewsPerVersion) - - inputFields.push(inputField) - })) + // UNIONS + if (def.kind === 'UnionTypeDefinition') { + const union = {} + const possibleTypes = [] + + union.name = def.name.value + union.kind = helpers.getKind(def.kind) + union.id = helpers.getId(union.name) + union.href = helpers.getFullLink('unions', union.id) + union.description = await helpers.getDescription(def.description.value) + union.isDeprecated = helpers.getDeprecationStatus(def.directives) + union.deprecationReason = await helpers.getDeprecationReason(def.directives, union) + union.preview = await helpers.getPreview(def.directives, union, previewsPerVersion) + + // union types do not have directives so cannot be under preview/deprecated + await Promise.all( + def.types.map(async (type) => { + const possibleType = {} + possibleType.name = type.name.value + possibleType.id = helpers.getId(possibleType.name) + possibleType.href = helpers.getFullLink('objects', possibleType.id) + possibleTypes.push(possibleType) + }) + ) + + union.possibleTypes = sortBy(possibleTypes, 'name') + + data.unions.push(union) + return } - inputObject.inputFields = sortBy(inputFields, 'name') - - data.inputObjects.push(inputObject) - return - } - - // SCALARS - if (def.kind === 'ScalarTypeDefinition') { - const scalar = {} - scalar.name = def.name.value - scalar.kind = helpers.getKind(def.kind) - scalar.id = helpers.getId(scalar.name) - scalar.href = helpers.getFullLink('scalars', scalar.id) - scalar.description = await helpers.getDescription(def.description.value) - scalar.isDeprecated = helpers.getDeprecationStatus(def.directives) - scalar.deprecationReason = await helpers.getDeprecationReason(def.directives, scalar) - scalar.preview = await helpers.getPreview(def.directives, scalar, previewsPerVersion) - data.scalars.push(scalar) - } - })) + // INPUT OBJECTS + // NOTE: input objects ending with `Input` are NOT included in the v4 input objects sidebar + // but they are still present in the docs (e.g., https://developer.github.com/v4/input_object/acceptenterpriseadministratorinvitationinput/) + // so we will include them here + if (def.kind === 'InputObjectTypeDefinition') { + const inputObject = {} + const inputFields = [] + + inputObject.name = def.name.value + inputObject.kind = helpers.getKind(def.kind) + inputObject.id = helpers.getId(inputObject.name) + inputObject.href = helpers.getFullLink('input-objects', inputObject.id) + inputObject.description = await helpers.getDescription(def.description.value) + inputObject.isDeprecated = helpers.getDeprecationStatus(def.directives) + inputObject.deprecationReason = await helpers.getDeprecationReason( + def.directives, + inputObject + ) + inputObject.preview = await helpers.getPreview( + def.directives, + inputObject, + previewsPerVersion + ) + + if (def.fields.length) { + await Promise.all( + def.fields.map(async (field) => { + const inputField = {} + + inputField.name = field.name.value + inputField.description = await helpers.getDescription(field.description.value) + inputField.type = helpers.getType(field) + inputField.id = helpers.getId(inputField.type) + inputField.kind = helpers.getTypeKind(inputField.type, schema) + inputField.href = helpers.getFullLink(inputField.kind, inputField.id) + inputField.isDeprecated = helpers.getDeprecationStatus(field.directives) + inputField.deprecationReason = await helpers.getDeprecationReason( + field.directives, + inputField + ) + inputField.preview = await helpers.getPreview( + field.directives, + inputField, + previewsPerVersion + ) + + inputFields.push(inputField) + }) + ) + } + + inputObject.inputFields = sortBy(inputFields, 'name') + + data.inputObjects.push(inputObject) + return + } + + // SCALARS + if (def.kind === 'ScalarTypeDefinition') { + const scalar = {} + scalar.name = def.name.value + scalar.kind = helpers.getKind(def.kind) + scalar.id = helpers.getId(scalar.name) + scalar.href = helpers.getFullLink('scalars', scalar.id) + scalar.description = await helpers.getDescription(def.description.value) + scalar.isDeprecated = helpers.getDeprecationStatus(def.directives) + scalar.deprecationReason = await helpers.getDeprecationReason(def.directives, scalar) + scalar.preview = await helpers.getPreview(def.directives, scalar, previewsPerVersion) + data.scalars.push(scalar) + } + }) + ) // add non-schema scalars and sort all scalars alphabetically data.scalars = sortBy(data.scalars.concat(externalScalars), 'name') diff --git a/script/graphql/utils/process-upcoming-changes.js b/script/graphql/utils/process-upcoming-changes.js index c82200218d08..507c675d05de 100644 --- a/script/graphql/utils/process-upcoming-changes.js +++ b/script/graphql/utils/process-upcoming-changes.js @@ -3,7 +3,7 @@ import yaml from 'js-yaml' import { groupBy } from 'lodash-es' import renderContent from '../../../lib/render-content/index.js' -export default async function processUpcomingChanges (upcomingChangesYml) { +export default async function processUpcomingChanges(upcomingChangesYml) { const upcomingChanges = yaml.load(upcomingChangesYml).upcoming_changes for (const change of upcomingChanges) { diff --git a/script/graphql/utils/schema-helpers.js b/script/graphql/utils/schema-helpers.js index c742efc3cecb..302ef35968ce 100644 --- a/script/graphql/utils/schema-helpers.js +++ b/script/graphql/utils/schema-helpers.js @@ -4,23 +4,19 @@ import fs from 'fs' import xGraphql from 'graphql' import path from 'path' -const graphqlTypes = JSON.parse(fs.readFileSync(path.join(process.cwd(), './lib/graphql/types.json'))) -const { - isScalarType, - isObjectType, - isInterfaceType, - isUnionType, - isEnumType, - isInputObjectType -} = xGraphql +const graphqlTypes = JSON.parse( + fs.readFileSync(path.join(process.cwd(), './lib/graphql/types.json')) +) +const { isScalarType, isObjectType, isInterfaceType, isUnionType, isEnumType, isInputObjectType } = + xGraphql const singleQuotesInsteadOfBackticks = / '(\S+?)' / -function addPeriod (string) { +function addPeriod(string) { return string.endsWith('.') ? string : string + '.' } -async function getArguments (args, schema) { +async function getArguments(args, schema) { if (!args.length) return const newArgs = [] @@ -42,59 +38,61 @@ async function getArguments (args, schema) { return newArgs } -async function getDeprecationReason (directives, schemaMember) { +async function getDeprecationReason(directives, schemaMember) { if (!schemaMember.isDeprecated) return // it's possible for a schema member to be deprecated and under preview - const deprecationDirective = directives.filter(dir => dir.name.value === 'deprecated') + const deprecationDirective = directives.filter((dir) => dir.name.value === 'deprecated') // catch any schema members that have more than one deprecation (none currently) - if (deprecationDirective.length > 1) console.log(`more than one deprecation found for ${schemaMember.name}`) + if (deprecationDirective.length > 1) + console.log(`more than one deprecation found for ${schemaMember.name}`) return renderContent(deprecationDirective[0].arguments[0].value.value) } -function getDeprecationStatus (directives) { +function getDeprecationStatus(directives) { if (!directives.length) return return directives[0].name.value === 'deprecated' } -async function getDescription (rawDescription) { +async function getDescription(rawDescription) { rawDescription = rawDescription.replace(singleQuotesInsteadOfBackticks, '`$1`') return renderContent(addPeriod(rawDescription)) } -function getFullLink (baseType, id) { +function getFullLink(baseType, id) { return `/graphql/reference/${baseType}#${id}` } -function getId (path) { +function getId(path) { return removeMarkers(path).toLowerCase() } // e.g., given `ObjectTypeDefinition`, get `objects` -function getKind (type) { - return graphqlTypes.find(graphqlType => graphqlType.type === type).kind +function getKind(type) { + return graphqlTypes.find((graphqlType) => graphqlType.type === type).kind } -async function getPreview (directives, schemaMember, previewsPerVersion) { +async function getPreview(directives, schemaMember, previewsPerVersion) { if (!directives.length) return // it's possible for a schema member to be deprecated and under preview - const previewDirective = directives.filter(dir => dir.name.value === 'preview') + const previewDirective = directives.filter((dir) => dir.name.value === 'preview') if (!previewDirective.length) return // catch any schema members that are under more than one preview (none currently) - if (previewDirective.length > 1) console.log(`more than one preview found for ${schemaMember.name}`) + if (previewDirective.length > 1) + console.log(`more than one preview found for ${schemaMember.name}`) // an input object's input field may have a ListValue directive that is not relevant to previews if (previewDirective[0].arguments[0].value.kind !== 'StringValue') return const previewName = previewDirective[0].arguments[0].value.value - const preview = previewsPerVersion.find(p => p.toggled_by.includes(previewName)) + const preview = previewsPerVersion.find((p) => p.toggled_by.includes(previewName)) if (!preview) console.error(`cannot find "${previewName}" in graphql_previews.yml`) return preview @@ -108,7 +106,7 @@ async function getPreview (directives, schemaMember, previewsPerVersion) { // 2. nullable lists: `[foo]`, `[foo!]` // 3. non-null lists: `[foo]!`, `[foo!]!` // see https://github.com/rmosolgo/graphql-ruby/blob/master/guides/type_definitions/lists.md#lists-nullable-lists-and-lists-of-nulls -function getType (field) { +function getType(field) { // 1. single items if (field.type.kind !== 'ListType') { // nullable item, e.g. `license` query has `License` type @@ -142,7 +140,10 @@ function getType (field) { } // non-null items, e.g. `marketplaceCategories` query has `[MarketplaceCategory!]!` type - if (field.type.type.type.kind === 'NonNullType' && field.type.type.type.type.kind === 'NamedType') { + if ( + field.type.type.type.kind === 'NonNullType' && + field.type.type.type.type.kind === 'NamedType' + ) { return `[${field.type.type.type.type.name.value}!]!` } } @@ -150,7 +151,7 @@ function getType (field) { console.error(`cannot get type of ${field.name.value}`) } -function getTypeKind (type, schema) { +function getTypeKind(type, schema) { type = removeMarkers(type) const typeFromSchema = schema.getType(type) @@ -177,10 +178,8 @@ function getTypeKind (type, schema) { console.error(`cannot find type kind of ${type}`) } -function removeMarkers (str) { - return str.replace('[', '') - .replace(']', '') - .replace(/!/g, '') +function removeMarkers(str) { + return str.replace('[', '').replace(']', '').replace(/!/g, '') } export default { @@ -193,5 +192,5 @@ export default { getKind, getPreview, getType, - getTypeKind + getTypeKind, } diff --git a/script/helpers/add-redirect-to-frontmatter.js b/script/helpers/add-redirect-to-frontmatter.js index 00035b8da1e0..20c1a71d5f90 100644 --- a/script/helpers/add-redirect-to-frontmatter.js +++ b/script/helpers/add-redirect-to-frontmatter.js @@ -1,7 +1,7 @@ #!/usr/bin/env node // add a new redirect string to redirect_from frontmatter -export default function addRedirectToFrontmatter (redirectFromData, newRedirectString) { +export default function addRedirectToFrontmatter(redirectFromData, newRedirectString) { if (Array.isArray(redirectFromData) && !redirectFromData.includes(newRedirectString)) { redirectFromData.push(newRedirectString) } else if (typeof redirectFromData === 'string') { diff --git a/script/helpers/find-extraneous-translation-files.js b/script/helpers/find-extraneous-translation-files.js index fe62022bf968..fbd4a81367a0 100644 --- a/script/helpers/find-extraneous-translation-files.js +++ b/script/helpers/find-extraneous-translation-files.js @@ -7,7 +7,7 @@ import languages from '../../lib/languages.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const walk = xWalkSync.entries -export default function findExtraneousTranslatedFiles () { +export default function findExtraneousTranslatedFiles() { const files = [] const relativePaths = {} @@ -15,15 +15,16 @@ export default function findExtraneousTranslatedFiles () { for (const languageCode in languages) { const language = languages[languageCode] const languageDir = path.join(__dirname, '..', language.dir) - relativePaths[languageCode] = walk(languageDir, { directories: false }) - .map(file => file.relativePath) + relativePaths[languageCode] = walk(languageDir, { directories: false }).map( + (file) => file.relativePath + ) } for (const languageCode in languages) { if (languageCode === 'en') continue const language = languages[languageCode] /* istanbul ignore next */ - difference(relativePaths[languageCode], relativePaths.en).forEach(file => { + difference(relativePaths[languageCode], relativePaths.en).forEach((file) => { files.push(path.join(__dirname, '..', language.dir, file)) }) } diff --git a/script/helpers/find-unused-assets.js b/script/helpers/find-unused-assets.js index a972e337a8db..6e0dc8d1822b 100644 --- a/script/helpers/find-unused-assets.js +++ b/script/helpers/find-unused-assets.js @@ -12,10 +12,7 @@ import getDataReferences from '../../lib/get-liquid-data-references.js' const imagesPath = '/assets/images' // these paths should remain in the repo even if they are not referenced directly -const ignoreList = [ - '/assets/images/help/site-policy', - 'site.data.reusables.policies' -] +const ignoreList = ['/assets/images/help/site-policy', 'site.data.reusables.policies'] // search these dirs for images or data references // content files are handled separately in assetsReferencedInContent @@ -26,12 +23,12 @@ const dirsToGrep = [ 'stylesheets', 'README.md', 'data/reusables', - 'data/variables' + 'data/variables', ] const validArgs = ['reusables', 'variables', 'images'] -export default async function findUnusedAssets (assetType) { +export default async function findUnusedAssets(assetType) { assert(validArgs.includes(assetType), `arg must be one of: ${validArgs.join(', ')}`) const pages = await getEnglishPages() @@ -43,19 +40,23 @@ export default async function findUnusedAssets (assetType) { const allImagesInRepo = getAllImagesInRepo() // step 2. find assets referenced in content by searching page markdown - const assetsReferencedInContent = flatten(pages.map(page => { - const fullContent = [page.intro, page.title, page.product, page.markdown].join() + const assetsReferencedInContent = flatten( + pages.map((page) => { + const fullContent = [page.intro, page.title, page.product, page.markdown].join() - return assetType === 'images' - ? getImageReferences(fullContent) - : getDataReferences(fullContent) - })) + return assetType === 'images' + ? getImageReferences(fullContent) + : getDataReferences(fullContent) + }) + ) // step 3. find assets referenced in non-content directories const assetsReferencedInNonContentDirs = getAssetsReferencedInNonContentDirs(assetType) // step 4. combine all the referenced assets into one array - const allReferencedAssets = [...new Set(assetsReferencedInContent.concat(assetsReferencedInNonContentDirs))] + const allReferencedAssets = [ + ...new Set(assetsReferencedInContent.concat(assetsReferencedInNonContentDirs)), + ] // step 5. return asssets that exist but are not referenced switch (assetType) { @@ -68,47 +69,43 @@ export default async function findUnusedAssets (assetType) { } } -async function getEnglishPages () { +async function getEnglishPages() { const pages = await loadPages() - return pages.filter(page => page.languageCode === 'en') + return pages.filter((page) => page.languageCode === 'en') } -function getAllImagesInRepo () { +function getAllImagesInRepo() { return walk(path.join(process.cwd(), imagesPath), { directories: false }) - .filter(relPath => !relPath.endsWith('.md') && !relPath.match(/^(octicons|site)\//)) - .map(relPath => path.join(imagesPath, relPath)) + .filter((relPath) => !relPath.endsWith('.md') && !relPath.match(/^(octicons|site)\//)) + .map((relPath) => path.join(imagesPath, relPath)) } -function getAssetsReferencedInNonContentDirs (assetType) { - const regex = assetType === 'images' - ? patterns.imagePath.source - : patterns.dataReference.source +function getAssetsReferencedInNonContentDirs(assetType) { + const regex = assetType === 'images' ? patterns.imagePath.source : patterns.dataReference.source const grepCmd = `egrep -rh '${regex}' ${dirsToGrep.join(' ')}` const grepResults = execSync(grepCmd).toString() - return assetType === 'images' - ? getImageReferences(grepResults) - : getDataReferences(grepResults) + return assetType === 'images' ? getImageReferences(grepResults) : getDataReferences(grepResults) } -function getImageReferences (text) { - return (text.match(patterns.imagePath) || []) - .map(ref => { - return ref - .replace(/\.\.\//g, '') - .trim() - }) +function getImageReferences(text) { + return (text.match(patterns.imagePath) || []).map((ref) => { + return ref.replace(/\.\.\//g, '').trim() + }) } -function getUnusedData (allDataInRepo, assetType, allReferencedAssets) { +function getUnusedData(allDataInRepo, assetType, allReferencedAssets) { const unusedData = [] - Object.keys(allDataInRepo).forEach(filename => { - Object.keys(allDataInRepo[filename]).forEach(key => { + Object.keys(allDataInRepo).forEach((filename) => { + Object.keys(allDataInRepo[filename]).forEach((key) => { const name = `site.data.${assetType}.${filename}.${key}` - if (!allReferencedAssets.includes(name) && !ignoreList.find(ignored => name.startsWith(ignored))) { + if ( + !allReferencedAssets.includes(name) && + !ignoreList.find((ignored) => name.startsWith(ignored)) + ) { unusedData.push(name) } }) @@ -117,6 +114,10 @@ function getUnusedData (allDataInRepo, assetType, allReferencedAssets) { return unusedData } -function getUnusedImages (allImagesInRepo, allReferencedAssets) { - return allImagesInRepo.filter(image => !allReferencedAssets.includes(image) && !ignoreList.find(ignored => image.startsWith(ignored))) +function getUnusedImages(allImagesInRepo, allReferencedAssets) { + return allImagesInRepo.filter( + (image) => + !allReferencedAssets.includes(image) && + !ignoreList.find((ignored) => image.startsWith(ignored)) + ) } diff --git a/script/helpers/get-liquid-conditionals.js b/script/helpers/get-liquid-conditionals.js index a1e94536cdbb..fd9c0715220f 100644 --- a/script/helpers/get-liquid-conditionals.js +++ b/script/helpers/get-liquid-conditionals.js @@ -1,12 +1,13 @@ #!/usr/bin/env node import { Tokenizer } from 'liquidjs' -export default function getLiquidConditionals (str, tagNames) { +export default function getLiquidConditionals(str, tagNames) { const tokenizer = new Tokenizer(str) tagNames = Array.isArray(tagNames) ? tagNames : [tagNames] - return tokenizer.readTopLevelTokens() - .filter(token => tagNames.includes(token.name)) - .map(token => token.args) + return tokenizer + .readTopLevelTokens() + .filter((token) => tagNames.includes(token.name)) + .map((token) => token.args) } diff --git a/script/helpers/git-utils.js b/script/helpers/git-utils.js index a0f301d5caee..dcb2547351f9 100644 --- a/script/helpers/git-utils.js +++ b/script/helpers/git-utils.js @@ -3,54 +3,54 @@ import xGithub from './github.js' const github = xGithub() // https://docs.github.com/rest/reference/git#get-a-reference -export async function getCommitSha (owner, repo, ref) { +export async function getCommitSha(owner, repo, ref) { try { const { data } = await github.git.getRef({ owner, repo, - ref + ref, }) return data.object.sha } catch (err) { console.log('error getting tree') - throw (err) + throw err } } // https://docs.github.com/rest/reference/git#list-matching-references -export async function listMatchingRefs (owner, repo, ref) { +export async function listMatchingRefs(owner, repo, ref) { try { // if the ref is found, this returns an array of objects; // if the ref is not found, this returns an empty array const { data } = await github.git.listMatchingRefs({ owner, repo, - ref + ref, }) return data } catch (err) { console.log('error getting tree') - throw (err) + throw err } } // https://docs.github.com/rest/reference/git#get-a-commit -export async function getTreeSha (owner, repo, commitSha) { +export async function getTreeSha(owner, repo, commitSha) { try { const { data } = await github.git.getCommit({ owner, repo, - commit_sha: commitSha + commit_sha: commitSha, }) return data.tree.sha } catch (err) { console.log('error getting tree') - throw (err) + throw err } } // https://docs.github.com/rest/reference/git#get-a-tree -export async function getTree (owner, repo, ref, allowedPaths = []) { +export async function getTree(owner, repo, ref, allowedPaths = []) { const commitSha = await getCommitSha(owner, repo, ref) const treeSha = await getTreeSha(owner, repo, commitSha) try { @@ -58,71 +58,71 @@ export async function getTree (owner, repo, ref, allowedPaths = []) { owner, repo, tree_sha: treeSha, - recursive: 1 + recursive: 1, }) // only return files that match the patterns in allowedPaths // skip actions/changes files return data.tree } catch (err) { console.log('error getting tree') - throw (err) + throw err } } // https://docs.github.com/rest/reference/git#get-a-blob -export async function getContentsForBlob (owner, repo, blob) { +export async function getContentsForBlob(owner, repo, blob) { const { data } = await github.git.getBlob({ owner, repo, - file_sha: blob.sha + file_sha: blob.sha, }) // decode blob contents return Buffer.from(data.content, 'base64') } // https://docs.github.com/rest/reference/repos#get-repository-content -export async function getContents (owner, repo, ref, path) { +export async function getContents(owner, repo, ref, path) { try { const { data } = await github.repos.getContent({ owner, repo, ref, - path + path, }) // decode contents return Buffer.from(data.content, 'base64').toString() } catch (err) { console.log(`error getting ${path} from ${owner}/${repo} at ref ${ref}`) - throw (err) + throw err } } // https://docs.github.com/en/rest/reference/pulls#list-pull-requests -export async function listPulls (owner, repo) { +export async function listPulls(owner, repo) { try { const { data } = await github.pulls.list({ owner, repo, - per_page: 100 + per_page: 100, }) return data } catch (err) { console.log(`error listing pulls in ${owner}/${repo}`) - throw (err) + throw err } } -export async function createIssueComment (owner, repo, pullNumber, body) { +export async function createIssueComment(owner, repo, pullNumber, body) { try { const { data } = await github.issues.createComment({ owner, repo, issue_number: pullNumber, - body + body, }) return data } catch (err) { console.log(`error creating a review comment on PR ${pullNumber} in ${owner}/${repo}`) - throw (err) + throw err } } diff --git a/script/helpers/github.js b/script/helpers/github.js index 15e02e035ef8..d9d847010a17 100644 --- a/script/helpers/github.js +++ b/script/helpers/github.js @@ -14,10 +14,9 @@ if (!process.env.GITHUB_TOKEN) { // 3. an installation token granted via GitHub Actions const apiToken = process.env.GITHUB_TOKEN - // See https://github.com/octokit/rest.js/issues/1207 -export default function github () { +export default function github() { return new Octokit({ - auth: `token ${apiToken}` + auth: `token ${apiToken}`, }) } diff --git a/script/helpers/walk-files.js b/script/helpers/walk-files.js index 3974667e4cd4..7e771e659da0 100644 --- a/script/helpers/walk-files.js +++ b/script/helpers/walk-files.js @@ -8,11 +8,11 @@ import walk from 'walk-sync' // // [end-readme] -export default function walkFiles (dir, ext, opts = {}) { +export default function walkFiles(dir, ext, opts = {}) { const dirPath = path.posix.join(process.cwd(), dir) const walkSyncOpts = { includeBasePath: true, directories: false } return walk(dirPath, walkSyncOpts) - .filter(file => file.endsWith(ext) && !file.endsWith('README.md')) - .filter(file => opts.includeEarlyAccess ? file : !file.includes('/early-access/')) + .filter((file) => file.endsWith(ext) && !file.endsWith('README.md')) + .filter((file) => (opts.includeEarlyAccess ? file : !file.includes('/early-access/'))) } diff --git a/script/i18n/homogenize-frontmatter.js b/script/i18n/homogenize-frontmatter.js index 28c84b32a397..ee0ff3ac3fd3 100755 --- a/script/i18n/homogenize-frontmatter.js +++ b/script/i18n/homogenize-frontmatter.js @@ -20,35 +20,44 @@ const fs = xFs.promises // Run! main() -async function main () { +async function main() { const translationDir = path.posix.join(__dirname, '../../translations') const translatedMarkdownFiles = walk(translationDir) - .filter(filename => { - return filename.includes('/content/') && - filename.endsWith('.md') && - !filename.endsWith('README.md') + .filter((filename) => { + return ( + filename.includes('/content/') && + filename.endsWith('.md') && + !filename.endsWith('README.md') + ) }) - .map(filename => `translations/${filename}`) + .map((filename) => `translations/${filename}`) console.log( - (await Promise.all( - translatedMarkdownFiles - .map(async relPath => updateTranslatedMarkdownFile(relPath) - .catch(e => `Error in ${relPath}: ${e.message}`) + ( + await Promise.all( + translatedMarkdownFiles.map(async (relPath) => + updateTranslatedMarkdownFile(relPath).catch((e) => `Error in ${relPath}: ${e.message}`) ) - )).filter(Boolean).join('\n') + ) + ) + .filter(Boolean) + .join('\n') ) } -async function extractFrontmatter (path) { +async function extractFrontmatter(path) { const fileContents = await readFileAsync(path, 'utf8') return fm(fileContents) } -async function updateTranslatedMarkdownFile (relPath) { +async function updateTranslatedMarkdownFile(relPath) { const localisedAbsPath = path.posix.join(__dirname, '../..', relPath) // find the corresponding english file by removing the first 2 path segments: /translations/<language code> - const engAbsPath = path.posix.join(__dirname, '../..', relPath.split(path.sep).slice(2).join(path.sep)) + const engAbsPath = path.posix.join( + __dirname, + '../..', + relPath.split(path.sep).slice(2).join(path.sep) + ) // Load frontmatter from the source english file let englishFrontmatter @@ -68,7 +77,11 @@ async function updateTranslatedMarkdownFile (relPath) { // Look for differences between the english and localised non-translatable properties let overwroteSomething = false for (const prop in localisedFrontmatter.data) { - if (!fm.schema.properties[prop].translatable && englishFrontmatter.data[prop] && localisedFrontmatter.data[prop] !== englishFrontmatter.data[prop]) { + if ( + !fm.schema.properties[prop].translatable && + englishFrontmatter.data[prop] && + localisedFrontmatter.data[prop] !== englishFrontmatter.data[prop] + ) { localisedFrontmatter.data[prop] = englishFrontmatter.data[prop] overwroteSomething = true } @@ -76,7 +89,10 @@ async function updateTranslatedMarkdownFile (relPath) { // rewrite the localised file, if it changed if (overwroteSomething) { - const toWrite = matter.stringify(localisedFrontmatter.content, localisedFrontmatter.data, { lineWidth: 10000, forceQuotes: true }) + const toWrite = matter.stringify(localisedFrontmatter.content, localisedFrontmatter.data, { + lineWidth: 10000, + forceQuotes: true, + }) await fs.writeFile(localisedAbsPath, toWrite) // return `${relPath}: updated` diff --git a/script/lint-translation-files.js b/script/lint-translation-files.js index d02c52b3ed9a..9d6f56be6320 100755 --- a/script/lint-translation-files.js +++ b/script/lint-translation-files.js @@ -1,7 +1,6 @@ #!/usr/bin/env node import { execSync } from 'child_process' - // [start-readme] // // Use this script as part of the Crowdin merge process to output a list of parsing and rendering @@ -30,7 +29,9 @@ try { // Reset the broken files. console.log('Resetting broken files...') -execSync(`cat ${parsingErrorsLog} ${renderErrorsLog} | egrep "^translations/.*/(.+.md|.+.yml)$" | uniq | xargs -L1 script/reset-translated-file.js --prefer-main`) +execSync( + `cat ${parsingErrorsLog} ${renderErrorsLog} | egrep "^translations/.*/(.+.md|.+.yml)$" | uniq | xargs -L1 script/reset-translated-file.js --prefer-main` +) // Print a message with next steps. console.log(`Success! diff --git a/script/list-image-sizes.js b/script/list-image-sizes.js index 9d33cee9a725..315153b3ef9a 100755 --- a/script/list-image-sizes.js +++ b/script/list-image-sizes.js @@ -16,10 +16,10 @@ const imagesExtensions = ['.jpg', '.jpeg', '.png', '.gif'] // [end-readme] const images = chain(walk(imagesPath, { directories: false })) - .filter(relativePath => { + .filter((relativePath) => { return imagesExtensions.includes(path.extname(relativePath.toLowerCase())) }) - .map(relativePath => { + .map((relativePath) => { const fullPath = path.join(imagesPath, relativePath) const { width, height } = imageSize(fullPath) const size = width * height @@ -28,7 +28,7 @@ const images = chain(walk(imagesPath, { directories: false })) .orderBy('size', 'desc') .value() -images.forEach(image => { +images.forEach((image) => { const { relativePath, width, height } = image console.log(`${width} x ${height} - ${relativePath}`) }) diff --git a/script/move-category-to-product.js b/script/move-category-to-product.js index dc99128fc43b..6f31fcf48333 100755 --- a/script/move-category-to-product.js +++ b/script/move-category-to-product.js @@ -20,7 +20,10 @@ const contentDir = path.posix.join(process.cwd(), 'content') program .description('Move a category-level docs set to the product level.') - .requiredOption('-c, --category <PATH>', 'Provide the path of the existing category, e.g., github/github-pages') + .requiredOption( + '-c, --category <PATH>', + 'Provide the path of the existing category, e.g., github/github-pages' + ) .requiredOption('-p, --product <PATH>', 'Provide the path of the new product, e.g., pages') .parse(process.argv) @@ -36,7 +39,7 @@ if (!fs.existsSync(oldProductPath)) { process.exit(1) } -const oldCategoryFiles = contentFiles.filter(file => file.includes(`/${oldCategoryId}/`)) +const oldCategoryFiles = contentFiles.filter((file) => file.includes(`/${oldCategoryId}/`)) if (!oldCategoryFiles.length) { console.error(`Error! Can't find ${oldCategory} files`) @@ -47,17 +50,14 @@ const newProductPath = path.posix.join(process.cwd(), 'content', newProduct) main() -function main () { +function main() { // Create the new product dir. mkdirp(newProductPath) // Add redirects to the frontmatter of the to-be-moved files. - oldCategoryFiles.forEach(file => { + oldCategoryFiles.forEach((file) => { const { content, data } = frontmatter(fs.readFileSync(file, 'utf8')) - const redirectString = file - .replace(contentDir, '') - .replace('index.md', '') - .replace('.md', '') + const redirectString = file.replace(contentDir, '').replace('index.md', '').replace('.md', '') data.redirect_from = addRedirectToFrontmatter(data.redirect_from, redirectString) fs.writeFileSync(file, frontmatter.stringify(content, data, { lineWidth: 10000 })) }) @@ -68,14 +68,22 @@ function main () { // Remove the category from the old product TOC. const oldProductTocPath = path.posix.join(oldProductPath, 'index.md') const productToc = frontmatter(fs.readFileSync(oldProductTocPath, 'utf8')) - productToc.data.children = productToc.data.children.filter(child => child !== `/${oldCategoryId}`) - fs.writeFileSync(oldProductTocPath, frontmatter.stringify(productToc.content, productToc.data, { lineWidth: 10000 })) - + productToc.data.children = productToc.data.children.filter( + (child) => child !== `/${oldCategoryId}` + ) + fs.writeFileSync( + oldProductTocPath, + frontmatter.stringify(productToc.content, productToc.data, { lineWidth: 10000 }) + ) + // Add the new product to the homepage TOC. const homepage = path.posix.join(contentDir, 'index.md') const homepageToc = frontmatter(fs.readFileSync(homepage, 'utf8')) homepageToc.data.children.push(newProduct) - fs.writeFileSync(homepage, frontmatter.stringify(homepageToc.content, homepageToc.data, { lineWidth: 10000 })) + fs.writeFileSync( + homepage, + frontmatter.stringify(homepageToc.content, homepageToc.data, { lineWidth: 10000 }) + ) console.log(`Moved ${oldCategory} files to ${newProduct}, added redirects, and updated TOCs!`) } diff --git a/script/move-reusables-to-markdown.js b/script/move-reusables-to-markdown.js index 7f26a68731be..d54f2a6f7b7a 100755 --- a/script/move-reusables-to-markdown.js +++ b/script/move-reusables-to-markdown.js @@ -22,17 +22,17 @@ const mkdirp = xMkdirp.sync // move reusables for each language Object.values(languages).forEach(({ dir }) => move(dir)) -function move (dir) { +function move(dir) { const fullDir = path.join(__dirname, '..', dir, 'data/reusables') console.log('removing', fullDir) walk(fullDir) - .filter(entry => entry.relativePath.endsWith('yml')) - .forEach(file => { + .filter((entry) => entry.relativePath.endsWith('yml')) + .forEach((file) => { const fullPath = path.join(file.basePath, file.relativePath) const fileContent = fs.readFileSync(fullPath, 'utf8') const data = flat(yaml.load(fileContent)) - Object.keys(data).forEach(key => { + Object.keys(data).forEach((key) => { const value = get(data, key) const markdownFilename = path.join(fullPath.replace('.yml', ''), `${key}.md`) mkdirp(path.dirname(markdownFilename)) diff --git a/script/pages-with-liquid-titles.js b/script/pages-with-liquid-titles.js index f7ec6c8bd331..7a6e974ab5d5 100755 --- a/script/pages-with-liquid-titles.js +++ b/script/pages-with-liquid-titles.js @@ -9,11 +9,10 @@ import patterns from '../lib/patterns.js' // // [end-readme] - -async function main () { +async function main() { const pages = await loadPages() const liquidPages = pages - .filter(page => page.title && patterns.hasLiquid.test(page.title)) + .filter((page) => page.title && patterns.hasLiquid.test(page.title)) .map(({ relativePath, title }) => { return { relativePath, title } }) @@ -21,8 +20,7 @@ async function main () { console.log(`\n\n${liquidPages.length} pages with liquid titles`) console.log(JSON.stringify(liquidPages, null, 2)) - const conditionalPages = liquidPages - .filter(page => page.title.includes('{% if')) + const conditionalPages = liquidPages.filter((page) => page.title.includes('{% if')) console.log(`\n\n\n\n${conditionalPages.length} pages with conditionals in their titles`) console.log(JSON.stringify(conditionalPages, null, 2)) diff --git a/script/ping-staging-apps.js b/script/ping-staging-apps.js index 6c73ded3cde7..43d093f0a7dd 100755 --- a/script/ping-staging-apps.js +++ b/script/ping-staging-apps.js @@ -20,12 +20,12 @@ const heroku = new Heroku({ token: process.env.HEROKU_API_TOKEN }) main() -async function main () { +async function main() { const apps = chain(await heroku.get('/apps')) .orderBy('name') .value() - async function ping (app) { + async function ping(app) { // ?warmup param has no effect but makes it easier to find these requests in the logs const url = `https://${app.name}.herokuapp.com/en?warmup` try { diff --git a/script/prevent-pushes-to-main.js b/script/prevent-pushes-to-main.js index 90e1131ccf55..b29dcef0ad96 100644 --- a/script/prevent-pushes-to-main.js +++ b/script/prevent-pushes-to-main.js @@ -1,7 +1,6 @@ #!/usr/bin/env node import { execSync } from 'child_process' - // [start-readme] // This script is intended to be used as a git "prepush" hook. @@ -14,9 +13,13 @@ const currentBranch = execSync('git symbolic-ref --short HEAD', { encoding: 'utf if (currentBranch === productionBranch) { console.error('') - console.error(`๐Ÿคš Whoa! Pushing to the ${productionBranch} branch has been disabled to prevent accidental deployments to production.`) + console.error( + `๐Ÿคš Whoa! Pushing to the ${productionBranch} branch has been disabled to prevent accidental deployments to production.` + ) console.error('') - console.error('If you\'re aware of the risks and really want to push to this branch, add --no-verify to bypass this check.') + console.error( + "If you're aware of the risks and really want to push to this branch, add --no-verify to bypass this check." + ) console.error('') process.exit(1) } diff --git a/script/prevent-translation-commits.js b/script/prevent-translation-commits.js index 782297c65578..e03916e5829a 100755 --- a/script/prevent-translation-commits.js +++ b/script/prevent-translation-commits.js @@ -19,15 +19,27 @@ if (process.env.CI) process.exit() if (process.env.ALLOW_TRANSLATION_COMMITS) process.exit() const filenames = execSync('git diff --cached --name-only').toString().trim().split('\n') -const localizedFilenames = filenames.filter(filename => filename.startsWith('translations/')) +const localizedFilenames = filenames.filter((filename) => filename.startsWith('translations/')) if (localizedFilenames.length) { - console.error('\nโœ‹ Uh oh! Detected changes to the following files in the `/translations` directory:') + console.error( + '\nโœ‹ Uh oh! Detected changes to the following files in the `/translations` directory:' + ) console.table(localizedFilenames.join('\n')) - console.error('The content in this directory is managed by our Crowdin integration and should not be edited directly in the repo.') - console.error('For more information on how the localization process works, see translations/README.md') - console.error('\nIf you have accidentally edited these files, you can unstage these changes on the command line using `git restore --staged translations`\n') - console.error('\nIf you are performing a merge from `main`, you should bypass this hook by using ` git commit --no-verify`\n') - console.error('\nIf you need to edit translated files often, you can set `ALLOW_TRANSLATION_COMMITS=true` in your .env file.`\n') + console.error( + 'The content in this directory is managed by our Crowdin integration and should not be edited directly in the repo.' + ) + console.error( + 'For more information on how the localization process works, see translations/README.md' + ) + console.error( + '\nIf you have accidentally edited these files, you can unstage these changes on the command line using `git restore --staged translations`\n' + ) + console.error( + '\nIf you are performing a merge from `main`, you should bypass this hook by using ` git commit --no-verify`\n' + ) + console.error( + '\nIf you need to edit translated files often, you can set `ALLOW_TRANSLATION_COMMITS=true` in your .env file.`\n' + ) process.exit(1) } diff --git a/script/purge-fastly-by-url.js b/script/purge-fastly-by-url.js index 3ff0014321bf..a5b01f4ae6b5 100755 --- a/script/purge-fastly-by-url.js +++ b/script/purge-fastly-by-url.js @@ -20,9 +20,14 @@ const requiredUrlPrefix = 'https://docs.github.com' const purgeCommand = 'curl -s -X PURGE -H "Fastly-Soft-Purge:1"' program - .description('Purge the Fastly cache for a single URL or a batch of URLs in a file, plus all language variants of the given URL(s).') + .description( + 'Purge the Fastly cache for a single URL or a batch of URLs in a file, plus all language variants of the given URL(s).' + ) .option('-s, --single <URL>', `provide a single ${requiredUrlPrefix} URL`) - .option('-b, --batch <FILE>', `provide a path to a file containing a list of ${requiredUrlPrefix} URLs`) + .option( + '-b, --batch <FILE>', + `provide a path to a file containing a list of ${requiredUrlPrefix} URLs` + ) .option('-d, --dry-run', 'print URLs to be purged without actually purging') .parse(process.argv) @@ -37,7 +42,9 @@ if (!singleUrl && !batchFile) { } if (singleUrl && !singleUrl.startsWith(requiredUrlPrefix)) { - console.error(`error: cannot purge ${singleUrl} because URLs must start with ${requiredUrlPrefix}.\n`) + console.error( + `error: cannot purge ${singleUrl} because URLs must start with ${requiredUrlPrefix}.\n` + ) process.exit(1) } @@ -54,18 +61,20 @@ if (singleUrl) { if (batchFile) { fs.readFileSync(batchFile, 'utf8') .split('\n') - .filter(line => line !== '') - .forEach(url => { + .filter((line) => line !== '') + .forEach((url) => { if (!url.startsWith(requiredUrlPrefix)) { - console.error(`error: cannot purge ${url} because URLs must start with ${requiredUrlPrefix}.\n`) + console.error( + `error: cannot purge ${url} because URLs must start with ${requiredUrlPrefix}.\n` + ) process.exit(1) } purge(url) }) } -function purge (url) { - getLanguageVariants(url).forEach(localizedUrl => { +function purge(url) { + getLanguageVariants(url).forEach((localizedUrl) => { if (dryRun) { console.log(`This is a dry run! Will purge cache for ${localizedUrl}`) return @@ -81,15 +90,15 @@ function purge (url) { }) } -function getLanguageVariants (url) { +function getLanguageVariants(url) { // for https://docs.github.com/en/foo, get https://docs.github.com/foo const languagelessUrl = getPathWithoutLanguage(url.replace(requiredUrlPrefix, '')) // then derive localized urls - return languageCodes.map(lc => path.join(requiredUrlPrefix, lc, languagelessUrl)) + return languageCodes.map((lc) => path.join(requiredUrlPrefix, lc, languagelessUrl)) } -function logStatus (result) { +function logStatus(result) { // only log status if it's not ok if (JSON.parse(result).status === 'ok') return console.log(result) diff --git a/script/purge-redis-pages.js b/script/purge-redis-pages.js index eee87915b906..2c185d0f423e 100755 --- a/script/purge-redis-pages.js +++ b/script/purge-redis-pages.js @@ -14,7 +14,6 @@ import createRedisClient from '../lib/redis/create-client.js' xDotenv.config() - const { REDIS_URL, HEROKU_RELEASE_VERSION, HEROKU_PRODUCTION_APP } = process.env const isHerokuProd = HEROKU_PRODUCTION_APP === 'true' const pageCacheDatabaseNumber = 1 @@ -41,17 +40,17 @@ if (!REDIS_URL) { console.log({ HEROKU_RELEASE_VERSION, - HEROKU_PRODUCTION_APP + HEROKU_PRODUCTION_APP, }) purgeRenderedPageCache() -function purgeRenderedPageCache () { +function purgeRenderedPageCache() { const redisClient = createRedisClient({ url: REDIS_URL, db: pageCacheDatabaseNumber, // These commands ARE important, so let's make sure they are all accounted for - enable_offline_queue: true + enable_offline_queue: true, }) let iteration = 0 @@ -69,20 +68,26 @@ function purgeRenderedPageCache () { // Define other subroutines // - async function scan (cursor = '0') { + async function scan(cursor = '0') { try { // [0]: Update the cursor position for the next scan // [1]: Get the SCAN result for this iteration const [nextCursor, keys] = await scanAsync( cursor, - 'MATCH', keyScanningPattern, - 'COUNT', scanSetSize.toString() + 'MATCH', + keyScanningPattern, + 'COUNT', + scanSetSize.toString() ) console.log(`\n[Iteration ${iteration++}] Received ${keys.length} keys...`) if (dryRun) { - console.log(`DRY RUN! This iteration might have set TTL for up to ${keys.length} keys:\n - ${keys.join('\n - ')}`) + console.log( + `DRY RUN! This iteration might have set TTL for up to ${ + keys.length + } keys:\n - ${keys.join('\n - ')}` + ) } // NOTE: It is possible for a SCAN cursor iteration to return 0 keys when @@ -125,9 +130,9 @@ function purgeRenderedPageCache () { } // Find existing TTLs to ensure we aren't extending the TTL if it's already set - async function getTtls (keys) { + async function getTtls(keys) { const pttlPipeline = redisClient.batch() - keys.forEach(key => pttlPipeline.pttl(key)) + keys.forEach((key) => pttlPipeline.pttl(key)) const pttlPipelineExecAsync = promisify(pttlPipeline.exec).bind(pttlPipeline) const pttlResults = await pttlPipelineExecAsync() @@ -139,7 +144,7 @@ function purgeRenderedPageCache () { return pttlResults } - async function updateTtls (keys) { + async function updateTtls(keys) { const pttlResults = await getTtls(keys) // Find pertinent keys to have TTLs set diff --git a/script/reconcile-category-dirs-with-ids.js b/script/reconcile-category-dirs-with-ids.js index 2b513cdc687a..d32dad74b80e 100755 --- a/script/reconcile-category-dirs-with-ids.js +++ b/script/reconcile-category-dirs-with-ids.js @@ -9,7 +9,6 @@ import { XmlEntities } from 'html-entities' import loadSiteData from '../lib/site-data.js' import renderContent from '../lib/render-content/index.js' - const slugger = new GithubSlugger() const entities = new XmlEntities() @@ -36,7 +35,7 @@ if (process.platform.startsWith('win')) { // Execute! main() -async function main () { +async function main() { const englishCategoryIndices = getEnglishCategoryIndices() const siteData = await getEnglishSiteData() @@ -85,7 +84,10 @@ Redirect: "${redirectPath}" const articlePath = path.join(categoryDirPath, articleFileName) // Figure out redirect path - const articlePathMinusExtension = path.join(categoryDirPath, path.basename(articleFileName, '.md')) + const articlePathMinusExtension = path.join( + categoryDirPath, + path.basename(articleFileName, '.md') + ) const redirectArticlePath = '/' + slash(path.relative(contentDir, articlePathMinusExtension)) // Log it @@ -108,7 +110,10 @@ Redirect: "${redirectArticlePath}" articleData.redirect_from.push(redirectArticlePath) // Update the article file on disk - fs.writeFileSync(articlePath, frontmatter.stringify(articleContent, articleData, { lineWidth: 10000 })) + fs.writeFileSync( + articlePath, + frontmatter.stringify(articleContent, articleData, { lineWidth: 10000 }) + ) } // Update the reference to this category in the product index file on disk @@ -118,8 +123,14 @@ Redirect: "${redirectArticlePath}" const productIndexPath = path.join(categoryDirParentDir, 'index.md') const productIndexContents = fs.readFileSync(productIndexPath, 'utf8') const { data: productIndexData, content: productIndex } = frontmatter(productIndexContents) - const revisedProductIndex = productIndex.replace(new RegExp(`(\\s+)(?:/${categoryDirName})(\\s+)`, 'g'), `$1/${expectedSlug}$2`) - fs.writeFileSync(productIndexPath, frontmatter.stringify(revisedProductIndex, productIndexData, { lineWidth: 10000 })) + const revisedProductIndex = productIndex.replace( + new RegExp(`(\\s+)(?:/${categoryDirName})(\\s+)`, 'g'), + `$1/${expectedSlug}$2` + ) + fs.writeFileSync( + productIndexPath, + frontmatter.stringify(revisedProductIndex, productIndexData, { lineWidth: 10000 }) + ) console.log(`*** Updated product index "${productIndexPath}" for โ˜๏ธ\n`) @@ -128,18 +139,18 @@ Redirect: "${redirectArticlePath}" } } -function getEnglishCategoryIndices () { +function getEnglishCategoryIndices() { const walkOptions = { globs: ['*/*/**/index.md'], ignore: ['{rest,graphql,developers}/**', 'enterprise/admin/index.md', '**/articles/**'], directories: false, - includeBasePath: true + includeBasePath: true, } return walk(contentDir, walkOptions) } -async function getEnglishSiteData () { +async function getEnglishSiteData() { const siteData = await loadSiteData() return siteData.en.site } diff --git a/script/reconcile-filenames-with-ids.js b/script/reconcile-filenames-with-ids.js index 0a6278735b76..241e54acedd3 100755 --- a/script/reconcile-filenames-with-ids.js +++ b/script/reconcile-filenames-with-ids.js @@ -13,12 +13,11 @@ const entities = new htmlEntities.XmlEntities() const contentDir = path.join(process.cwd(), 'content') -const contentFiles = walk(contentDir, { includeBasePath: true, directories: false }) - .filter(file => { - return file.endsWith('.md') && - !file.endsWith('index.md') && - !file.includes('README') - }) +const contentFiles = walk(contentDir, { includeBasePath: true, directories: false }).filter( + (file) => { + return file.endsWith('.md') && !file.endsWith('index.md') && !file.includes('README') + } +) // [start-readme] // @@ -35,7 +34,7 @@ if (process.platform.startsWith('win')) { process.exit() } -contentFiles.forEach(oldFullPath => { +contentFiles.forEach((oldFullPath) => { const { data, content } = frontmatter(fs.readFileSync(oldFullPath, 'utf8')) // skip pages with frontmatter flag diff --git a/script/remove-extraneous-translation-files.js b/script/remove-extraneous-translation-files.js index b12640309ea9..3d7471b99e7e 100755 --- a/script/remove-extraneous-translation-files.js +++ b/script/remove-extraneous-translation-files.js @@ -2,7 +2,6 @@ import fs from 'fs' import findExtraneousFiles from './helpers/find-extraneous-translation-files.js' - // [start-readme] // // An [automated test](/tests/extraneous-translation-files.js) checks for files in the `translations/` directory @@ -13,10 +12,12 @@ import findExtraneousFiles from './helpers/find-extraneous-translation-files.js' main() -async function main () { +async function main() { const files = findExtraneousFiles() - console.log(`Found ${files.length} extraneous translation ${files.length === 1 ? 'file' : 'files'}\n\n`) - files.forEach(file => { + console.log( + `Found ${files.length} extraneous translation ${files.length === 1 ? 'file' : 'files'}\n\n` + ) + files.forEach((file) => { console.log(file) fs.unlinkSync(file) }) diff --git a/script/remove-stale-staging-apps.js b/script/remove-stale-staging-apps.js index f7028ca96117..6d01496dc51a 100755 --- a/script/remove-stale-staging-apps.js +++ b/script/remove-stale-staging-apps.js @@ -14,10 +14,11 @@ import getOctokit from './helpers/github.js' xDotenv.config() - // Check for required Heroku API token if (!process.env.HEROKU_API_TOKEN) { - console.error('Error! You must have a HEROKU_API_TOKEN environment variable for deployer-level access.') + console.error( + 'Error! You must have a HEROKU_API_TOKEN environment variable for deployer-level access.' + ) process.exit(1) } // Check for required GitHub PAT @@ -34,31 +35,30 @@ const protectedAppNames = ['help-docs', 'help-docs-deployer'] main() -async function main () { +async function main() { const apps = chain(await heroku.get('/apps')) .orderBy('name') .value() const prInfoMatch = /^(?<repo>docs(?:-internal)?)-(?<pullNumber>\d+)--.*$/ - const appsPlusPullIds = apps - .map(app => { - const match = prInfoMatch.exec(app.name) - const { repo, pullNumber } = ((match || {}).groups || {}) + const appsPlusPullIds = apps.map((app) => { + const match = prInfoMatch.exec(app.name) + const { repo, pullNumber } = (match || {}).groups || {} - return { - app, - repo, - pullNumber: parseInt(pullNumber, 10) || null - } - }) + return { + app, + repo, + pullNumber: parseInt(pullNumber, 10) || null, + } + }) - const appsWithPullIds = appsPlusPullIds.filter(appi => appi.repo && appi.pullNumber > 0) + const appsWithPullIds = appsPlusPullIds.filter((appi) => appi.repo && appi.pullNumber > 0) const nonMatchingAppNames = appsPlusPullIds - .filter(appi => !(appi.repo && appi.pullNumber > 0)) - .map(appi => appi.app.name) - .filter(name => !protectedAppNames.includes(name)) + .filter((appi) => !(appi.repo && appi.pullNumber > 0)) + .map((appi) => appi.app.name) + .filter((name) => !protectedAppNames.includes(name)) let staleCount = 0 let spammyCount = 0 @@ -79,27 +79,32 @@ async function main () { stale: { total: staleCount, spammy: spammyCount, - closed: staleCount - spammyCount - } + closed: staleCount - spammyCount, + }, } console.log(`๐Ÿงฎ COUNTS!\n${JSON.stringify(counts, null, 2)}`) const nonMatchingCount = nonMatchingAppNames.length if (nonMatchingCount > 0) { - console.log('โš ๏ธ ๐Ÿ‘€', chalk.yellow(`Non-matching app names (${nonMatchingCount}):\n - ${nonMatchingAppNames.join('\n - ')}`)) + console.log( + 'โš ๏ธ ๐Ÿ‘€', + chalk.yellow( + `Non-matching app names (${nonMatchingCount}):\n - ${nonMatchingAppNames.join('\n - ')}` + ) + ) } } -function displayParams (params) { +function displayParams(params) { const { owner, repo, pull_number: pullNumber } = params return `${owner}/${repo}#${pullNumber}` } -async function assessPullRequest (repo, pullNumber) { +async function assessPullRequest(repo, pullNumber) { const params = { owner: 'github', repo: repo, - pull_number: pullNumber + pull_number: pullNumber, } let isStale = false @@ -125,11 +130,14 @@ async function assessPullRequest (repo, pullNumber) { return { isStale, isSpammy } } -async function deleteHerokuApp (appName) { +async function deleteHerokuApp(appName) { try { await heroku.delete(`/apps/${appName}`) console.log('โœ…', chalk.green(`Removed stale app "${appName}"`)) } catch (error) { - console.log('โŒ', chalk.red(`ERROR: Failed to remove stale app "${appName}" - ${error.message}`)) + console.log( + 'โŒ', + chalk.red(`ERROR: Failed to remove stale app "${appName}" - ${error.message}`) + ) } } diff --git a/script/remove-unused-assets.js b/script/remove-unused-assets.js index 2c882c6848ae..f445bcbc8086 100755 --- a/script/remove-unused-assets.js +++ b/script/remove-unused-assets.js @@ -5,7 +5,6 @@ import fs from 'fs' import findUnusedAssets from './helpers/find-unused-assets.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) - // [start-readme] // // Run this script to remove reusables and image files that exist in the repo but @@ -18,7 +17,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const dryRun = process.argv.slice(2).includes('--dry-run') main() -async function main () { +async function main() { if (dryRun) { console.log('This is a dry run! The script will report unused files without deleting anything.') } @@ -28,61 +27,64 @@ async function main () { printUnusedVariables(await findUnusedAssets('variables')) } -function removeUnusedReusables (reusables) { +function removeUnusedReusables(reusables) { logMessage(reusables, 'reusable') - reusables.forEach(reusable => { - const reusablePath = path.join(__dirname, '..', reusable - .replace('site', '') - .replace(/\./g, '/') - .replace(/$/, '.md')) - dryRun - ? console.log(reusable) - : fs.unlinkSync(reusablePath) + reusables.forEach((reusable) => { + const reusablePath = path.join( + __dirname, + '..', + reusable.replace('site', '').replace(/\./g, '/').replace(/$/, '.md') + ) + dryRun ? console.log(reusable) : fs.unlinkSync(reusablePath) }) } -function removeUnusedImages (images) { +function removeUnusedImages(images) { logMessage(images, 'image') - images.forEach(image => { + images.forEach((image) => { const imagePath = path.join(__dirname, '..', image) - dryRun - ? console.log(image) - : fs.unlinkSync(imagePath) + dryRun ? console.log(image) : fs.unlinkSync(imagePath) }) } // multiple variables are embedded in within the same YML file // so we can't just delete the files, and we can't parse/modify // them either because js-yaml does not preserve whitespace :[ -function printUnusedVariables (variables) { +function printUnusedVariables(variables) { logMessage(variables, 'variable') - variables.forEach(variable => { + variables.forEach((variable) => { const variableKey = variable.split('.').pop() - const variablePath = path.join(process.cwd(), variable - .replace('site', '') - .replace(`.${variableKey}`, '') - .replace(/\./g, '/') - .replace(/$/, '.yml')) + const variablePath = path.join( + process.cwd(), + variable + .replace('site', '') + .replace(`.${variableKey}`, '') + .replace(/\./g, '/') + .replace(/$/, '.yml') + ) dryRun ? console.log(variable) - : console.log(`* found but did not delete '${variableKey}' in ${variablePath.replace(process.cwd(), '')}`) + : console.log( + `* found but did not delete '${variableKey}' in ${variablePath.replace( + process.cwd(), + '' + )}` + ) }) if (!dryRun) console.log('\nYou will need to manually delete any variables you want to remove.') } -function logMessage (list, type) { +function logMessage(list, type) { let action if (dryRun) { action = '\n**Found' } else { - action = type === 'variable' - ? ':eyes: **Found' - : ':scissors: **Removed' + action = type === 'variable' ? ':eyes: **Found' : ':scissors: **Removed' } console.log(`${action} ${list.length} unused ${type} ${list.length === 1 ? 'file' : 'files'}**\n`) diff --git a/script/reset-known-broken-translation-files.js b/script/reset-known-broken-translation-files.js index 9b289a674630..0d873f8ab48d 100755 --- a/script/reset-known-broken-translation-files.js +++ b/script/reset-known-broken-translation-files.js @@ -23,29 +23,29 @@ if (!process.env.GITHUB_TOKEN) { main() -async function main () { +async function main() { // Get body text of OP from https://github.com/github/localization-support/issues/489. - const { data: { body } } = await github.issues.get({ + const { + data: { body }, + } = await github.issues.get({ owner: 'github', repo: 'localization-support', - issue_number: '489' + issue_number: '489', }) // Get the list of broken files from the body text. - const brokenFiles = body - .replace(/^[\s\S]*?## List of Broken Translations/m, '') - .trim() + const brokenFiles = body.replace(/^[\s\S]*?## List of Broken Translations/m, '').trim() // Turn it into a simple array of files. - const brokenFilesArray = brokenFiles - .split('\n') - .map(line => line.replace('- [ ] ', '').trim()) + const brokenFilesArray = brokenFiles.split('\n').map((line) => line.replace('- [ ] ', '').trim()) // Run the script to revert them. - await Promise.all(brokenFilesArray.map(async (file) => { - console.log(`resetting ${file}`) - await exec(`script/reset-translated-file.js --prefer-main ${file}`) - })) + await Promise.all( + brokenFilesArray.map(async (file) => { + console.log(`resetting ${file}`) + await exec(`script/reset-translated-file.js --prefer-main ${file}`) + }) + ) // Print a message with next steps. console.log(` diff --git a/script/reset-translated-file.js b/script/reset-translated-file.js index 6ce08cdd9171..79a807bde180 100755 --- a/script/reset-translated-file.js +++ b/script/reset-translated-file.js @@ -23,14 +23,19 @@ import chalk from 'chalk' // // [end-readme] - program .description('reset translated files') - .option('-m, --prefer-main', 'Reset file to the translated file, try using the file from `main` branch first, if not found (usually due to renaming), fall back to English source.') + .option( + '-m, --prefer-main', + 'Reset file to the translated file, try using the file from `main` branch first, if not found (usually due to renaming), fall back to English source.' + ) .parse(process.argv) const resetToEnglishSource = (translationFilePath) => { - assert(translationFilePath.startsWith('translations/'), 'path argument must be in the format `translations/<lang>/path/to/file`') + assert( + translationFilePath.startsWith('translations/'), + 'path argument must be in the format `translations/<lang>/path/to/file`' + ) assert(fs.existsSync(translationFilePath), `file does not exist: ${translationFilePath}`) const relativePath = translationFilePath.split(path.sep).slice(2).join(path.sep) @@ -47,9 +52,7 @@ const [pathArg] = program.args assert(pathArg, 'first arg must be a target filename') // Is the arg a fully-qualified path? -const relativePath = fs.existsSync(pathArg) - ? path.relative(process.cwd(), pathArg) - : pathArg +const relativePath = fs.existsSync(pathArg) ? path.relative(process.cwd(), pathArg) : pathArg if (program.opts().preferMain) { try { @@ -57,7 +60,11 @@ if (program.opts().preferMain) { console.log('-> reverted to file from main branch: %s', relativePath) } catch (e) { if (e.message.includes('pathspec')) { - console.warn(chalk.red(`cannot find ${relativePath} in main branch (likely because it was renamed); falling back to English source file.`)) + console.warn( + chalk.red( + `cannot find ${relativePath} in main branch (likely because it was renamed); falling back to English source file.` + ) + ) resetToEnglishSource(relativePath) } else { console.warn(e.message) diff --git a/script/rest/openapi-check.js b/script/rest/openapi-check.js index ed3f0b72dd7e..981d9e1f2d41 100755 --- a/script/rest/openapi-check.js +++ b/script/rest/openapi-check.js @@ -13,12 +13,15 @@ import getOperations from './utils/get-operations.js' program .description('Generate dereferenced OpenAPI and decorated schema files.') - .requiredOption('-f, --files [files...]', 'A list of OpenAPI description files to check. Can parse literal glob patterns.') + .requiredOption( + '-f, --files [files...]', + 'A list of OpenAPI description files to check. Can parse literal glob patterns.' + ) .parse(process.argv) const filenames = program.opts().files -const filesToCheck = filenames.flatMap(filename => glob.sync(filename)) +const filesToCheck = filenames.flatMap((filename) => glob.sync(filename)) if (filesToCheck.length) { check(filesToCheck) @@ -27,21 +30,26 @@ if (filesToCheck.length) { process.exit(1) } -async function check (files) { +async function check(files) { console.log('Verifying OpenAPI files are valid with decorator') - const documents = files.map(filename => [filename, JSON.parse(fs.readFileSync(path.join(process.cwd(), filename)))]) + const documents = files.map((filename) => [ + filename, + JSON.parse(fs.readFileSync(path.join(process.cwd(), filename))), + ]) for (const [filename, schema] of documents) { try { // munge OpenAPI definitions object in an array of operations objects const operations = await getOperations(schema) // process each operation, asynchronously rendering markdown and stuff - await Promise.all(operations.map(operation => operation.process())) + await Promise.all(operations.map((operation) => operation.process())) console.log(`Successfully could decorate OpenAPI operations for document ${filename}`) } catch (error) { console.error(error) - console.log(`๐Ÿ› Whoops! It looks like the decorator script wasn't able to parse the dereferenced schema in file ${filename}. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help.`) + console.log( + `๐Ÿ› Whoops! It looks like the decorator script wasn't able to parse the dereferenced schema in file ${filename}. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help.` + ) process.exit(1) } } diff --git a/script/rest/update-files.js b/script/rest/update-files.js index 5d2d233e87a7..95c0f941f36a 100755 --- a/script/rest/update-files.js +++ b/script/rest/update-files.js @@ -22,18 +22,23 @@ const decoratedPath = path.join(process.cwd(), 'lib/rest/static/decorated') program .description('Generate dereferenced OpenAPI and decorated schema files.') - .option('--decorate-only', 'โš ๏ธ Only used by a ๐Ÿค– to generate decorated schema files from existing dereferenced schema files.') + .option( + '--decorate-only', + 'โš ๏ธ Only used by a ๐Ÿค– to generate decorated schema files from existing dereferenced schema files.' + ) .parse(process.argv) const decorateOnly = program.opts().decorateOnly main() -async function main () { +async function main() { // Generate the dereferenced OpenAPI schema files if (!decorateOnly) { if (!fs.existsSync(githubRepoDir)) { - console.log(`๐Ÿ›‘ The ${githubRepoDir} does not exist. Make sure you have a local, bootstrapped checkout of github/github at the same level as your github/docs-internal repo before running this script.`) + console.log( + `๐Ÿ›‘ The ${githubRepoDir} does not exist. Make sure you have a local, bootstrapped checkout of github/github at the same level as your github/docs-internal repo before running this script.` + ) process.exit(1) } @@ -42,12 +47,16 @@ async function main () { await decorate() - console.log('\n๐Ÿ The static REST API files are now up-to-date with your local `github/github` checkout. To revert uncommitted changes, run `git checkout lib/rest/static/*.\n\n') + console.log( + '\n๐Ÿ The static REST API files are now up-to-date with your local `github/github` checkout. To revert uncommitted changes, run `git checkout lib/rest/static/*.\n\n' + ) } -async function getDereferencedFiles () { +async function getDereferencedFiles() { // Get the github/github repo branch name and pull latest - const githubBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: githubRepoDir }).toString().trim() + const githubBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: githubRepoDir }) + .toString() + .trim() // Only pull master branch because development mode branches are assumed // to be up-to-date during active work. @@ -59,12 +68,19 @@ async function getDereferencedFiles () { rimraf(tempDocsDir) mkdirp(tempDocsDir) - console.log(`\n๐Ÿƒโ€โ™€๏ธ๐Ÿƒ๐Ÿƒโ€โ™€๏ธRunning \`bin/openapi bundle\` in branch '${githubBranch}' of your github/github checkout to generate the dereferenced OpenAPI schema files.\n`) + console.log( + `\n๐Ÿƒโ€โ™€๏ธ๐Ÿƒ๐Ÿƒโ€โ™€๏ธRunning \`bin/openapi bundle\` in branch '${githubBranch}' of your github/github checkout to generate the dereferenced OpenAPI schema files.\n` + ) try { - execSync(`${path.join(githubRepoDir, 'bin/openapi')} bundle -o ${tempDocsDir} --include_unpublished`, { stdio: 'inherit' }) + execSync( + `${path.join(githubRepoDir, 'bin/openapi')} bundle -o ${tempDocsDir} --include_unpublished`, + { stdio: 'inherit' } + ) } catch (error) { console.error(error) - console.log('๐Ÿ›‘ Whoops! It looks like the `bin/openapi bundle` command failed to run in your `github/github` repository checkout. To troubleshoot, ensure that your OpenAPI schema YAML is formatted correctly. A CI test runs on your `github/github` PR that flags malformed YAML. You can check the PR diff view for comments left by the openapi CI test to find and fix any formatting errors.') + console.log( + '๐Ÿ›‘ Whoops! It looks like the `bin/openapi bundle` command failed to run in your `github/github` repository checkout. To troubleshoot, ensure that your OpenAPI schema YAML is formatted correctly. A CI test runs on your `github/github` PR that flags malformed YAML. You can check the PR diff view for comments left by the openapi CI test to find and fix any formatting errors.' + ) process.exit(1) } @@ -76,14 +92,14 @@ async function getDereferencedFiles () { // property in the dereferenced schema is replaced with the branch // name of the `github/github` checkout. A CI test // checks the version and fails if it's not a semantic version. - schemas.forEach(filename => { + schemas.forEach((filename) => { const schema = JSON.parse(fs.readFileSync(path.join(dereferencedPath, filename))) schema.info.version = `${githubBranch} !!DEVELOPMENT MODE - DO NOT MERGE!!` fs.writeFileSync(path.join(dereferencedPath, filename), JSON.stringify(schema, null, 2)) }) } -async function decorate () { +async function decorate() { console.log('\n๐ŸŽ„ Decorating the OpenAPI schema files in lib/rest/static/dereferenced.\n') const dereferencedSchemas = schemas.reduce((acc, filename) => { @@ -98,17 +114,18 @@ async function decorate () { const operations = await getOperations(schema) // process each operation, asynchronously rendering markdown and stuff - await Promise.all(operations.map(operation => operation.process())) + await Promise.all(operations.map((operation) => operation.process())) - const filename = path.join(decoratedPath, `${schemaName}.json`) - .replace('.deref', '') + const filename = path.join(decoratedPath, `${schemaName}.json`).replace('.deref', '') // write processed operations to disk fs.writeFileSync(filename, JSON.stringify(operations, null, 2)) console.log('Wrote', path.relative(process.cwd(), filename)) } catch (error) { console.error(error) - console.log('๐Ÿ› Whoops! It looks like the decorator script wasn\'t able to parse the dereferenced schema. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help.') + console.log( + "๐Ÿ› Whoops! It looks like the decorator script wasn't able to parse the dereferenced schema. A recent change may not yet be supported by the decorator. Please reach out in the #docs-engineering slack channel for help." + ) process.exit(1) } } diff --git a/script/rest/utils/create-code-samples.js b/script/rest/utils/create-code-samples.js index 5021333935ea..0c3b640dbe7f 100644 --- a/script/rest/utils/create-code-samples.js +++ b/script/rest/utils/create-code-samples.js @@ -4,30 +4,29 @@ import { stringify } from 'javascript-stringify' import { get, mapValues, snakeCase } from 'lodash-es' export default createCodeSamples - const PARAMETER_EXAMPLES = { owner: 'octocat', repo: 'hello-world', email: 'octocat@github.com', - emails: ['octocat@github.com'] + emails: ['octocat@github.com'], } -function createCodeSamples (operation) { +function createCodeSamples(operation) { const route = { method: operation.verb.toUpperCase(), path: operation.requestPath, - operation + operation, } const serverUrl = operation.serverUrl const codeSampleParams = { route, serverUrl } return [ { lang: 'Shell', source: toShellExample(codeSampleParams) }, - { lang: 'JavaScript', source: toJsExample(codeSampleParams) } + { lang: 'JavaScript', source: toJsExample(codeSampleParams) }, ] } -function toShellExample ({ route, serverUrl }) { +function toShellExample({ route, serverUrl }) { const pathParams = mapValues(getExamplePathParams(route), (value, paramName) => PARAMETER_EXAMPLES[paramName] ? value : snakeCase(value).toUpperCase() ) @@ -35,8 +34,9 @@ function toShellExample ({ route, serverUrl }) { const params = getExampleBodyParams(route) const { method } = route - const requiredPreview = get(route, 'operation.x-github.previews', []) - .find(preview => preview.required) + const requiredPreview = get(route, 'operation.x-github.previews', []).find( + (preview) => preview.required + ) const defaultAcceptHeader = requiredPreview ? `application/vnd.github.${requiredPreview.name}-preview+json` @@ -48,7 +48,7 @@ function toShellExample ({ route, serverUrl }) { if (route.operation.contentType === 'application/x-www-form-urlencoded') { requestBodyParams = '' const paramNames = Object.keys(params) - paramNames.forEach(elem => { + paramNames.forEach((elem) => { requestBodyParams = `${requestBodyParams} --data-urlencode ${elem}=${params[elem]}` }) requestBodyParams = requestBodyParams.trim() @@ -58,20 +58,20 @@ function toShellExample ({ route, serverUrl }) { method !== 'GET' && `-X ${method}`, defaultAcceptHeader ? `-H "Accept: ${defaultAcceptHeader}"` : '', `${serverUrl}${path}`, - Object.keys(params).length && requestBodyParams + Object.keys(params).length && requestBodyParams, ].filter(Boolean) return `curl \\\n ${args.join(' \\\n ')}` } -function toJsExample ({ route }) { +function toJsExample({ route }) { const params = route.operation.parameters - .filter(param => !param.deprecated) - .filter(param => param.in !== 'header') - .filter(param => param.required) + .filter((param) => !param.deprecated) + .filter((param) => param.in !== 'header') + .filter((param) => param.required) .reduce( (_params, param) => Object.assign(_params, { - [param.name]: getExampleParamValue(param.name, param.schema) + [param.name]: getExampleParamValue(param.name, param.schema), }), {} ) @@ -79,23 +79,23 @@ function toJsExample ({ route }) { // add any required preview headers to the params object const requiredPreviewNames = get(route.operation, 'x-github.previews', []) - .filter(preview => preview.required) - .map(preview => preview.name) + .filter((preview) => preview.required) + .map((preview) => preview.name) if (requiredPreviewNames.length) { Object.assign(params, { - mediaType: { previews: requiredPreviewNames } + mediaType: { previews: requiredPreviewNames }, }) } // add required content type header (presently only for `POST /markdown/raw`) - const contentTypeHeader = route.operation.parameters.find(param => { + const contentTypeHeader = route.operation.parameters.find((param) => { return param.name.toLowerCase() === 'content-type' && get(param, 'schema.enum') }) if (contentTypeHeader) { Object.assign(params, { - headers: { 'content-type': contentTypeHeader.schema.enum[0] } + headers: { 'content-type': contentTypeHeader.schema.enum[0] }, }) } @@ -103,8 +103,8 @@ function toJsExample ({ route }) { return `await octokit.request('${route.method} ${route.path}'${args})` } -function getExamplePathParams ({ operation }) { - const pathParams = operation.parameters.filter(param => param.in === 'path') +function getExamplePathParams({ operation }) { + const pathParams = operation.parameters.filter((param) => param.in === 'path') if (pathParams.length === 0) { return {} } @@ -114,7 +114,7 @@ function getExamplePathParams ({ operation }) { }, {}) } -function getExampleBodyParams ({ operation }) { +function getExampleBodyParams({ operation }) { const contentType = Object.keys(get(operation, 'requestBody.content', []))[0] let schema try { @@ -149,7 +149,7 @@ function getExampleBodyParams ({ operation }) { }, {}) } -function getExampleParamValue (name, schema) { +function getExampleParamValue(name, schema) { const value = PARAMETER_EXAMPLES[name] if (value) { return value diff --git a/script/rest/utils/get-operations.js b/script/rest/utils/get-operations.js index a1ef7b12a2d5..c7a49ef2f30b 100644 --- a/script/rest/utils/get-operations.js +++ b/script/rest/utils/get-operations.js @@ -5,13 +5,12 @@ import Operation from './operation.js' // and returns an array of its operation objects with their // HTTP verb and requestPath attached as properties -export default async function getOperations (schema) { +export default async function getOperations(schema) { const operations = [] for (const [requestPath, operationsAtPath] of Object.entries(schema.paths)) { for (const [verb, props] of Object.entries(operationsAtPath)) { - const serverUrl = schema.servers[0].url - .replace('{protocol}', 'http(s)') + const serverUrl = schema.servers[0].url.replace('{protocol}', 'http(s)') const operation = new Operation(verb, requestPath, props, serverUrl) operations.push(operation) } diff --git a/script/rest/utils/operation-schema.js b/script/rest/utils/operation-schema.js index 7db0a22707b4..a539b59f571c 100644 --- a/script/rest/utils/operation-schema.js +++ b/script/rest/utils/operation-schema.js @@ -13,11 +13,10 @@ export default { 'slug', 'x-codeSamples', 'category', - 'categoryLabel' + 'categoryLabel', ], properties: { - // Properties from the source OpenAPI schema that this module depends on externalDocs: { description: 'The public documentation for the given operation', @@ -25,60 +24,60 @@ export default { required: ['description', 'url'], properties: { description: { - type: 'string' + type: 'string', }, url: { - type: 'string' - } - } + type: 'string', + }, + }, }, operationId: { type: 'string', - minLength: 1 + minLength: 1, }, parameters: { description: 'Parameters to the operation that can be present in the URL path, the query, headers, or a POST body', - type: 'array' + type: 'array', }, // Additional derived properties not found in the source OpenAPI schema verb: { description: 'The HTTP method', type: 'string', - enum: ['get', 'put', 'post', 'delete', 'patch', 'head'] + enum: ['get', 'put', 'post', 'delete', 'patch', 'head'], }, requestPath: { description: 'The URL path', type: 'string', - minLength: 1 + minLength: 1, }, descriptionHTML: { description: 'The rendered HTML version of the markdown `description` property', - type: 'string' + type: 'string', }, notes: { - type: 'array' + type: 'array', }, slug: { description: 'GitHub.com-style param-case property for use as a unique DOM id', - type: 'string' + type: 'string', }, category: { description: 'the `issues` in `/v3/issues/events/`; supports legacy developer site URLs', - type: 'string' + type: 'string', }, categoryLabel: { description: 'humanized form of category', - type: 'string' + type: 'string', }, subcategory: { description: 'the `events` in `/v3/issues/events/`; supports legacy developer site URLs', - type: 'string' + type: 'string', }, subcategoryLabel: { description: 'humanized form of subcategory', - type: 'string' - } - } + type: 'string', + }, + }, } diff --git a/script/rest/utils/operation.js b/script/rest/utils/operation.js index 6493c66d885b..23f9e648419d 100644 --- a/script/rest/utils/operation.js +++ b/script/rest/utils/operation.js @@ -13,11 +13,11 @@ const slugger = new GitHubSlugger() const categoryTitles = { scim: 'SCIM' } export default class Operation { - constructor (verb, requestPath, props, serverUrl) { + constructor(verb, requestPath, props, serverUrl) { const defaultProps = { parameters: [], 'x-codeSamples': [], - responses: {} + responses: {}, } Object.assign(this, { verb, requestPath, serverUrl }, defaultProps, props) @@ -46,11 +46,11 @@ export default class Operation { return this } - get schema () { + get schema() { return operationSchema } - async process () { + async process() { this['x-codeSamples'] = createCodeSamples(this) await Promise.all([ @@ -60,7 +60,7 @@ export default class Operation { this.renderParameterDescriptions(), this.renderBodyParameterDescriptions(), this.renderPreviewNotes(), - this.renderNotes() + this.renderNotes(), ]) const ajv = new Ajv() @@ -71,110 +71,123 @@ export default class Operation { } } - async renderDescription () { + async renderDescription() { this.descriptionHTML = await renderContent(this.description) return this } - async renderCodeSamples () { - return Promise.all(this['x-codeSamples'].map(async (sample) => { - const markdown = createCodeBlock(sample.source, sample.lang.toLowerCase()) - sample.html = await renderContent(markdown) - return sample - })) + async renderCodeSamples() { + return Promise.all( + this['x-codeSamples'].map(async (sample) => { + const markdown = createCodeBlock(sample.source, sample.lang.toLowerCase()) + sample.html = await renderContent(markdown) + return sample + }) + ) } - async renderResponses () { + async renderResponses() { // clone and delete this.responses so we can turn it into a clean array of objects const rawResponses = JSON.parse(JSON.stringify(this.responses)) delete this.responses - this.responses = await Promise.all(Object.keys(rawResponses).map(async (responseCode) => { - const rawResponse = rawResponses[responseCode] - const httpStatusCode = responseCode - const httpStatusMessage = httpStatusCodes.getMessage(Number(responseCode)) - const responseDescription = rawResponse.description - - const cleanResponses = [] - - /* Responses can have zero, one, or multiple examples. The `examples` - * property often only contains one example object. Both the `example` - * and `examples` properties can be used in the OpenAPI but `example` - * doesn't work with `$ref`. - * This works: - * schema: - * '$ref': '../../components/schemas/foo.yaml' - * example: - * id: 10 - * description: This is a summary - * foo: bar - * - * This doesn't - * schema: - * '$ref': '../../components/schemas/foo.yaml' - * example: - * '$ref': '../../components/examples/bar.yaml' - */ - const examplesProperty = get(rawResponse, 'content.application/json.examples') - const exampleProperty = get(rawResponse, 'content.application/json.example') - - // Return early if the response doesn't have an example payload - if (!exampleProperty && !examplesProperty) { - return [{ - httpStatusCode, - httpStatusMessage, - description: responseDescription - }] - } - - // Use the same format for `example` as `examples` property so that all - // examples can be handled the same way. - const normalizedExampleProperty = { - default: { - value: exampleProperty + this.responses = await Promise.all( + Object.keys(rawResponses).map(async (responseCode) => { + const rawResponse = rawResponses[responseCode] + const httpStatusCode = responseCode + const httpStatusMessage = httpStatusCodes.getMessage(Number(responseCode)) + const responseDescription = rawResponse.description + + const cleanResponses = [] + + /* Responses can have zero, one, or multiple examples. The `examples` + * property often only contains one example object. Both the `example` + * and `examples` properties can be used in the OpenAPI but `example` + * doesn't work with `$ref`. + * This works: + * schema: + * '$ref': '../../components/schemas/foo.yaml' + * example: + * id: 10 + * description: This is a summary + * foo: bar + * + * This doesn't + * schema: + * '$ref': '../../components/schemas/foo.yaml' + * example: + * '$ref': '../../components/examples/bar.yaml' + */ + const examplesProperty = get(rawResponse, 'content.application/json.examples') + const exampleProperty = get(rawResponse, 'content.application/json.example') + + // Return early if the response doesn't have an example payload + if (!exampleProperty && !examplesProperty) { + return [ + { + httpStatusCode, + httpStatusMessage, + description: responseDescription, + }, + ] } - } - const rawExamples = examplesProperty || normalizedExampleProperty - const rawExampleKeys = Object.keys(rawExamples) - - for (const exampleKey of rawExampleKeys) { - const exampleValue = rawExamples[exampleKey].value - const exampleSummary = rawExamples[exampleKey].summary - const cleanResponse = { - httpStatusCode, - httpStatusMessage + // Use the same format for `example` as `examples` property so that all + // examples can be handled the same way. + const normalizedExampleProperty = { + default: { + value: exampleProperty, + }, } - // If there is only one example, use the response description - // property. For cases with more than one example, some don't have - // summary properties with a description, so we can sentence case - // the property name as a fallback - cleanResponse.description = rawExampleKeys.length === 1 - ? exampleSummary || responseDescription - : exampleSummary || sentenceCase(exampleKey) - - const payloadMarkdown = createCodeBlock(exampleValue, 'json') - cleanResponse.payload = await renderContent(payloadMarkdown) - - cleanResponses.push(cleanResponse) - } - return cleanResponses - })) + const rawExamples = examplesProperty || normalizedExampleProperty + const rawExampleKeys = Object.keys(rawExamples) + + for (const exampleKey of rawExampleKeys) { + const exampleValue = rawExamples[exampleKey].value + const exampleSummary = rawExamples[exampleKey].summary + const cleanResponse = { + httpStatusCode, + httpStatusMessage, + } + + // If there is only one example, use the response description + // property. For cases with more than one example, some don't have + // summary properties with a description, so we can sentence case + // the property name as a fallback + cleanResponse.description = + rawExampleKeys.length === 1 + ? exampleSummary || responseDescription + : exampleSummary || sentenceCase(exampleKey) + + const payloadMarkdown = createCodeBlock(exampleValue, 'json') + cleanResponse.payload = await renderContent(payloadMarkdown) + + cleanResponses.push(cleanResponse) + } + return cleanResponses + }) + ) // flatten child arrays this.responses = flatten(this.responses) } - async renderParameterDescriptions () { - return Promise.all(this.parameters.map(async (param) => { - param.descriptionHTML = await renderContent(param.description) - return param - })) + async renderParameterDescriptions() { + return Promise.all( + this.parameters.map(async (param) => { + param.descriptionHTML = await renderContent(param.description) + return param + }) + ) } - async renderBodyParameterDescriptions () { - let bodyParamsObject = get(this, `requestBody.content.${this.contentType}.schema.properties`, {}) + async renderBodyParameterDescriptions() { + let bodyParamsObject = get( + this, + `requestBody.content.${this.contentType}.schema.properties`, + {} + ) let requiredParams = get(this, `requestBody.content.${this.contentType}.schema.required`, []) const oneOfObject = get(this, `requestBody.content.${this.contentType}.schema.oneOf`, undefined) @@ -182,9 +195,8 @@ export default class Operation { // use the first option or munge the options together. if (oneOfObject) { const firstOneOfObject = oneOfObject[0] - const allOneOfAreObjects = oneOfObject - .filter(elem => elem.type === 'object') - .length === oneOfObject.length + const allOneOfAreObjects = + oneOfObject.filter((elem) => elem.type === 'object').length === oneOfObject.length // TODO: Remove this check // This operation shouldn't have a oneOf in this case, it needs to be @@ -198,8 +210,7 @@ export default class Operation { // first requestBody object. for (let i = 1; i < oneOfObject.length; i++) { Object.assign(firstOneOfObject.properties, oneOfObject[i].properties) - requiredParams = firstOneOfObject.required - .concat(oneOfObject[i].required) + requiredParams = firstOneOfObject.required.concat(oneOfObject[i].required) } bodyParamsObject = firstOneOfObject.properties } else if (oneOfObject) { @@ -215,28 +226,29 @@ export default class Operation { this.bodyParameters = await getBodyParams(bodyParamsObject, requiredParams) } - async renderPreviewNotes () { - const previews = get(this, 'x-github.previews', []) - .filter(preview => preview.note) - - return Promise.all(previews.map(async (preview) => { - const note = preview.note - // remove extra leading and trailing newlines - .replace(/```\n\n\n/mg, '```\n') - .replace(/```\n\n/mg, '```\n') - .replace(/\n\n\n```/mg, '\n```') - .replace(/\n\n```/mg, '\n```') - - // convert single-backtick code snippets to fully fenced triple-backtick blocks - // example: This is the description.\n\n`application/vnd.github.machine-man-preview+json` - .replace(/\n`application/, '\n```\napplication') - .replace(/json`$/, 'json\n```') - preview.html = await renderContent(note) - })) + async renderPreviewNotes() { + const previews = get(this, 'x-github.previews', []).filter((preview) => preview.note) + + return Promise.all( + previews.map(async (preview) => { + const note = preview.note + // remove extra leading and trailing newlines + .replace(/```\n\n\n/gm, '```\n') + .replace(/```\n\n/gm, '```\n') + .replace(/\n\n\n```/gm, '\n```') + .replace(/\n\n```/gm, '\n```') + + // convert single-backtick code snippets to fully fenced triple-backtick blocks + // example: This is the description.\n\n`application/vnd.github.machine-man-preview+json` + .replace(/\n`application/, '\n```\napplication') + .replace(/json`$/, 'json\n```') + preview.html = await renderContent(note) + }) + ) } // add additional notes to this array whenever we want - async renderNotes () { + async renderNotes() { this.notes = [] return Promise.all(this.notes.map(async (note) => renderContent(note))) @@ -244,94 +256,99 @@ export default class Operation { } // need to use this function recursively to get child and grandchild params -async function getBodyParams (paramsObject, requiredParams) { +async function getBodyParams(paramsObject, requiredParams) { if (!isPlainObject(paramsObject)) return [] - return Promise.all(Object.keys(paramsObject).map(async (paramKey) => { - const param = paramsObject[paramKey] - param.name = paramKey - param.in = 'body' - param.rawType = param.type - param.rawDescription = param.description - - // Stores the types listed under the `Type` column in the `Parameters` - // table in the REST API docs. When the parameter contains oneOf - // there are multiple acceptable parameters that we should list. - const paramArray = [] - - const oneOfArray = param.oneOf - const isOneOfObjectOrArray = oneOfArray - ? oneOfArray.filter(elem => elem.type !== 'object' || elem.type !== 'array') - : false - - // When oneOf has the type array or object, the type is defined - // in a child object - if (oneOfArray && isOneOfObjectOrArray.length > 0) { - // Store the defined types - paramArray.push(oneOfArray - .filter(elem => elem.type) - .map(elem => elem.type) - ) - - // If an object doesn't have a description, it is invalid - const oneOfArrayWithDescription = oneOfArray.filter(elem => elem.description) - - // Use the parent description when set, otherwise enumerate each - // description in the `Description` column of the `Parameters` table. - if (!param.description && oneOfArrayWithDescription.length > 1) { - param.description = oneOfArray - .filter(elem => elem.description) - .map(elem => `**Type ${elem.type}** - ${elem.description}`) - .join('\n\n') - } else if (!param.description && oneOfArrayWithDescription.length === 1) { - // When there is only on valid description, use that one. - param.description = oneOfArrayWithDescription[0].description + return Promise.all( + Object.keys(paramsObject).map(async (paramKey) => { + const param = paramsObject[paramKey] + param.name = paramKey + param.in = 'body' + param.rawType = param.type + param.rawDescription = param.description + + // Stores the types listed under the `Type` column in the `Parameters` + // table in the REST API docs. When the parameter contains oneOf + // there are multiple acceptable parameters that we should list. + const paramArray = [] + + const oneOfArray = param.oneOf + const isOneOfObjectOrArray = oneOfArray + ? oneOfArray.filter((elem) => elem.type !== 'object' || elem.type !== 'array') + : false + + // When oneOf has the type array or object, the type is defined + // in a child object + if (oneOfArray && isOneOfObjectOrArray.length > 0) { + // Store the defined types + paramArray.push(oneOfArray.filter((elem) => elem.type).map((elem) => elem.type)) + + // If an object doesn't have a description, it is invalid + const oneOfArrayWithDescription = oneOfArray.filter((elem) => elem.description) + + // Use the parent description when set, otherwise enumerate each + // description in the `Description` column of the `Parameters` table. + if (!param.description && oneOfArrayWithDescription.length > 1) { + param.description = oneOfArray + .filter((elem) => elem.description) + .map((elem) => `**Type ${elem.type}** - ${elem.description}`) + .join('\n\n') + } else if (!param.description && oneOfArrayWithDescription.length === 1) { + // When there is only on valid description, use that one. + param.description = oneOfArrayWithDescription[0].description + } } - } - // Arrays require modifying the displayed type (e.g., array of strings) - if (param.type === 'array') { - if (param.items.type) paramArray.push(`array of ${param.items.type}s`) - if (param.items.oneOf) { - paramArray.push(param.items.oneOf - .map(elem => `array of ${elem.type}s`) - ) + // Arrays require modifying the displayed type (e.g., array of strings) + if (param.type === 'array') { + if (param.items.type) paramArray.push(`array of ${param.items.type}s`) + if (param.items.oneOf) { + paramArray.push(param.items.oneOf.map((elem) => `array of ${elem.type}s`)) + } + } else if (param.type) { + paramArray.push(param.type) } - } else if (param.type) { - paramArray.push(param.type) - } - if (param.nullable) paramArray.push('nullable') + if (param.nullable) paramArray.push('nullable') - param.type = paramArray.flat().join(' or ') - param.description = param.description || '' - const isRequired = requiredParams && requiredParams.includes(param.name) - const requiredString = isRequired ? '**Required**. ' : '' - param.description = await renderContent(requiredString + param.description) + param.type = paramArray.flat().join(' or ') + param.description = param.description || '' + const isRequired = requiredParams && requiredParams.includes(param.name) + const requiredString = isRequired ? '**Required**. ' : '' + param.description = await renderContent(requiredString + param.description) - // there may be zero, one, or multiple object parameters that have children parameters - param.childParamsGroups = [] - const childParamsGroup = await getChildParamsGroup(param) + // there may be zero, one, or multiple object parameters that have children parameters + param.childParamsGroups = [] + const childParamsGroup = await getChildParamsGroup(param) - if (childParamsGroup && childParamsGroup.params.length) { - param.childParamsGroups.push(childParamsGroup) - } + if (childParamsGroup && childParamsGroup.params.length) { + param.childParamsGroups.push(childParamsGroup) + } - // if the param is an object, it may have child object params that have child params :/ - if (param.rawType === 'object') { - param.childParamsGroups.push(...flatten(childParamsGroup.params - .filter(param => param.childParamsGroups.length) - .map(param => param.childParamsGroups))) - } + // if the param is an object, it may have child object params that have child params :/ + if (param.rawType === 'object') { + param.childParamsGroups.push( + ...flatten( + childParamsGroup.params + .filter((param) => param.childParamsGroups.length) + .map((param) => param.childParamsGroups) + ) + ) + } - return param - })) + return param + }) + ) } -async function getChildParamsGroup (param) { +async function getChildParamsGroup(param) { // only objects, arrays of objects, anyOf, allOf, and oneOf have child params if (!(param.rawType === 'array' || param.rawType === 'object' || param.oneOf)) return - if (param.oneOf && !param.oneOf.filter(param => param.type === 'object' || param.type === 'array')) return + if ( + param.oneOf && + !param.oneOf.filter((param) => param.type === 'object' || param.type === 'array') + ) + return if (param.items && param.items.type !== 'object') return const childParamsObject = param.rawType === 'array' ? param.items.properties : param.properties @@ -349,11 +366,11 @@ async function getChildParamsGroup (param) { parentName: param.name, parentType, id, - params: childParams + params: childParams, } } -function createCodeBlock (input, language) { +function createCodeBlock(input, language) { // stringify JSON if needed if (language === 'json' && typeof input !== 'string') { input = JSON.stringify(input, null, 2) diff --git a/script/search/algolia-get-remote-index-names.js b/script/search/algolia-get-remote-index-names.js index 0248c3909d02..22956a559742 100644 --- a/script/search/algolia-get-remote-index-names.js +++ b/script/search/algolia-get-remote-index-names.js @@ -2,14 +2,14 @@ import { namePrefix } from '../../lib/search/config.js' import getAlgoliaClient from './algolia-client.js' -export default async function getRemoteIndexNames () { +export default async function getRemoteIndexNames() { const algoliaClient = getAlgoliaClient() const indices = await algoliaClient.listIndices() // ignore other indices that may be present in the Algolia account like `helphub-`, etc const indexNames = indices.items - .map(field => field.name) - .filter(name => name.startsWith(namePrefix)) + .map((field) => field.name) + .filter((name) => name.startsWith(namePrefix)) return indexNames } diff --git a/script/search/algolia-search-index.js b/script/search/algolia-search-index.js index 821ec2cf92f8..e8e71597f1a4 100644 --- a/script/search/algolia-search-index.js +++ b/script/search/algolia-search-index.js @@ -6,31 +6,30 @@ import validateRecords from './validate-records.js' import getAlgoliaClient from './algolia-client.js' class AlgoliaIndex { - constructor (name, records) { + constructor(name, records) { this.name = name - this.records = records - .map(record => { - record.customRanking = rank(record) - return record - }) + this.records = records.map((record) => { + record.customRanking = rank(record) + return record + }) this.validate() return this } - validate () { + validate() { return validateRecords(this.name, this.records) } // This method consumes Algolia's `browseObjects` event emitter, // aggregating results into an array of all the records // https://www.algolia.com/doc/api-client/getting-started/upgrade-guides/javascript/#the-browse-and-browsefrom-methods - async fetchExistingRecords () { + async fetchExistingRecords() { const client = getAlgoliaClient() // return an empty array if the index does not exist yet const { items: indices } = await client.listIndices() - if (!indices.find(index => index.name === this.name)) { + if (!indices.find((index) => index.name === this.name)) { console.log(`index '${this.name}' does not exist!`) return [] } @@ -39,13 +38,13 @@ class AlgoliaIndex { let records = [] await index.browseObjects({ - batch: batch => (records = records.concat(batch)) + batch: (batch) => (records = records.concat(batch)), }) return records } - async syncWithRemote () { + async syncWithRemote() { const client = getAlgoliaClient() console.log('\n\nsyncing %s with remote', this.name) @@ -60,11 +59,11 @@ class AlgoliaIndex { // Create a hash of every existing record, to compare to the new records // The `object-hash` module is indifferent to object key order by default. :+1: - const existingHashes = existingRecords.map(record => objectHash(record)) + const existingHashes = existingRecords.map((record) => objectHash(record)) // If a hash is found, that means the existing Algolia record contains the // same data as new record, and the record doesn't need to be updated. - const recordsToUpdate = this.records.filter(record => { + const recordsToUpdate = this.records.filter((record) => { return !existingHashes.includes(objectHash(record)) }) diff --git a/script/search/build-records.js b/script/search/build-records.js index 6643dfd60d47..5da495dc2aa3 100644 --- a/script/search/build-records.js +++ b/script/search/build-records.js @@ -8,23 +8,23 @@ const pageMarker = chalk.green('|') const recordMarker = chalk.grey('.') const port = 4002 -export default async function buildRecords (indexName, indexablePages, pageVersion, languageCode) { +export default async function buildRecords(indexName, indexablePages, pageVersion, languageCode) { console.log(`\n\nBuilding records for index '${indexName}' (${languages[languageCode].name})`) const records = [] const pages = indexablePages // exclude pages that are not in the current language - .filter(page => page.languageCode === languageCode) + .filter((page) => page.languageCode === languageCode) // exclude pages that don't have a permalink for the current product version - .filter(page => page.permalinks.some(permalink => permalink.pageVersion === pageVersion)) + .filter((page) => page.permalinks.some((permalink) => permalink.pageVersion === pageVersion)) // Find the approve permalink for the given language and GitHub product variant (dotcom v enterprise) const permalinks = pages - .map(page => { - return page.permalinks.find(permalink => { + .map((page) => { + return page.permalinks.find((permalink) => { return permalink.languageCode === languageCode && permalink.pageVersion === pageVersion }) }) - .map(permalink => { + .map((permalink) => { permalink.url = `http://localhost:${port}${permalink.href}` return permalink }) diff --git a/script/search/find-indexable-pages.js b/script/search/find-indexable-pages.js index 5c7b8603a312..43458dba8118 100644 --- a/script/search/find-indexable-pages.js +++ b/script/search/find-indexable-pages.js @@ -1,15 +1,15 @@ #!/usr/bin/env node import { loadPages } from '../../lib/page-data.js' -export default async function findIndexablePages () { +export default async function findIndexablePages() { const allPages = await loadPages() const indexablePages = allPages // exclude hidden pages - .filter(page => !page.hidden) + .filter((page) => !page.hidden) // exclude pages that are part of WIP or hidden products - .filter(page => !page.parentProduct || !page.parentProduct.wip || page.parentProduct.hidden) + .filter((page) => !page.parentProduct || !page.parentProduct.wip || page.parentProduct.hidden) // exclude index homepages - .filter(page => !page.relativePath.endsWith('index.md')) + .filter((page) => !page.relativePath.endsWith('index.md')) console.log('total pages', allPages.length) console.log('indexable pages', indexablePages.length) diff --git a/script/search/lunr-get-index-names.js b/script/search/lunr-get-index-names.js index 0a20fe0a4e0e..89456f73282a 100644 --- a/script/search/lunr-get-index-names.js +++ b/script/search/lunr-get-index-names.js @@ -5,6 +5,6 @@ import xFs from 'fs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const fs = xFs.promises -export default async function getIndexNames () { +export default async function getIndexNames() { return await fs.readdir(path.join(__dirname, '../../lib/search/indexes')) } diff --git a/script/search/lunr-search-index.js b/script/search/lunr-search-index.js index 6a2591215a24..2b7b339a71aa 100644 --- a/script/search/lunr-search-index.js +++ b/script/search/lunr-search-index.js @@ -22,11 +22,11 @@ xLunrDe(lunr) const fs = xFs.promises export default class LunrIndex { - constructor (name, records) { + constructor(name, records) { this.name = name // Add custom rankings - this.records = records.map(record => { + this.records = records.map((record) => { record.customRanking = rank(record) return record }) @@ -36,15 +36,16 @@ export default class LunrIndex { return this } - validate () { + validate() { return validateRecords(this.name, this.records) } - build () { + build() { const language = this.name.split('-').pop() const records = this.records - this.index = lunr(function constructIndex () { // No arrow here! + this.index = lunr(function constructIndex() { + // No arrow here! if (['ja', 'es', 'pt', 'de'].includes(language)) { this.use(lunr[language]) } @@ -67,38 +68,40 @@ export default class LunrIndex { }) } - toJSON () { + toJSON() { this.build() return JSON.stringify(this.index, null, 2) } - get recordsObject () { - return Object.fromEntries( - this.records.map(record => [record.objectID, record]) - ) + get recordsObject() { + return Object.fromEntries(this.records.map((record) => [record.objectID, record])) } - async write () { + async write() { this.build() // Write the parsed records await Promise.resolve(this.recordsObject) .then(JSON.stringify) .then(compress) - .then(content => fs.writeFile( - path.posix.join(__dirname, '../../lib/search/indexes', `${this.name}-records.json.br`), - content - // Do not set to 'utf8' - )) + .then((content) => + fs.writeFile( + path.posix.join(__dirname, '../../lib/search/indexes', `${this.name}-records.json.br`), + content + // Do not set to 'utf8' + ) + ) // Write the index await Promise.resolve(this.index) .then(JSON.stringify) .then(compress) - .then(content => fs.writeFile( - path.posix.join(__dirname, '../../lib/search/indexes', `${this.name}.json.br`), - content - // Do not set to 'utf8' - )) + .then((content) => + fs.writeFile( + path.posix.join(__dirname, '../../lib/search/indexes', `${this.name}.json.br`), + content + // Do not set to 'utf8' + ) + ) } } diff --git a/script/search/parse-page-sections-into-records.js b/script/search/parse-page-sections-into-records.js index f6b4ff82e6e4..130640803303 100644 --- a/script/search/parse-page-sections-into-records.js +++ b/script/search/parse-page-sections-into-records.js @@ -6,20 +6,13 @@ import { maxContentLength } from '../../lib/search/config.js' // that follows each heading becomes the content of the search record. const urlPrefix = 'https://docs.github.com' -const ignoredHeadingSlugs = [ - 'in-this-article', - 'further-reading' -] +const ignoredHeadingSlugs = ['in-this-article', 'further-reading'] -export default function parsePageSectionsIntoRecords (href, $) { +export default function parsePageSectionsIntoRecords(href, $) { const title = $('h1').text().trim() const breadcrumbsArray = $('nav.breadcrumbs a') .map((i, el) => { - return $(el) - .text() - .trim() - .replace(/\n/g, ' ') - .replace(/\s+/g, ' ') + return $(el).text().trim().replace(/\n/g, ' ').replace(/\s+/g, ' ') }) .get() .slice(0, -1) @@ -67,7 +60,7 @@ export default function parsePageSectionsIntoRecords (href, $) { heading, title, content, - topics + topics, } }) .get() @@ -75,24 +68,26 @@ export default function parsePageSectionsIntoRecords (href, $) { // There are no sections. Treat the entire article as the record. const objectID = href const url = [urlPrefix, objectID].join('') - const content = $('.article-grid-body p, .article-grid-body ul, .article-grid-body ol, .article-grid-body table') + const content = $( + '.article-grid-body p, .article-grid-body ul, .article-grid-body ol, .article-grid-body table' + ) .map((i, el) => $(el).text()) .get() .join(' ') .trim() .slice(0, maxContentLength) - records = [{ - objectID, - url, - breadcrumbs, - title, - content, - topics - }] + records = [ + { + objectID, + url, + breadcrumbs, + title, + content, + topics, + }, + ] } - return chain(records) - .uniqBy('objectID') - .value() + return chain(records).uniqBy('objectID').value() } diff --git a/script/search/rank.js b/script/search/rank.js index 7ab88c6c23f2..efeeffe606ca 100644 --- a/script/search/rank.js +++ b/script/search/rank.js @@ -5,13 +5,9 @@ // higher in this list == higher search ranking // anything NOT matched by this list gets the highest ranking // a lower ranking means the record will have a higher priority -const rankings = [ - '/rest', - '/graphql', - '/site-policy' -].reverse() +const rankings = ['/rest', '/graphql', '/site-policy'].reverse() -export default function rank (record) { +export default function rank(record) { for (const index in rankings) { const pattern = rankings[index] if (record.url.includes(pattern)) return Number(index) diff --git a/script/search/sync.js b/script/search/sync.js index 1d562033ccec..0d38cae4f66f 100644 --- a/script/search/sync.js +++ b/script/search/sync.js @@ -25,38 +25,48 @@ const cacheDir = path.join(process.cwd(), './.search-cache') // Build a search data file for every combination of product version and language // e.g. `github-docs-dotcom-en.json` and `github-docs-2.14-ja.json` -export default async function syncSearchIndexes (opts = {}) { +export default async function syncSearchIndexes(opts = {}) { if (opts.dryRun) { - console.log('This is a dry run! The script will build the indices locally but not upload anything.\n') + console.log( + 'This is a dry run! The script will build the indices locally but not upload anything.\n' + ) rimraf(cacheDir) mkdirp(cacheDir) } if (opts.language) { if (!Object.keys(languages).includes(opts.language)) { - console.log(`Error! ${opts.language} not found. You must provide a currently supported two-letter language code.`) + console.log( + `Error! ${opts.language} not found. You must provide a currently supported two-letter language code.` + ) process.exit(1) } } if (opts.version) { if (!Object.keys(allVersions).includes(opts.version)) { - console.log(`Error! ${opts.version} not found. You must provide a currently supported version in <PLAN@RELEASE> format.`) + console.log( + `Error! ${opts.version} not found. You must provide a currently supported version in <PLAN@RELEASE> format.` + ) process.exit(1) } } // build indices for a specific language if provided; otherwise build indices for all languages const languagesToBuild = opts.language - ? Object.keys(languages).filter(language => language === opts.language) + ? Object.keys(languages).filter((language) => language === opts.language) : Object.keys(languages) // build indices for a specific version if provided; otherwise build indices for all veersions const versionsToBuild = opts.version - ? Object.keys(allVersions).filter(version => version === opts.version) + ? Object.keys(allVersions).filter((version) => version === opts.version) : Object.keys(allVersions) - console.log(`Building indices for ${opts.language || 'all languages'} and ${opts.version || 'all versions'}.\n`) + console.log( + `Building indices for ${opts.language || 'all languages'} and ${ + opts.version || 'all versions' + }.\n` + ) // Exclude WIP pages, hidden pages, index pages, etc const indexablePages = await findIndexablePages() @@ -67,9 +77,10 @@ export default async function syncSearchIndexes (opts = {}) { // if GHES, resolves to the release number like 2.21, 2.22, etc. // if FPT, resolves to 'dotcom' // if GHAE, resolves to 'ghae' - const indexVersion = allVersions[pageVersion].plan === 'enterprise-server' - ? allVersions[pageVersion].currentRelease - : allVersions[pageVersion].miscBaseName + const indexVersion = + allVersions[pageVersion].plan === 'enterprise-server' + ? allVersions[pageVersion].currentRelease + : allVersions[pageVersion].miscBaseName // github-docs-dotcom-en, github-docs-2.22-en const indexName = `${namePrefix}-${indexVersion}-${languageCode}` @@ -102,13 +113,12 @@ export default async function syncSearchIndexes (opts = {}) { ? await getLunrIndexNames() : await getRemoteIndexNames() const cachedIndexNamesFile = path.join(__dirname, '../../lib/search/cached-index-names.json') - fs.writeFileSync( - cachedIndexNamesFile, - JSON.stringify(remoteIndexNames, null, 2) - ) + fs.writeFileSync(cachedIndexNamesFile, JSON.stringify(remoteIndexNames, null, 2)) if (!process.env.CI) { - console.log(chalk.green(`\nCached index names in ${path.relative(process.cwd(), cachedIndexNamesFile)}`)) + console.log( + chalk.green(`\nCached index names in ${path.relative(process.cwd(), cachedIndexNamesFile)}`) + ) console.log(chalk.green('(If this file has any changes, please commit them)')) } diff --git a/script/search/validate-records.js b/script/search/validate-records.js index 5ad9a746fea1..f252f6a5a981 100644 --- a/script/search/validate-records.js +++ b/script/search/validate-records.js @@ -5,18 +5,18 @@ import isURL from 'is-url' import countArrayValues from 'count-array-values' import { maxRecordLength } from '../../lib/search/config.js' -export default function validateRecords (name, records) { +export default function validateRecords(name, records) { assert(isString(name) && name.length, '`name` is required') assert(isArray(records) && records.length, '`records` must be a non-empty array') // each ID is unique - const objectIDs = records.map(record => record.objectID) + const objectIDs = records.map((record) => record.objectID) const dupes = countArrayValues(objectIDs) .filter(({ value, count }) => count > 1) .map(({ value }) => value) assert(!dupes.length, `every objectID must be unique. dupes: ${dupes.join('; ')}`) - records.forEach(record => { + records.forEach((record) => { assert( isString(record.objectID) && record.objectID.length, `objectID must be a string. received: ${record.objectID}, ${JSON.stringify(record)}` diff --git a/script/standardize-frontmatter-order.js b/script/standardize-frontmatter-order.js index 7151a770229d..4b90392bc529 100755 --- a/script/standardize-frontmatter-order.js +++ b/script/standardize-frontmatter-order.js @@ -10,8 +10,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const properties = Object.keys(schema.properties) const contentDir = path.join(__dirname, '../content') -const contentFiles = walk(contentDir, { includeBasePath: true }) - .filter(relativePath => relativePath.endsWith('.md') && !relativePath.includes('README')) +const contentFiles = walk(contentDir, { includeBasePath: true }).filter( + (relativePath) => relativePath.endsWith('.md') && !relativePath.includes('README') +) // [start-readme] // @@ -28,10 +29,10 @@ const contentFiles = walk(contentDir, { includeBasePath: true }) // // [end-readme] -contentFiles.forEach(fullPath => { +contentFiles.forEach((fullPath) => { const { content, data } = matter(fs.readFileSync(fullPath, 'utf8')) const newData = {} - properties.forEach(prop => { + properties.forEach((prop) => { if (data[prop]) newData[prop] = data[prop] }) diff --git a/script/sync-search-indices.js b/script/sync-search-indices.js index 2ab563d4613b..3e4dc26f62e5 100755 --- a/script/sync-search-indices.js +++ b/script/sync-search-indices.js @@ -11,12 +11,12 @@ import 'make-promises-safe' main() -async function main () { +async function main() { const sync = searchSync const opts = { dryRun: 'DRY_RUN' in process.env, language: process.env.LANGUAGE, - version: process.env.VERSION + version: process.env.VERSION, } await sync(opts) } diff --git a/script/test-render-translation.js b/script/test-render-translation.js index d93023672223..3885fcd1d577 100755 --- a/script/test-render-translation.js +++ b/script/test-render-translation.js @@ -23,7 +23,7 @@ const exec = promisify(xChildProcess.exec) main() -async function main () { +async function main() { const siteData = await loadAndPatchSiteData() const pages = await loadPages() const contextByLanguage = {} @@ -34,16 +34,19 @@ async function main () { contextByLanguage[crowdinLangCode] = { site: siteData[langObj.code].site, currentLanguage: langObj.code, - currentVersion: 'free-pro-team@latest' + currentVersion: 'free-pro-team@latest', } } const rootDir = path.join(__dirname, '..') - const changedFilesRelPaths = execSync('git -c diff.renameLimit=10000 diff --name-only origin/main | egrep "^translations/.*/.+.md$"', { maxBuffer: 1024 * 1024 * 100 }) + const changedFilesRelPaths = execSync( + 'git -c diff.renameLimit=10000 diff --name-only origin/main | egrep "^translations/.*/.+.md$"', + { maxBuffer: 1024 * 1024 * 100 } + ) .toString() .split('\n') - .filter(path => path !== '' && !path.endsWith('README.md')) + .filter((path) => path !== '' && !path.endsWith('README.md')) .sort() console.log(`Found ${changedFilesRelPaths.length} translated files.`) @@ -54,8 +57,8 @@ async function main () { const context = { ...contextByLanguage[lang], pages, - page: pages.find(page => page.fullPath === fullPath), - redirects: {} + page: pages.find((page) => page.fullPath === fullPath), + redirects: {}, } if (!context.page && !relPath.includes('data/reusables')) continue const fileContents = await fs.promises.readFile(fullPath, 'utf8') @@ -69,7 +72,7 @@ async function main () { } } -async function loadAndPatchSiteData (filesWithKnownIssues = {}) { +async function loadAndPatchSiteData(filesWithKnownIssues = {}) { try { const siteData = loadSiteData() return siteData diff --git a/script/update-crowdin-issue.js b/script/update-crowdin-issue.js index aeadb5ffec46..835ab6bcd4b0 100755 --- a/script/update-crowdin-issue.js +++ b/script/update-crowdin-issue.js @@ -25,10 +25,14 @@ const parsingErrorsLog = '~/docs-translation-parsing-error.txt' const renderingErrorsLog = '~/docs-translation-rendering-error.txt' // Get just the fixable files: -const fixable = execSync(`cat ${fixableErrorsLog} | egrep "^translations/.*/(.+.md|.+.yml)$" | sed -e 's/^/- [ ] /' | uniq`).toString() +const fixable = execSync( + `cat ${fixableErrorsLog} | egrep "^translations/.*/(.+.md|.+.yml)$" | sed -e 's/^/- [ ] /' | uniq` +).toString() // Get a list of files to be added to the body of the issue -const filesToAdd = execSync(`cat ${parsingErrorsLog} ${renderingErrorsLog} | egrep "^translations/.*/(.+.md|.+.yml)$" | sed -e 's/^/- [ ] /' | uniq`).toString() +const filesToAdd = execSync( + `cat ${parsingErrorsLog} ${renderingErrorsLog} | egrep "^translations/.*/(.+.md|.+.yml)$" | sed -e 's/^/- [ ] /' | uniq` +).toString() // Cat the three error logs together const allErrors = execSync('cat ~/docs-*').toString() @@ -61,19 +65,21 @@ const issueNumber = '489' main() -async function main () { +async function main() { await updateIssueBody() await addNewComment() console.log('Success! You can safely delete the temporary logfiles under ~/docs-*.') } -async function updateIssueBody () { +async function updateIssueBody() { // Get current body text of OP from https://github.com/github/localization-support/issues/489. - const { data: { body } } = await github.issues.get({ + const { + data: { body }, + } = await github.issues.get({ owner, repo, - issue_number: issueNumber + issue_number: issueNumber, }) // Update the body with the list of newly broken files @@ -85,25 +91,29 @@ async function updateIssueBody () { owner, repo, issue_number: issueNumber, - body: newBody + body: newBody, }) - console.log('Added newly found broken files to OP of https://github.com/github/localization-support/issues/489!\n') + console.log( + 'Added newly found broken files to OP of https://github.com/github/localization-support/issues/489!\n' + ) } catch (err) { console.error(err) } } -async function addNewComment () { +async function addNewComment() { try { await github.issues.createComment({ owner, repo, issue_number: issueNumber, - body: comment + body: comment, }) - console.log('Added comment to the end of https://github.com/github/localization-support/issues/489!\n') + console.log( + 'Added comment to the end of https://github.com/github/localization-support/issues/489!\n' + ) } catch (err) { console.error(err) } diff --git a/script/update-enterprise-dates.js b/script/update-enterprise-dates.js index 6cd888c8cdb4..a50b5035e073 100755 --- a/script/update-enterprise-dates.js +++ b/script/update-enterprise-dates.js @@ -23,13 +23,17 @@ if (!process.env.GITHUB_TOKEN) { main() -async function main () { +async function main() { // send owner, repo, ref, path let rawDates = [] try { - rawDates = JSON.parse(await getContents('github', 'enterprise-releases', 'master', 'releases.json')) + rawDates = JSON.parse( + await getContents('github', 'enterprise-releases', 'master', 'releases.json') + ) } catch { - console.log('Failed to get the https://github.com/github/enterprise-releases/blob/master/releases.json content. Check that your token has the correct permissions.') + console.log( + 'Failed to get the https://github.com/github/enterprise-releases/blob/master/releases.json content. Check that your token has the correct permissions.' + ) process.exit(1) } @@ -37,7 +41,7 @@ async function main () { Object.entries(rawDates).forEach(([releaseNumber, releaseObject]) => { formattedDates[releaseNumber] = { releaseDate: releaseObject.release_candidate || releaseObject.start, - deprecationDate: releaseObject.end + deprecationDate: releaseObject.end, } }) diff --git a/script/update-internal-links.js b/script/update-internal-links.js index 17ecfef3cefd..3c59d13c71d9 100755 --- a/script/update-internal-links.js +++ b/script/update-internal-links.js @@ -18,9 +18,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const allVersions = Object.keys(xAllVersions) const walkFiles = (pathToWalk) => { - return walk(path.posix.join(__dirname, '..', pathToWalk), { includeBasePath: true, directories: false }) - .filter(file => file.endsWith('.md') && !file.endsWith('README.md')) - .filter(file => !file.includes('/early-access/')) // ignore EA for now + return walk(path.posix.join(__dirname, '..', pathToWalk), { + includeBasePath: true, + directories: false, + }) + .filter((file) => file.endsWith('.md') && !file.endsWith('README.md')) + .filter((file) => !file.includes('/early-access/')) // ignore EA for now } const allFiles = walkFiles('content').concat(walkFiles('data')) @@ -29,7 +32,7 @@ const allFiles = walkFiles('content').concat(walkFiles('data')) // Hacky but it captures the current rare edge cases. const linkInlineMarkup = { emphasis: '*', - strong: '**' + strong: '**', } const currentVersionWithSpacesRegex = /\/enterprise\/{{ currentVersion }}/g @@ -51,7 +54,7 @@ const currentVersionWithoutSpaces = '/enterprise/{{currentVersion}}' main() -async function main () { +async function main() { console.log('Working...') const pageList = await loadPages() const pageMap = await loadPageMap(pageList) @@ -62,7 +65,7 @@ async function main () { pages: pageMap, redirects, site: site.en.site, - currentLanguage: 'en' + currentLanguage: 'en', } for (const file of allFiles) { @@ -78,7 +81,7 @@ async function main () { // We can't do async functions within visit, so gather the nodes upfront const nodesPerFile = [] - visit(ast, node => { + visit(ast, (node) => { if (node.type !== 'link') return if (!node.url.startsWith('/')) return if (node.url.startsWith('/assets')) return @@ -128,9 +131,7 @@ async function main () { versionMatch = oldLink.match(/(enterprise-server(?:@.[^/]*?)?)\//) // Remove the fragment for now. - linkToCheck = linkToCheck - .replace(/#.*$/, '') - .replace(patterns.trailingSlash, '$1') + linkToCheck = linkToCheck.replace(/#.*$/, '').replace(patterns.trailingSlash, '$1') // Try to find the rendered link in the set of pages! foundPage = findPage(linkToCheck, pageMap, redirects) @@ -143,22 +144,31 @@ async function main () { } if (!foundPage) { - console.error(`Can't find link in pageMap! ${oldLink} in ${file.replace(process.cwd(), '')}`) + console.error( + `Can't find link in pageMap! ${oldLink} in ${file.replace(process.cwd(), '')}` + ) process.exit(1) } // If the original link includes a fragment OR the original title includes Liquid, do not change; // otherwise, use the found page title. (We don't want to update the title if a fragment is found because // the title likely points to the fragment section header, not the page title.) - const newTitle = fragmentMatch || oldTitle.includes('{%') || !hasQuotesAroundLink ? oldTitle : foundPage.title + const newTitle = + fragmentMatch || oldTitle.includes('{%') || !hasQuotesAroundLink + ? oldTitle + : foundPage.title // If the original link includes a fragment, append it to the found page path. // Also remove the language code because Markdown links don't include language codes. - let newLink = getPathWithoutLanguage(fragmentMatch ? foundPage.path + fragmentMatch[1] : foundPage.path) + let newLink = getPathWithoutLanguage( + fragmentMatch ? foundPage.path + fragmentMatch[1] : foundPage.path + ) // If the original link includes a hardcoded version, preserve it; otherwise, remove versioning // because Markdown links don't include versioning. - newLink = versionMatch ? `/${versionMatch[1]}${getPathWithoutVersion(newLink)}` : getPathWithoutVersion(newLink) + newLink = versionMatch + ? `/${versionMatch[1]}${getPathWithoutVersion(newLink)}` + : getPathWithoutVersion(newLink) let newMarkdownLink = `[${inlineMarkup}${newTitle}${inlineMarkup}](${newLink})` @@ -183,18 +193,18 @@ async function main () { console.log('Done!') } -function findPage (tryPath, pageMap, redirects) { +function findPage(tryPath, pageMap, redirects) { if (pageMap[tryPath]) { return { title: pageMap[tryPath].title, - path: tryPath + path: tryPath, } } if (pageMap[redirects[tryPath]]) { return { title: pageMap[redirects[tryPath]].title, - path: redirects[tryPath] + path: redirects[tryPath], } } } diff --git a/script/update-readme.js b/script/update-readme.js index f409330ce172..9561dc2ba646 100755 --- a/script/update-readme.js +++ b/script/update-readme.js @@ -21,37 +21,33 @@ const endComment = 'end-readme' const startCommentRegex = new RegExp(startComment) const endCommentRegex = new RegExp(endComment) -const ignoreList = [ - 'README.md' -] +const ignoreList = ['README.md'] -const scriptsToRuleThemAll = [ - 'bootstrap', - 'server', - 'test' -] +const scriptsToRuleThemAll = ['bootstrap', 'server', 'test'] -const allScripts = walk(__dirname, { directories: false }) - .filter(script => ignoreList.every(ignoredPath => !script.includes(ignoredPath))) +const allScripts = walk(__dirname, { directories: false }).filter((script) => + ignoreList.every((ignoredPath) => !script.includes(ignoredPath)) +) const otherScripts = difference(allScripts, scriptsToRuleThemAll) // build an object with script name as key and readme comment as value const allComments = {} -allScripts.forEach(script => { +allScripts.forEach((script) => { const fullPath = path.join(__dirname, script) let addToReadme = false - const readmeComment = fs.readFileSync(fullPath, 'utf8') + const readmeComment = fs + .readFileSync(fullPath, 'utf8') .split('\n') - .filter(cmt => { + .filter((cmt) => { if (startCommentRegex.test(cmt)) addToReadme = true if (endCommentRegex.test(cmt)) addToReadme = false if (addToReadme && !cmt.includes(startComment) && !cmt.includes(endComment)) return cmt return false }) // remove comment markers and clean up newlines - .map(cmt => cmt.replace(/^(\/\/|#) ?/m, '')) + .map((cmt) => cmt.replace(/^(\/\/|#) ?/m, '')) .join('\n') .trim() @@ -84,9 +80,11 @@ if (template === fs.readFileSync(readme, 'utf8')) { console.log('The README.md has been updated!') } -function createTemplate (arrayOfScripts) { - return arrayOfScripts.map(script => { - const comment = allComments[script] - return dedent`### [\`${script}\`](${script})\n\n${comment}\n\n---\n\n` - }).join('\n') +function createTemplate(arrayOfScripts) { + return arrayOfScripts + .map((script) => { + const comment = allComments[script] + return dedent`### [\`${script}\`](${script})\n\n${comment}\n\n---\n\n` + }) + .join('\n') } diff --git a/script/update-versioning-in-files.js b/script/update-versioning-in-files.js index f88b83185b6f..b566b0e63aec 100755 --- a/script/update-versioning-in-files.js +++ b/script/update-versioning-in-files.js @@ -8,61 +8,59 @@ const contentPath = path.join(process.cwd(), 'content') const dataPath = path.join(process.cwd(), 'data') const contentFiles = walk(contentPath, { includeBasePath: true, directories: false }) - .filter(file => file.endsWith('.md')) - .filter(file => !file.endsWith('README.md')) + .filter((file) => file.endsWith('.md')) + .filter((file) => !file.endsWith('README.md')) const dataFiles = walk(dataPath, { includeBasePath: true, directories: false }) - .filter(file => file.includes('data/reusables') || file.includes('data/variables')) - .filter(file => !file.endsWith('README.md')) + .filter((file) => file.includes('data/reusables') || file.includes('data/variables')) + .filter((file) => !file.endsWith('README.md')) -dataFiles - .forEach(file => { - const content = fs.readFileSync(file, 'utf8') +dataFiles.forEach((file) => { + const content = fs.readFileSync(file, 'utf8') - // Update Liquid in data files - const newContent = updateLiquid(content) + // Update Liquid in data files + const newContent = updateLiquid(content) - fs.writeFileSync(file, newContent) - }) + fs.writeFileSync(file, newContent) +}) -contentFiles - .forEach(file => { - const { data, content } = frontmatter(fs.readFileSync(file, 'utf8')) +contentFiles.forEach((file) => { + const { data, content } = frontmatter(fs.readFileSync(file, 'utf8')) - // Update Liquid in content files - const newContent = content ? updateLiquid(content) : '' + // Update Liquid in content files + const newContent = content ? updateLiquid(content) : '' - // Update versions frontmatter - if (data) { - if (!data.versions && data.productVersions) { - data.versions = data.productVersions - Object.keys(data.versions).forEach(version => { - // update dotcom, actions, rest, etc. - if (version !== 'enterprise') { - data.versions['free-pro-team'] = data.versions[version] - delete data.versions[version] - } else { - data.versions['enterprise-server'] = data.versions.enterprise - delete data.versions.enterprise - } - }) - } + // Update versions frontmatter + if (data) { + if (!data.versions && data.productVersions) { + data.versions = data.productVersions + Object.keys(data.versions).forEach((version) => { + // update dotcom, actions, rest, etc. + if (version !== 'enterprise') { + data.versions['free-pro-team'] = data.versions[version] + delete data.versions[version] + } else { + data.versions['enterprise-server'] = data.versions.enterprise + delete data.versions.enterprise + } + }) + } - delete data.productVersions + delete data.productVersions - // Update Liquid in frontmatter props - Object.keys(data) - // Only process a subset of props - .filter(key => key === 'title' || key === 'intro' || key === 'product') - .forEach(key => { - data[key] = updateLiquid(data[key]) - }) - } + // Update Liquid in frontmatter props + Object.keys(data) + // Only process a subset of props + .filter((key) => key === 'title' || key === 'intro' || key === 'product') + .forEach((key) => { + data[key] = updateLiquid(data[key]) + }) + } - fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) - }) + fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) +}) -function updateLiquid (content) { +function updateLiquid(content) { return content .replace(/page.version/g, 'currentVersion') .replace(/["'](?:')?dotcom["'](?:')?/g, '"free-pro-team@latest"') diff --git a/server.mjs b/server.mjs index 53727151b927..3f0425e707cc 100644 --- a/server.mjs +++ b/server.mjs @@ -12,7 +12,6 @@ import http from 'http' xDotenv.config() // Intentionally require these for both cluster primary and workers - const { PORT, NODE_ENV } = process.env const port = Number(PORT) || 4000 @@ -23,21 +22,21 @@ if (NODE_ENV === 'production') { nonClusteredMain() } -function clusteredMain () { +function clusteredMain() { // Spin up a cluster! throng({ master: setupPrimary, worker: setupWorker, - count: calculateWorkerCount() + count: calculateWorkerCount(), }) } -async function nonClusteredMain () { +async function nonClusteredMain() { await checkPortAvailability() await startServer() } -async function checkPortAvailability () { +async function checkPortAvailability() { // Check that the development server is not already running const portInUse = await portUsed.check(port) if (portInUse) { @@ -48,7 +47,7 @@ async function checkPortAvailability () { } } -async function startServer () { +async function startServer() { const app = createApp() // If in a deployed environment... @@ -67,7 +66,7 @@ async function startServer () { } // This function will only be run in the primary process -async function setupPrimary () { +async function setupPrimary() { process.on('beforeExit', () => { console.log('Shutting down primary...') console.log('Exiting!') @@ -79,7 +78,7 @@ async function setupPrimary () { } // IMPORTANT: This function will be run in a separate worker process! -async function setupWorker (id, disconnect) { +async function setupWorker(id, disconnect) { let exited = false // Wrap stdout and stderr to include the worker ID as a static prefix @@ -100,7 +99,7 @@ async function setupWorker (id, disconnect) { // Load the server in each worker process and share the port via sharding await startServer() - function shutdown () { + function shutdown() { if (exited) return exited = true @@ -109,7 +108,7 @@ async function setupWorker (id, disconnect) { } } -function calculateWorkerCount () { +function calculateWorkerCount() { // Heroku's recommended WEB_CONCURRENCY count based on the WEB_MEMORY config, // or explicitly configured by us const { WEB_CONCURRENCY } = process.env diff --git a/stylesheets/breadcrumbs.scss b/stylesheets/breadcrumbs.scss index a46322769609..7e7dc50ea020 100644 --- a/stylesheets/breadcrumbs.scss +++ b/stylesheets/breadcrumbs.scss @@ -1,4 +1,3 @@ - /* Breadcrumbs ------------------------------------------------------------------------------*/ diff --git a/stylesheets/gradient.scss b/stylesheets/gradient.scss index b6062ad72b78..e5e133f72b22 100644 --- a/stylesheets/gradient.scss +++ b/stylesheets/gradient.scss @@ -34,7 +34,7 @@ $gradients: ( -70deg, var(--color-auto-blue-5) 0%, var(--color-auto-pink-5) 100% - ) + ), ) !default; @mixin bg-gradient($parent, $gradient) { diff --git a/stylesheets/images.scss b/stylesheets/images.scss index 1ecb764d285b..b5e786219d29 100644 --- a/stylesheets/images.scss +++ b/stylesheets/images.scss @@ -16,7 +16,8 @@ } // make sure images that contain emoji render at the expected size -img[src*="https://github.githubassets.com/images/icons/emoji"] { +img[src*="https://github.githubassets.com/images/icons/emoji"] +{ height: 20; width: 20; align: absmiddle; @@ -25,4 +26,4 @@ img[src*="https://github.githubassets.com/images/icons/emoji"] { .markdown-body img { max-height: 500px; padding: 0; -} \ No newline at end of file +} diff --git a/stylesheets/utilities.scss b/stylesheets/utilities.scss index cfe2da3f0ccb..ae042941c46e 100644 --- a/stylesheets/utilities.scss +++ b/stylesheets/utilities.scss @@ -7,9 +7,9 @@ } .transition-200 { - transition: 200ms + transition: 200ms; } .rotate-180 { - transform: rotateX(180deg) + transform: rotateX(180deg); } diff --git a/tests/browser/browser.js b/tests/browser/browser.js index 2478bc4ab5ef..461751f77f01 100644 --- a/tests/browser/browser.js +++ b/tests/browser/browser.js @@ -67,7 +67,7 @@ describe('browser search', () => { await newPage.goto('http://localhost:4001/ja/enterprise-server@2.22/admin/installation') await newPage.setRequestInterception(true) - newPage.on('request', interceptedRequest => { + newPage.on('request', (interceptedRequest) => { if (interceptedRequest.method() === 'GET' && /search\?/i.test(interceptedRequest.url())) { const { searchParams } = new URL(interceptedRequest.url()) expect(searchParams.get('version')).toBe('2.22') @@ -77,7 +77,9 @@ describe('browser search', () => { }) await newPage.click('[data-testid=mobile-menu-button]') - const searchInput = await newPage.$('[data-testid=mobile-header] [data-testid=site-search-input]') + const searchInput = await newPage.$( + '[data-testid=mobile-header] [data-testid=site-search-input]' + ) await searchInput.click() await searchInput.type('test') await newPage.waitForSelector('.search-result') @@ -90,7 +92,7 @@ describe('browser search', () => { await newPage.goto('http://localhost:4001/en/github-ae@latest/admin/overview') await newPage.setRequestInterception(true) - newPage.on('request', interceptedRequest => { + newPage.on('request', (interceptedRequest) => { if (interceptedRequest.method() === 'GET' && /search\?/i.test(interceptedRequest.url())) { const { searchParams } = new URL(interceptedRequest.url()) expect(searchParams.get('version')).toBe('ghae') @@ -100,7 +102,9 @@ describe('browser search', () => { }) await newPage.click('[data-testid=mobile-menu-button]') - const searchInput = await newPage.$('[data-testid=mobile-header] [data-testid=site-search-input]') + const searchInput = await newPage.$( + '[data-testid=mobile-header] [data-testid=site-search-input]' + ) await searchInput.click() await searchInput.type('test') await newPage.waitForSelector('.search-result') @@ -110,18 +114,20 @@ describe('browser search', () => { describe('survey', () => { it('sends an event to /events when submitting form', async () => { // Visit a page that displays the prompt - await page.goto('http://localhost:4001/en/actions/getting-started-with-github-actions/about-github-actions') + await page.goto( + 'http://localhost:4001/en/actions/getting-started-with-github-actions/about-github-actions' + ) // Track network requests await page.setRequestInterception(true) - page.on('request', request => { + page.on('request', (request) => { // Ignore GET requests if (!/\/events$/.test(request.url())) return request.continue() expect(request.method()).toMatch(/POST|PUT/) request.respond({ contentType: 'application/json', body: JSON.stringify({ id: 'abcd1234' }), - status: 200 + status: 200, }) }) @@ -145,7 +151,9 @@ describe('survey', () => { describe('csrf meta', () => { it('should have a csrf-token meta tag on the page', async () => { - await page.goto('http://localhost:4001/en/actions/getting-started-with-github-actions/about-github-actions') + await page.goto( + 'http://localhost:4001/en/actions/getting-started-with-github-actions/about-github-actions' + ) await page.waitForSelector('meta[name="csrf-token"]') }) }) @@ -153,14 +161,28 @@ describe('csrf meta', () => { describe('platform specific content', () => { // from tests/javascripts/user-agent.js const userAgents = [ - { name: 'Mac', id: 'mac', ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9' }, - { name: 'Windows', id: 'windows', ua: 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36' }, - { name: 'Linux', id: 'linux', ua: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1' } + { + name: 'Mac', + id: 'mac', + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9', + }, + { + name: 'Windows', + id: 'windows', + ua: 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36', + }, + { + name: 'Linux', + id: 'linux', + ua: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1', + }, ] const linuxUserAgent = userAgents[2] - const pageWithSwitcher = 'http://localhost:4001/en/github/using-git/configuring-git-to-handle-line-endings' + const pageWithSwitcher = + 'http://localhost:4001/en/github/using-git/configuring-git-to-handle-line-endings' const pageWithoutSwitcher = 'http://localhost:4001/en/github/using-git' - const pageWithDefaultPlatform = 'http://localhost:4001/en/actions/hosting-your-own-runners/configuring-the-self-hosted-runner-application-as-a-service' + const pageWithDefaultPlatform = + 'http://localhost:4001/en/actions/hosting-your-own-runners/configuring-the-self-hosted-runner-application-as-a-service' it('should have a platform switcher', async () => { await page.goto(pageWithSwitcher) @@ -187,7 +209,7 @@ describe('platform specific content', () => { await page.setUserAgent(agent.ua) await page.goto(pageWithSwitcher) const selectedPlatformElement = await page.waitForSelector('a.platform-switcher.selected') - const selectedPlatform = await page.evaluate(el => el.textContent, selectedPlatformElement) + const selectedPlatform = await page.evaluate((el) => el.textContent, selectedPlatformElement) expect(selectedPlatform).toBe(agent.name) } }) @@ -196,9 +218,12 @@ describe('platform specific content', () => { for (const agent of userAgents) { await page.setUserAgent(agent.ua) await page.goto(pageWithDefaultPlatform) - const defaultPlatform = await page.$eval('[data-default-platform]', el => el.dataset.defaultPlatform) + const defaultPlatform = await page.$eval( + '[data-default-platform]', + (el) => el.dataset.defaultPlatform + ) const selectedPlatformElement = await page.waitForSelector('a.platform-switcher.selected') - const selectedPlatform = await page.evaluate(el => el.textContent, selectedPlatformElement) + const selectedPlatform = await page.evaluate((el) => el.textContent, selectedPlatformElement) expect(defaultPlatform).toBe(linuxUserAgent.id) expect(selectedPlatform).toBe(linuxUserAgent.name) } @@ -219,7 +244,7 @@ describe('platform specific content', () => { expect(selectedSwitch).toHaveLength(1) // content for NOT selected platforms is expected to become hidden - const otherPlatforms = platforms.filter(e => e !== platform) + const otherPlatforms = platforms.filter((e) => e !== platform) for (const other of otherPlatforms) { await page.waitForSelector(`.extended-markdown.${other}`, { hidden: true, timeout: 3000 }) } @@ -268,10 +293,10 @@ describe('filter cards', () => { await page.goto('http://localhost:4001/en/actions/guides') await page.select('[data-testid=card-filter-dropdown][name="type"]', 'overview') const shownCards = await page.$$('[data-testid=article-card]') - const shownCardTypes = await page.$$eval('[data-testid=article-card-type]', cardTypes => - cardTypes.map(cardType => cardType.textContent) + const shownCardTypes = await page.$$eval('[data-testid=article-card-type]', (cardTypes) => + cardTypes.map((cardType) => cardType.textContent) ) - shownCardTypes.map(type => expect(type).toBe('Overview')) + shownCardTypes.map((type) => expect(type).toBe('Overview')) expect(shownCards.length).toBeGreaterThan(0) }) @@ -279,17 +304,17 @@ describe('filter cards', () => { await page.goto(`http://localhost:4001/en/enterprise-server@${latest}/actions/guides`) await page.select('[data-testid=card-filter-dropdown][name="type"]', 'overview') const shownCards = await page.$$('[data-testid=article-card]') - const shownCardTypes = await page.$$eval('[data-testid=article-card-type]', cardTypes => - cardTypes.map(cardType => cardType.textContent) + const shownCardTypes = await page.$$eval('[data-testid=article-card-type]', (cardTypes) => + cardTypes.map((cardType) => cardType.textContent) ) - shownCardTypes.map(type => expect(type).toBe('Overview')) + shownCardTypes.map((type) => expect(type).toBe('Overview')) expect(shownCards.length).toBeGreaterThan(0) }) }) describe('language banner', () => { it('directs user to the English version of the article', async () => { - const wipLanguageKey = Object.keys(languages).find(key => languages[key].wip) + const wipLanguageKey = Object.keys(languages).find((key) => languages[key].wip) // This kinda sucks, but if we don't have a WIP language, we currently can't // run a reliable test. But hey, on the bright side, if we don't have a WIP @@ -297,7 +322,7 @@ describe('language banner', () => { if (wipLanguageKey) { const res = await page.goto(`http://localhost:4001/${wipLanguageKey}/actions`) expect(res.ok()).toBe(true) - const href = await page.$eval('a#to-english-doc', el => el.href) + const href = await page.$eval('a#to-english-doc', (el) => el.href) expect(href.endsWith('/en/actions')).toBe(true) } }) @@ -353,9 +378,8 @@ describe.skip('next/link client-side navigation', () => { await page.goto('http://localhost:4001/en/actions/guides') const [response] = await Promise.all([ - page.waitForResponse( - (response) => - response.url().startsWith('http://localhost:4001/_next/data') + page.waitForResponse((response) => + response.url().startsWith('http://localhost:4001/_next/data') ), page.waitForNavigation({ waitUntil: 'networkidle2' }), page.click('.sidebar-articles:nth-child(2) .sidebar-article:nth-child(1) a'), diff --git a/tests/content/algolia-search.js b/tests/content/algolia-search.js index 0c3b2bae4636..219323207c18 100644 --- a/tests/content/algolia-search.js +++ b/tests/content/algolia-search.js @@ -7,8 +7,8 @@ const languageCodes = Object.keys(xLanguages) describe('algolia', () => { test('has remote indexNames in every language for every supported GHE version', () => { expect(supported.length).toBeGreaterThan(1) - supported.forEach(version => { - languageCodes.forEach(languageCode => { + supported.forEach((version) => { + languageCodes.forEach((languageCode) => { const indexName = `${namePrefix}-${version}-${languageCode}` // workaround for GHES release branches not in production yet @@ -28,14 +28,14 @@ describe('algolia', () => { test('has remote indexNames in every language for dotcom', async () => { expect(languageCodes.length).toBeGreaterThan(0) - languageCodes.forEach(languageCode => { + languageCodes.forEach((languageCode) => { const indexName = `${namePrefix}-dotcom-${languageCode}` expect(remoteIndexNames.includes(indexName)).toBe(true) }) }) }) -function getDate (date) { +function getDate(date) { const dateObj = date ? new Date(date) : new Date() return dateObj.toISOString().slice(0, 10) } diff --git a/tests/content/category-pages.js b/tests/content/category-pages.js index 858f8d65ccb8..40b223c395ff 100644 --- a/tests/content/category-pages.js +++ b/tests/content/category-pages.js @@ -30,64 +30,63 @@ describe('category pages', () => { globs: ['*/index.md', 'enterprise/*/index.md'], ignore: ['{rest,graphql}/**', 'enterprise/index.md', '**/articles/**', 'early-access/**'], directories: false, - includeBasePath: true + includeBasePath: true, } const productIndices = walk(contentDir, walkOptions) - const productNames = productIndices.map(index => path.basename(path.dirname(index))) + const productNames = productIndices.map((index) => path.basename(path.dirname(index))) // Combine those to fit Jest's `.each` usage const productTuples = zip(productNames, productIndices) - describe.each(productTuples)( - 'product "%s"', - (productName, productIndex) => { - // Get links included in product index page. - // Each link corresponds to a product subdirectory (category). - // Example: "getting-started-with-github" - const contents = fs.readFileSync(productIndex, 'utf8') // TODO move to async - const { data } = matter(contents) - - const productDir = path.dirname(productIndex) - - const categoryLinks = data.children - // Only include category directories, not standalone category files like content/actions/quickstart.md - .filter(link => fs.existsSync(getPath(productDir, link, 'index'))) - // TODO this should move to async, but you can't asynchronously define tests with Jest... - - // Map those to the Markdown file paths that represent that category page index - const categoryPaths = categoryLinks.map(link => getPath(productDir, link, 'index')) - - // Make them relative for nicer display in test names - const categoryRelativePaths = categoryPaths.map(p => path.relative(contentDir, p)) - - // Combine those to fit Jest's `.each` usage - const categoryTuples = zip(categoryRelativePaths, categoryPaths, categoryLinks) - - if (!categoryTuples.length) return - - describe.each(categoryTuples)( - 'category index "%s"', - (indexRelPath, indexAbsPath, indexLink) => { - let publishedArticlePaths, availableArticlePaths, indexTitle, categoryVersions - const articleVersions = {} - - beforeAll(async () => { - const categoryDir = path.dirname(indexAbsPath) - - // Get child article links included in each subdir's index page - const indexContents = await readFileAsync(indexAbsPath, 'utf8') - const { data } = matter(indexContents) - categoryVersions = getApplicableVersions(data.versions, indexAbsPath) - const articleLinks = data.children.filter(child => { - const mdPath = getPath(productDir, indexLink, child) - return fs.existsSync(mdPath) && fs.statSync(mdPath).isFile() - }) + describe.each(productTuples)('product "%s"', (productName, productIndex) => { + // Get links included in product index page. + // Each link corresponds to a product subdirectory (category). + // Example: "getting-started-with-github" + const contents = fs.readFileSync(productIndex, 'utf8') // TODO move to async + const { data } = matter(contents) + + const productDir = path.dirname(productIndex) + + const categoryLinks = data.children + // Only include category directories, not standalone category files like content/actions/quickstart.md + .filter((link) => fs.existsSync(getPath(productDir, link, 'index'))) + // TODO this should move to async, but you can't asynchronously define tests with Jest... + + // Map those to the Markdown file paths that represent that category page index + const categoryPaths = categoryLinks.map((link) => getPath(productDir, link, 'index')) + + // Make them relative for nicer display in test names + const categoryRelativePaths = categoryPaths.map((p) => path.relative(contentDir, p)) + + // Combine those to fit Jest's `.each` usage + const categoryTuples = zip(categoryRelativePaths, categoryPaths, categoryLinks) + + if (!categoryTuples.length) return - // Save the index title for later testing - indexTitle = await renderContent(data.title, { site: siteData }, { textOnly: true }) + describe.each(categoryTuples)( + 'category index "%s"', + (indexRelPath, indexAbsPath, indexLink) => { + let publishedArticlePaths, availableArticlePaths, indexTitle, categoryVersions + const articleVersions = {} - publishedArticlePaths = (await Promise.all( + beforeAll(async () => { + const categoryDir = path.dirname(indexAbsPath) + + // Get child article links included in each subdir's index page + const indexContents = await readFileAsync(indexAbsPath, 'utf8') + const { data } = matter(indexContents) + categoryVersions = getApplicableVersions(data.versions, indexAbsPath) + const articleLinks = data.children.filter((child) => { + const mdPath = getPath(productDir, indexLink, child) + return fs.existsSync(mdPath) && fs.statSync(mdPath).isFile() + }) + + // Save the index title for later testing + indexTitle = await renderContent(data.title, { site: siteData }, { textOnly: true }) + + publishedArticlePaths = ( + await Promise.all( articleLinks.map(async (articleLink) => { const articlePath = getPath(productDir, indexLink, articleLink) const articleContents = await readFileAsync(articlePath, 'utf8') @@ -99,14 +98,18 @@ describe('category pages', () => { // ".../content/github/{category}/{article}.md" => "/{article}" return `/${path.relative(categoryDir, articlePath).replace(/\.md$/, '')}` }) - )).filter(Boolean) + ) + ).filter(Boolean) - // Get all of the child articles that exist in the subdir - const childEntries = await fs.promises.readdir(categoryDir, { withFileTypes: true }) - const childFileEntries = childEntries.filter(ent => ent.isFile() && ent.name !== 'index.md') - const childFilePaths = childFileEntries.map(ent => path.join(categoryDir, ent.name)) + // Get all of the child articles that exist in the subdir + const childEntries = await fs.promises.readdir(categoryDir, { withFileTypes: true }) + const childFileEntries = childEntries.filter( + (ent) => ent.isFile() && ent.name !== 'index.md' + ) + const childFilePaths = childFileEntries.map((ent) => path.join(categoryDir, ent.name)) - availableArticlePaths = (await Promise.all( + availableArticlePaths = ( + await Promise.all( childFilePaths.map(async (articlePath) => { const articleContents = await readFileAsync(articlePath, 'utf8') const { data } = matter(articleContents) @@ -117,62 +120,62 @@ describe('category pages', () => { // ".../content/github/{category}/{article}.md" => "/{article}" return `/${path.relative(categoryDir, articlePath).replace(/\.md$/, '')}` }) - )).filter(Boolean) - - await Promise.all( - childFilePaths.map(async (articlePath) => { - const articleContents = await readFileAsync(articlePath, 'utf8') - const { data } = matter(articleContents) - - articleVersions[articlePath] = getApplicableVersions(data.versions, articlePath) - }) ) - }) - - test('contains all expected articles', () => { - const missingArticlePaths = difference(availableArticlePaths, publishedArticlePaths) - const errorMessage = formatArticleError('Missing article links:', missingArticlePaths) - expect(missingArticlePaths.length, errorMessage).toBe(0) - }) + ).filter(Boolean) - test('does not have any unexpected articles', () => { - const unexpectedArticles = difference(publishedArticlePaths, availableArticlePaths) - const errorMessage = formatArticleError('Unexpected article links:', unexpectedArticles) - expect(unexpectedArticles.length, errorMessage).toBe(0) - }) + await Promise.all( + childFilePaths.map(async (articlePath) => { + const articleContents = await readFileAsync(articlePath, 'utf8') + const { data } = matter(articleContents) - test('contains only articles and map topics with versions that are also available in the parent category', () => { - Object.entries(articleVersions).forEach(([articleName, articleVersions]) => { - const unexpectedVersions = difference(articleVersions, categoryVersions) - const errorMessage = `${articleName} has versions that are not available in parent category` - expect(unexpectedVersions.length, errorMessage).toBe(0) + articleVersions[articlePath] = getApplicableVersions(data.versions, articlePath) }) + ) + }) + + test('contains all expected articles', () => { + const missingArticlePaths = difference(availableArticlePaths, publishedArticlePaths) + const errorMessage = formatArticleError('Missing article links:', missingArticlePaths) + expect(missingArticlePaths.length, errorMessage).toBe(0) + }) + + test('does not have any unexpected articles', () => { + const unexpectedArticles = difference(publishedArticlePaths, availableArticlePaths) + const errorMessage = formatArticleError('Unexpected article links:', unexpectedArticles) + expect(unexpectedArticles.length, errorMessage).toBe(0) + }) + + test('contains only articles and map topics with versions that are also available in the parent category', () => { + Object.entries(articleVersions).forEach(([articleName, articleVersions]) => { + const unexpectedVersions = difference(articleVersions, categoryVersions) + const errorMessage = `${articleName} has versions that are not available in parent category` + expect(unexpectedVersions.length, errorMessage).toBe(0) }) + }) - // TODO: Unskip this test once the related script has been executed - test.skip('slugified title matches parent directory name', () => { - // Get the parent directory name - const categoryDirPath = path.dirname(indexAbsPath) - const categoryDirName = path.basename(categoryDirPath) + // TODO: Unskip this test once the related script has been executed + test.skip('slugified title matches parent directory name', () => { + // Get the parent directory name + const categoryDirPath = path.dirname(indexAbsPath) + const categoryDirName = path.basename(categoryDirPath) - slugger.reset() - const expectedSlug = slugger.slug(entities.decode(indexTitle)) + slugger.reset() + const expectedSlug = slugger.slug(entities.decode(indexTitle)) - // Check if the directory name matches the expected slug - expect(categoryDirName).toBe(expectedSlug) + // Check if the directory name matches the expected slug + expect(categoryDirName).toBe(expectedSlug) - // If this fails, execute "script/reconcile-category-dirs-with-ids.js" - }) - } - ) - } - ) + // If this fails, execute "script/reconcile-category-dirs-with-ids.js" + }) + } + ) + }) }) -function getPath (productDir, link, filename) { +function getPath(productDir, link, filename) { return path.join(productDir, link, `${filename}.md`) } -function formatArticleError (message, articles) { +function formatArticleError(message, articles) { return `${message}\n - ${articles.join('\n - ')}` } diff --git a/tests/content/crowdin-config.js b/tests/content/crowdin-config.js index c17280f3d26d..e280cd99827b 100644 --- a/tests/content/crowdin-config.js +++ b/tests/content/crowdin-config.js @@ -27,11 +27,13 @@ describe('crowdin.yml config file', () => { test('ignores all hidden pages', async () => { const hiddenPages = pages - .filter(page => page.hidden && page.languageCode === 'en') - .map(page => `/content/${page.relativePath}`) - const overlooked = hiddenPages.filter(page => !isIgnored(page, ignoredPagePaths)) + .filter((page) => page.hidden && page.languageCode === 'en') + .map((page) => `/content/${page.relativePath}`) + const overlooked = hiddenPages.filter((page) => !isIgnored(page, ignoredPagePaths)) const message = `Found some hidden pages that are not yet excluded from localization. - Please copy and paste the lines below into the \`ignore\` section of /crowdin.yml: \n\n"${overlooked.join('",\n"')}"` + Please copy and paste the lines below into the \`ignore\` section of /crowdin.yml: \n\n"${overlooked.join( + '",\n"' + )}"` // This may not be true anymore given the separation of Early Access docs // expect(hiddenPages.length).toBeGreaterThan(0) @@ -42,8 +44,8 @@ describe('crowdin.yml config file', () => { // file is ignored if its exact filename in the list, // or if it's within an ignored directory -function isIgnored (filename, ignoredPagePaths) { - return ignoredPagePaths.some(ignoredPath => { +function isIgnored(filename, ignoredPagePaths) { + return ignoredPagePaths.some((ignoredPath) => { const isDirectory = !ignoredPath.endsWith('.md') return ignoredPath === filename || (isDirectory && filename.startsWith(ignoredPath)) }) diff --git a/tests/content/featured-links.js b/tests/content/featured-links.js index 4ca822ccb75b..4a72d5da3a5c 100644 --- a/tests/content/featured-links.js +++ b/tests/content/featured-links.js @@ -17,13 +17,19 @@ describe('featuredLinks', () => { const $ = await getDOM('/en') const $featuredLinks = $('[data-testid=article-list] a') expect($featuredLinks).toHaveLength(9) - expect($featuredLinks.eq(0).attr('href')).toBe('/en/github/getting-started-with-github/set-up-git') + expect($featuredLinks.eq(0).attr('href')).toBe( + '/en/github/getting-started-with-github/set-up-git' + ) expect($featuredLinks.eq(0).children('h4').text().startsWith('Set up Git')).toBe(true) - expect($featuredLinks.eq(0).children('p').text().startsWith('At the heart of GitHub')).toBe(true) + expect($featuredLinks.eq(0).children('p').text().startsWith('At the heart of GitHub')).toBe( + true + ) expect($featuredLinks.eq(8).attr('href')).toBe('/en/github/working-with-github-pages') expect($featuredLinks.eq(8).children('h4').text().startsWith('GitHub Pages')).toBe(true) - expect($featuredLinks.eq(8).children('p').text().startsWith('You can create a website')).toBe(true) + expect($featuredLinks.eq(8).children('p').text().startsWith('You can create a website')).toBe( + true + ) }) test('localized intro links link to localized pages', async () => { @@ -39,9 +45,15 @@ describe('featuredLinks', () => { const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/user/insights`) const $featuredLinks = $('[data-testid=article-list] a') expect($featuredLinks).toHaveLength(6) - expect($featuredLinks.eq(0).attr('href')).toBe(`/en/enterprise-server@${enterpriseServerReleases.latest}/insights/installing-and-configuring-github-insights/about-github-insights`) - expect($featuredLinks.eq(0).children('h4').text().startsWith('About GitHub Insights')).toBe(true) - expect($featuredLinks.eq(0).children('p').text().startsWith('GitHub Insights provides metrics')).toBe(true) + expect($featuredLinks.eq(0).attr('href')).toBe( + `/en/enterprise-server@${enterpriseServerReleases.latest}/insights/installing-and-configuring-github-insights/about-github-insights` + ) + expect($featuredLinks.eq(0).children('h4').text().startsWith('About GitHub Insights')).toBe( + true + ) + expect( + $featuredLinks.eq(0).children('p').text().startsWith('GitHub Insights provides metrics') + ).toBe(true) }) // If any of these tests fail, check to see if the content has changed and update text if needed. @@ -54,12 +66,22 @@ describe('featuredLinks', () => { // Confirm that the following Enterprise link IS included on this Enterprise page. msg = `Enterprise article link is not rendered as expected on ${enterpriseVersionedLandingPage}` - expect($productArticlesLinks.text().includes('Working with a GitHub Packages registry'), msg).toBe(true) + expect( + $productArticlesLinks.text().includes('Working with a GitHub Packages registry'), + msg + ).toBe(true) // Confirm that the following Dotcom-only links are NOT included on this Enterprise page. msg = `Dotcom-only article link is rendered, but should not be, on ${enterpriseVersionedLandingPage}` - expect($productArticlesLinks.text().includes('Working with the Container registry')).toBe(false) - expect($productArticlesLinks.text().includes('Migrating to the Container registry from the Docker registry'), msg).toBe(false) + expect($productArticlesLinks.text().includes('Working with the Container registry')).toBe( + false + ) + expect( + $productArticlesLinks + .text() + .includes('Migrating to the Container registry from the Docker registry'), + msg + ).toBe(false) }) }) @@ -68,7 +90,7 @@ describe('featuredLinks', () => { const gettingStartedLinks = await getJSON('/en?json=featuredLinks.gettingStarted') const expectedFirstLink = { href: '/en/github/getting-started-with-github/set-up-git', - title: 'Set up Git' + title: 'Set up Git', } expect(gettingStartedLinks[0].href).toEqual(expectedFirstLink.href) expect(gettingStartedLinks[0].title).toEqual(expectedFirstLink.title) diff --git a/tests/content/gitignore.js b/tests/content/gitignore.js index 413d475781a9..184fad7d6c79 100644 --- a/tests/content/gitignore.js +++ b/tests/content/gitignore.js @@ -6,6 +6,6 @@ const entries = gitignore.split(/\r?\n/) describe('.gitignore file', () => { test('includes an entry for .env', () => { - expect(entries.some(entry => entry === '.env')).toBe(true) + expect(entries.some((entry) => entry === '.env')).toBe(true) }) }) diff --git a/tests/content/glossary.js b/tests/content/glossary.js index 55cfd146a178..50385862480b 100644 --- a/tests/content/glossary.js +++ b/tests/content/glossary.js @@ -15,7 +15,7 @@ describe('glossaries', () => { }) test('every entry has a valid term', async () => { - function hasValidTerm (entry) { + function hasValidTerm(entry) { return entry.term && entry.term.length && !entry.term.includes('*') } @@ -26,7 +26,7 @@ describe('glossaries', () => { test('external glossary has entries, and they all have descriptions', async () => { expect(glossaries.external.length).toBeGreaterThan(20) - glossaries.external.forEach(entry => { + glossaries.external.forEach((entry) => { const message = `entry '${entry.term}' is missing a description` expect(entry.description && entry.description.length > 0, message).toBe(true) }) @@ -34,7 +34,7 @@ describe('glossaries', () => { test('internal glossary has entries, and they all have descriptions', async () => { expect(glossaries.internal.length).toBeGreaterThan(20) - glossaries.internal.forEach(entry => { + glossaries.internal.forEach((entry) => { const message = `entry '${entry.term}' is missing a description` expect(entry.description && entry.description.length > 0, message).toBe(true) }) @@ -49,7 +49,7 @@ describe('glossaries', () => { test('candidates all have a term, but no description', async () => { expect(glossaries.candidates.length).toBeGreaterThan(20) - glossaries.candidates.forEach(entry => { + glossaries.candidates.forEach((entry) => { const message = `entry '${entry.term}' not expected to have a description` expect(!entry.description, message).toBe(true) }) diff --git a/tests/content/graphql.js b/tests/content/graphql.js index 495f70976301..ccdf9745e40a 100644 --- a/tests/content/graphql.js +++ b/tests/content/graphql.js @@ -1,7 +1,11 @@ import fs from 'fs' import path from 'path' import readJsonFile from '../../lib/read-json-file.js' -import { schemaValidator, previewsValidator, upcomingChangesValidator } from '../../lib/graphql/validator.js' +import { + schemaValidator, + previewsValidator, + upcomingChangesValidator, +} from '../../lib/graphql/validator.js' import revalidator from 'revalidator' import xAllVersions from '../../lib/all-versions.js' import { jest } from '@jest/globals' @@ -10,14 +14,14 @@ const previewsJson = readJsonFile('./lib/graphql/static/previews.json') const upcomingChangesJson = readJsonFile('./lib/graphql/static/upcoming-changes.json') const prerenderedObjectsJson = readJsonFile('./lib/graphql/static/prerendered-objects.json') const allVersions = Object.values(xAllVersions) -const graphqlVersions = allVersions.map(v => v.miscVersionName) -const graphqlTypes = readJsonFile('./lib/graphql/types.json').map(t => t.kind) +const graphqlVersions = allVersions.map((v) => v.miscVersionName) +const graphqlTypes = readJsonFile('./lib/graphql/types.json').map((t) => t.kind) describe('graphql json files', () => { jest.setTimeout(3 * 60 * 1000) test('static files have versions as top-level keys', () => { - graphqlVersions.forEach(version => { + graphqlVersions.forEach((version) => { expect(version in previewsJson).toBe(true) expect(version in upcomingChangesJson).toBe(true) expect(version in prerenderedObjectsJson).toBe(true) @@ -25,13 +29,15 @@ describe('graphql json files', () => { }) test('schemas object validation', () => { - graphqlVersions.forEach(version => { - const schemaJsonPerVersion = JSON.parse(fs.readFileSync(path.join(process.cwd(), `lib/graphql/static/schema-${version}.json`))) + graphqlVersions.forEach((version) => { + const schemaJsonPerVersion = JSON.parse( + fs.readFileSync(path.join(process.cwd(), `lib/graphql/static/schema-${version}.json`)) + ) // all graphql types are arrays except for queries graphqlTypes - .filter(type => type !== 'queries') - .forEach(type => { - schemaJsonPerVersion[type].forEach(typeObj => { + .filter((type) => type !== 'queries') + .forEach((type) => { + schemaJsonPerVersion[type].forEach((typeObj) => { const { valid, errors } = revalidator.validate(typeObj, schemaValidator[type]) const errorMessage = JSON.stringify(errors, null, 2) expect(valid, errorMessage).toBe(true) @@ -39,14 +45,14 @@ describe('graphql json files', () => { }) // check query connections separately - schemaJsonPerVersion.queries.connections.forEach(connection => { + schemaJsonPerVersion.queries.connections.forEach((connection) => { const { valid, errors } = revalidator.validate(connection, schemaValidator.queryConnections) const errorMessage = JSON.stringify(errors, null, 2) expect(valid, errorMessage).toBe(true) }) // check query fields separately - schemaJsonPerVersion.queries.fields.forEach(field => { + schemaJsonPerVersion.queries.fields.forEach((field) => { const { valid, errors } = revalidator.validate(field, schemaValidator.queryFields) const errorMessage = JSON.stringify(errors, null, 2) expect(valid, errorMessage).toBe(true) @@ -55,8 +61,8 @@ describe('graphql json files', () => { }) test('previews object validation', () => { - graphqlVersions.forEach(version => { - previewsJson[version].forEach(previewObj => { + graphqlVersions.forEach((version) => { + previewsJson[version].forEach((previewObj) => { const { valid, errors } = revalidator.validate(previewObj, previewsValidator) const errorMessage = JSON.stringify(errors, null, 2) expect(valid, errorMessage).toBe(true) @@ -65,10 +71,10 @@ describe('graphql json files', () => { }) test('upcoming changes object validation', () => { - graphqlVersions.forEach(version => { - Object.values(upcomingChangesJson[version]).forEach(changes => { + graphqlVersions.forEach((version) => { + Object.values(upcomingChangesJson[version]).forEach((changes) => { // each object value is an array of changes - changes.forEach(changeObj => { + changes.forEach((changeObj) => { const { valid, errors } = revalidator.validate(changeObj, upcomingChangesValidator) const errorMessage = JSON.stringify(errors, null, 2) expect(valid, errorMessage).toBe(true) @@ -78,7 +84,7 @@ describe('graphql json files', () => { }) test('prerendered objects validation', () => { - graphqlVersions.forEach(version => { + graphqlVersions.forEach((version) => { // shape of prerenderedObject: { // html: <div>foo</div>, // miniToc: {contents: '<a>bar</a>', headingLevel: N, indentationLevel: N} diff --git a/tests/content/liquid-line-breaks.js b/tests/content/liquid-line-breaks.js index d854e4173656..d84744043deb 100644 --- a/tests/content/liquid-line-breaks.js +++ b/tests/content/liquid-line-breaks.js @@ -32,7 +32,8 @@ Some examples include: }} */ -const liquidRefsWithLinkBreaksRegex = /\{\{[ \t]*\n\s*[^\s}]+\s*\}\}|\{\{\s*[^\s}]+[ \t]*\n\s*\}\}/gm +const liquidRefsWithLinkBreaksRegex = + /\{\{[ \t]*\n\s*[^\s}]+\s*\}\}|\{\{\s*[^\s}]+[ \t]*\n\s*\}\}/gm describe('Liquid references', () => { describe('must not contain line breaks', () => { @@ -40,15 +41,15 @@ describe('Liquid references', () => { globs: ['**/*.md'], ignore: ['**/README.md'], directories: false, - includeBasePath: true + includeBasePath: true, } const contentMarkdownAbsPaths = walk(contentDir, mdWalkOptions).sort() - const contentMarkdownRelPaths = contentMarkdownAbsPaths.map(p => path.relative(rootDir, p)) + const contentMarkdownRelPaths = contentMarkdownAbsPaths.map((p) => path.relative(rootDir, p)) const contentMarkdownTuples = zip(contentMarkdownRelPaths, contentMarkdownAbsPaths) const reusableMarkdownAbsPaths = walk(reusablesDir, mdWalkOptions).sort() - const reusableMarkdownRelPaths = reusableMarkdownAbsPaths.map(p => path.relative(rootDir, p)) + const reusableMarkdownRelPaths = reusableMarkdownAbsPaths.map((p) => path.relative(rootDir, p)) const reusableMarkdownTuples = zip(reusableMarkdownRelPaths, reusableMarkdownAbsPaths) test.each([...contentMarkdownTuples, ...reusableMarkdownTuples])( @@ -57,8 +58,11 @@ describe('Liquid references', () => { const fileContents = await readFileAsync(markdownAbsPath, 'utf8') const { content } = matter(fileContents) - const matches = (content.match(liquidRefsWithLinkBreaksRegex) || []) - const errorMessage = formatRefError('Found unexpected line breaks in Liquid reference:', matches) + const matches = content.match(liquidRefsWithLinkBreaksRegex) || [] + const errorMessage = formatRefError( + 'Found unexpected line breaks in Liquid reference:', + matches + ) expect(matches.length, errorMessage).toBe(0) } ) @@ -67,36 +71,36 @@ describe('Liquid references', () => { const yamlWalkOptions = { globs: ['**/*.yml'], directories: false, - includeBasePath: true + includeBasePath: true, } const variableYamlAbsPaths = walk(variablesDir, yamlWalkOptions).sort() - const variableYamlRelPaths = variableYamlAbsPaths.map(p => path.relative(rootDir, p)) + const variableYamlRelPaths = variableYamlAbsPaths.map((p) => path.relative(rootDir, p)) const variableYamlTuples = zip(variableYamlRelPaths, variableYamlAbsPaths) - test.each(variableYamlTuples)( - 'in "%s"', - async (yamlRelPath, yamlAbsPath) => { - const fileContents = await readFileAsync(yamlAbsPath, 'utf8') - const dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - - const matches = [] - - for (const [key, content] of Object.entries(dictionary)) { - if (typeof content !== 'string') continue - const valMatches = (content.match(liquidRefsWithLinkBreaksRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } - } + test.each(variableYamlTuples)('in "%s"', async (yamlRelPath, yamlAbsPath) => { + const fileContents = await readFileAsync(yamlAbsPath, 'utf8') + const dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - const errorMessage = formatRefError('Found unexpected line breaks in Liquid reference:', matches) - expect(matches.length, errorMessage).toBe(0) + const matches = [] + + for (const [key, content] of Object.entries(dictionary)) { + if (typeof content !== 'string') continue + const valMatches = content.match(liquidRefsWithLinkBreaksRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) + } } - ) + + const errorMessage = formatRefError( + 'Found unexpected line breaks in Liquid reference:', + matches + ) + expect(matches.length, errorMessage).toBe(0) + }) }) }) -function formatRefError (message, breaks) { +function formatRefError(message, breaks) { return `${message}\n - ${breaks.join('\n - ')}` } diff --git a/tests/content/remove-liquid-statements.js b/tests/content/remove-liquid-statements.js index 6f796e452fb0..773470084cfa 100644 --- a/tests/content/remove-liquid-statements.js +++ b/tests/content/remove-liquid-statements.js @@ -30,7 +30,7 @@ const frontmatter1 = path.join(removeLiquidStatementsFixtures, 'frontmatter1.md' const frontmatter2 = path.join(removeLiquidStatementsFixtures, 'frontmatter2.md') // process frontmatter -function processFrontmatter (contents, file) { +function processFrontmatter(contents, file) { const { content, data } = matter(contents) removeDeprecatedFrontmatter(file, data.versions, versionToDeprecate, nextOldestVersion) return matter.stringify(content, data, { lineWidth: 10000 }) @@ -44,13 +44,17 @@ describe('removing liquid statements only', () => { expect($('.example1').text().trim()).toBe('Alpha') expect($('.example2').text().trim()).toBe('Alpha') expect($('.example3').text().trim()).toBe('Alpha') - expect($('.example4').text().trim()).toBe(`{% if currentVersion ver_gt "enterprise-server@2.16" %}\n + expect($('.example4').text().trim()) + .toBe(`{% if currentVersion ver_gt "enterprise-server@2.16" %}\n Alpha\n\n{% else %}\n\nBravo\n\nCharlie\n\n{% endif %}`) - expect($('.example5').text().trim()).toBe(`{% if currentVersion ver_lt "enterprise-server@2.16" %}\n + expect($('.example5').text().trim()) + .toBe(`{% if currentVersion ver_lt "enterprise-server@2.16" %}\n Alpha\n\nBravo\n\n{% else %}\n\nCharlie\n\n{% endif %}`) - expect($('.example6').text().trim()).toBe(`Alpha\n\n{% if currentVersion ver_lt "enterprise-server@2.16" %}\n + expect($('.example6').text().trim()) + .toBe(`Alpha\n\n{% if currentVersion ver_lt "enterprise-server@2.16" %}\n Bravo\n\n{% endif %}`) - expect($('.example7').text().trim()).toBe(`Alpha\n\n{% if currentVersion ver_gt "enterprise-server@2.16" %}\n + expect($('.example7').text().trim()) + .toBe(`Alpha\n\n{% if currentVersion ver_gt "enterprise-server@2.16" %}\n Bravo\n\n{% else %}\n\nCharlie\n\n{% endif %}`) expect($('.example8').text().trim()).toBe('Alpha') expect($('.example9').text().trim()).toBe(`{% if currentVersion == "free-pro-team@latest" %}\n @@ -63,11 +67,17 @@ Alpha\n\n{% else %}\n\nBravo\n\n{% if currentVersion ver_gt "enterprise-server@2 let contents = await readFileAsync(andGreaterThan1, 'utf8') contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion) const $ = cheerio.load(contents) - expect($('.example1').text().trim()).toBe('{% if currentVersion != "free-pro-team@latest" %}\n\nAlpha\n\n{% endif %}') - expect($('.example2').text().trim()).toBe('{% if currentVersion != "free-pro-team@latest" %}\n\nAlpha\n\n{% endif %}') - expect($('.example3').text().trim()).toBe(`{% if currentVersion ver_gt "enterprise-server@2.16" %}\n + expect($('.example1').text().trim()).toBe( + '{% if currentVersion != "free-pro-team@latest" %}\n\nAlpha\n\n{% endif %}' + ) + expect($('.example2').text().trim()).toBe( + '{% if currentVersion != "free-pro-team@latest" %}\n\nAlpha\n\n{% endif %}' + ) + expect($('.example3').text().trim()) + .toBe(`{% if currentVersion ver_gt "enterprise-server@2.16" %}\n Alpha\n\n{% else %}\n\nBravo\n\n{% if currentVersion != "free-pro-team@latest" %}\n\nCharlie\n\n{% endif %}\n{% endif %}`) - expect($('.example4').text().trim()).toBe(`{% if currentVersion ver_lt "enterprise-server@2.16" %}\n + expect($('.example4').text().trim()) + .toBe(`{% if currentVersion ver_lt "enterprise-server@2.16" %}\n Alpha\n\n{% if currentVersion != "free-pro-team@latest" %}\n\nBravo\n\n{% endif %}\n\n{% else %}\n\nCharlie\n\n{% endif %}`) expect($('.example5').text().trim()).toBe(`{% if currentVersion != "free-pro-team@latest" %}\n Alpha\n\n{% if currentVersion ver_gt "enterprise-server@2.16" %}\n\nBravo\n\n{% endif %}\n\n{% endif %}`) @@ -77,13 +87,18 @@ Alpha\n\n{% if currentVersion ver_gt "enterprise-server@2.16" %}\n\nBravo\n\n{% let contents = await readFileAsync(andGreaterThan2, 'utf8') contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion) const $ = cheerio.load(contents) - expect($('.example1').text().trim()).toBe('{% if currentVersion ver_lt "enterprise-server@2.16" %}\n\nAlpha\n\n{% endif %}') - expect($('.example2').text().trim()).toBe('{% if currentVersion ver_lt "enterprise-server@2.16" %}\n\nAlpha\n\n{% endif %}') + expect($('.example1').text().trim()).toBe( + '{% if currentVersion ver_lt "enterprise-server@2.16" %}\n\nAlpha\n\n{% endif %}' + ) + expect($('.example2').text().trim()).toBe( + '{% if currentVersion ver_lt "enterprise-server@2.16" %}\n\nAlpha\n\n{% endif %}' + ) expect($('.example3').text().trim()).toBe(`{% if currentVersion == "free-pro-team@latest" %}\n Alpha\n\n{% else %}\n\nBravo\n\n{% if currentVersion ver_lt "enterprise-server@2.16" %}\n\nCharlie\n\n{% endif %}\n{% endif %}`) expect($('.example4').text().trim()).toBe(`{% if currentVersion != "free-pro-team@latest" %}\n Alpha\n\n{% if currentVersion ver_lt "enterprise-server@2.16" %}\n\nBravo\n\n{% endif %}\n\n{% else %}\n\nCharlie\n\n{% endif %}`) - expect($('.example5').text().trim()).toBe(`{% if currentVersion ver_lt "enterprise-server@2.16" %}\n + expect($('.example5').text().trim()) + .toBe(`{% if currentVersion ver_lt "enterprise-server@2.16" %}\n Alpha\n\n{% if currentVersion != "free-pro-team@latest" %}\n\nBravo\n\n{% endif %}\n\n{% endif %}`) }) @@ -92,12 +107,15 @@ Alpha\n\n{% if currentVersion != "free-pro-team@latest" %}\n\nBravo\n\n{% endif contents = removeLiquidStatements(contents, versionToDeprecate, nextOldestVersion) const $ = cheerio.load(contents) expect($('.example1').text().trim()).toBe('Alpha') - expect($('.example2').text().trim()).toBe('{% if currentVersion == "free-pro-team@latest" %}\n\nAlpha\n\n{% endif %}') + expect($('.example2').text().trim()).toBe( + '{% if currentVersion == "free-pro-team@latest" %}\n\nAlpha\n\n{% endif %}' + ) expect($('.example3').text().trim()).toBe(`{% if currentVersion == "free-pro-team@latest" %}\n Alpha\n\n{% else %}\n\nBravo\n\nCharlie\n\n{% endif %}`) expect($('.example4').text().trim()).toBe(`{% if currentVersion == "free-pro-team@latest" %}\n Alpha\n\nBravo\n\n{% else %}\n\nCharlie\n\n{% endif %}`) - expect($('.example5').text().trim()).toBe(`Alpha\n\n{% if currentVersion == "free-pro-team@latest" %}\n + expect($('.example5').text().trim()) + .toBe(`Alpha\n\n{% if currentVersion == "free-pro-team@latest" %}\n Bravo\n\n{% endif %}`) expect($('.example6').text().trim()).toBe(`{% if currentVersion != "free-pro-team@latest" %}\n Alpha\n\n{% endif %}`) @@ -130,10 +148,12 @@ Alpha\n\n{% else %}\n\nBravo\n\n{% endif %}`) expect($('.example4').text().trim()).toBe(`{% if currentVersion == "free-pro-team@latest" %}\n Alpha\n\n{% else %}\n\nCharlie\n\n{% endif %}`) expect($('.example5').text().trim()).toBe('Charlie') - expect($('.example6').text().trim()).toBe(`{% if currentVersion ver_lt "enterprise-server@2.16" %}\n + expect($('.example6').text().trim()) + .toBe(`{% if currentVersion ver_lt "enterprise-server@2.16" %}\n Alpha\n\n{% else %}\n\nCharlie\n\n{% endif %}`) expect($('.example7').text().trim()).toBe('') - expect($('.example8').text().trim()).toBe(`Bravo\n\n{% if currentVersion ver_gt "enterprise-server@2.16" %}\n + expect($('.example8').text().trim()) + .toBe(`Bravo\n\n{% if currentVersion ver_gt "enterprise-server@2.16" %}\n Charlie\n\n{% else %}\n\nDelta\n\n{% endif %}\n\nEcho`) }) }) @@ -145,16 +165,16 @@ describe('updating frontmatter', () => { const $ = cheerio.load(contents) // console.log('foo') // console.log($.text()) - expect($.text().includes('enterprise-server: \'*\'')).toBe(true) - expect($.text().includes('enterprise-server: \'>=2.13\'')).toBe(false) + expect($.text().includes("enterprise-server: '*'")).toBe(true) + expect($.text().includes("enterprise-server: '>=2.13'")).toBe(false) }) test('updates frontmatter versions Enterprise if set to greater-than-or-equal-to next oldest version', async () => { let contents = await readFileAsync(frontmatter2, 'utf8') contents = processFrontmatter(contents, frontmatter2) const $ = cheerio.load(contents) - expect($.text().includes('enterprise-server: \'*\'')).toBe(true) - expect($.text().includes('enterprise-server: \'>=2.14\'')).toBe(false) + expect($.text().includes("enterprise-server: '*'")).toBe(true) + expect($.text().includes("enterprise-server: '>=2.14'")).toBe(false) }) }) diff --git a/tests/content/site-data-references.js b/tests/content/site-data-references.js index 667541df3c34..7556e1a05653 100644 --- a/tests/content/site-data-references.js +++ b/tests/content/site-data-references.js @@ -20,17 +20,17 @@ describe('data references', () => { beforeAll(async () => { data = await loadSiteData() pages = await loadPages() - pages = pages.filter(page => page.languageCode === 'en') + pages = pages.filter((page) => page.languageCode === 'en') }) test('every data reference found in English content files is defined and has a value', () => { let errors = [] expect(pages.length).toBeGreaterThan(0) - pages.forEach(page => { + pages.forEach((page) => { const file = path.join('content', page.relativePath) const pageRefs = getDataReferences(page.markdown) - pageRefs.forEach(key => { + pageRefs.forEach((key) => { const value = get(data.en, key) if (typeof value !== 'string') errors.push({ key, value, file }) }) @@ -44,16 +44,18 @@ describe('data references', () => { let errors = [] expect(pages.length).toBeGreaterThan(0) - await Promise.all(pages.map(async page => { - const metadataFile = path.join('content', page.relativePath) - const fileContents = await readFileAsync(path.join(__dirname, '../..', metadataFile)) - const { data: metadata } = frontmatter(fileContents, { filepath: page.fullPath }) - const metadataRefs = getDataReferences(JSON.stringify(metadata)) - metadataRefs.forEach(key => { - const value = get(data.en, key) - if (typeof value !== 'string') errors.push({ key, value, metadataFile }) + await Promise.all( + pages.map(async (page) => { + const metadataFile = path.join('content', page.relativePath) + const fileContents = await readFileAsync(path.join(__dirname, '../..', metadataFile)) + const { data: metadata } = frontmatter(fileContents, { filepath: page.fullPath }) + const metadataRefs = getDataReferences(JSON.stringify(metadata)) + metadataRefs.forEach((key) => { + const value = get(data.en, key) + if (typeof value !== 'string') errors.push({ key, value, metadataFile }) + }) }) - })) + ) errors = uniqWith(errors, isEqual) // remove duplicates expect(errors.length, JSON.stringify(errors, null, 2)).toBe(0) @@ -65,17 +67,23 @@ describe('data references', () => { const reusables = Object.values(allReusables) expect(reusables.length).toBeGreaterThan(0) - await Promise.all(reusables.map(async reusablesPerFile => { - let reusableFile = path.join(__dirname, '../../data/reusables/', getFilenameByValue(allReusables, reusablesPerFile)) - reusableFile = await getFilepath(reusableFile) - - const reusableRefs = getDataReferences(JSON.stringify(reusablesPerFile)) - - reusableRefs.forEach(key => { - const value = get(data.en, key) - if (typeof value !== 'string') errors.push({ key, value, reusableFile }) + await Promise.all( + reusables.map(async (reusablesPerFile) => { + let reusableFile = path.join( + __dirname, + '../../data/reusables/', + getFilenameByValue(allReusables, reusablesPerFile) + ) + reusableFile = await getFilepath(reusableFile) + + const reusableRefs = getDataReferences(JSON.stringify(reusablesPerFile)) + + reusableRefs.forEach((key) => { + const value = get(data.en, key) + if (typeof value !== 'string') errors.push({ key, value, reusableFile }) + }) }) - })) + ) errors = uniqWith(errors, isEqual) // remove duplicates expect(errors.length, JSON.stringify(errors, null, 2)).toBe(0) @@ -87,29 +95,35 @@ describe('data references', () => { const variables = Object.values(allVariables) expect(variables.length).toBeGreaterThan(0) - await Promise.all(variables.map(async variablesPerFile => { - let variableFile = path.join(__dirname, '../../data/variables/', getFilenameByValue(allVariables, variablesPerFile)) - variableFile = await getFilepath(variableFile) - - const variableRefs = getDataReferences(JSON.stringify(variablesPerFile)) - - variableRefs.forEach(key => { - const value = get(data.en, key) - if (typeof value !== 'string') errors.push({ key, value, variableFile }) + await Promise.all( + variables.map(async (variablesPerFile) => { + let variableFile = path.join( + __dirname, + '../../data/variables/', + getFilenameByValue(allVariables, variablesPerFile) + ) + variableFile = await getFilepath(variableFile) + + const variableRefs = getDataReferences(JSON.stringify(variablesPerFile)) + + variableRefs.forEach((key) => { + const value = get(data.en, key) + if (typeof value !== 'string') errors.push({ key, value, variableFile }) + }) }) - })) + ) errors = uniqWith(errors, isEqual) // remove duplicates expect(errors.length, JSON.stringify(errors, null, 2)).toBe(0) }) }) -function getFilenameByValue (object, value) { - return Object.keys(object).find(key => object[key] === value) +function getFilenameByValue(object, value) { + return Object.keys(object).find((key) => object[key] === value) } // if path exists, assume it's a directory; otherwise, assume a YML extension -async function getFilepath (filepath) { +async function getFilepath(filepath) { try { await fs.stat(filepath) filepath = filepath + '/' diff --git a/tests/content/site-data.js b/tests/content/site-data.js index 218f8cabe0ff..2dee3694a6a3 100644 --- a/tests/content/site-data.js +++ b/tests/content/site-data.js @@ -31,7 +31,10 @@ describe('siteData module (English)', () => { }) test('includes English reusables', async () => { - const reusable = get(data, 'en.site.data.reusables.command_line.switching_directories_procedural') + const reusable = get( + data, + 'en.site.data.reusables.command_line.switching_directories_procedural' + ) expect(reusable).toBe('1. Change the current working directory to your local repository.') }) @@ -73,7 +76,10 @@ describe('siteData module (English)', () => { }) test('includes markdown files as data', async () => { - const reusable = get(data, 'en.site.data.reusables.enterprise_enterprise_support.submit-support-ticket-first-section') + const reusable = get( + data, + 'en.site.data.reusables.enterprise_enterprise_support.submit-support-ticket-first-section' + ) expect(typeof reusable).toBe('string') expect(reusable.includes('1. ')).toBe(true) }) @@ -87,8 +93,12 @@ describe('siteData module (English)', () => { test('warn if any YAML reusables are found', async () => { const reusables = walkSync(path.join(__dirname, '../../data/reusables')) expect(reusables.length).toBeGreaterThan(100) - const yamlReusables = reusables.filter(filename => filename.endsWith('.yml') || filename.endsWith('.yaml')) - const message = `reusables are now written as individual Markdown files. Please migrate the following YAML files to Markdown:\n${yamlReusables.join('\n')}` + const yamlReusables = reusables.filter( + (filename) => filename.endsWith('.yml') || filename.endsWith('.yaml') + ) + const message = `reusables are now written as individual Markdown files. Please migrate the following YAML files to Markdown:\n${yamlReusables.join( + '\n' + )}` expect(yamlReusables.length, message).toBe(0) }) diff --git a/tests/content/site-tree.js b/tests/content/site-tree.js index 4cee6f366a11..b4bb44e4dbdc 100644 --- a/tests/content/site-tree.js +++ b/tests/content/site-tree.js @@ -23,16 +23,20 @@ describe('siteTree', () => { test('object order and structure', () => { expect(siteTree.en[nonEnterpriseDefaultVersion].childPages[0].href).toBe('/en/get-started') - expect(siteTree.en[nonEnterpriseDefaultVersion].childPages[0].childPages[0].href).toBe('/en/get-started/quickstart') + expect(siteTree.en[nonEnterpriseDefaultVersion].childPages[0].childPages[0].href).toBe( + '/en/get-started/quickstart' + ) }) describe('localized titles', () => { test('titles for categories', () => { - const japaneseTitle = siteTree.ja[nonEnterpriseDefaultVersion].childPages[0].childPages[0].page.title + const japaneseTitle = + siteTree.ja[nonEnterpriseDefaultVersion].childPages[0].childPages[0].page.title expect(typeof japaneseTitle).toBe('string') expect(japaneseCharacters.presentIn(japaneseTitle)).toBe(true) - const englishTitle = siteTree.en[nonEnterpriseDefaultVersion].childPages[0].childPages[0].page.title + const englishTitle = + siteTree.en[nonEnterpriseDefaultVersion].childPages[0].childPages[0].page.title expect(typeof englishTitle).toBe('string') expect(japaneseCharacters.presentIn(englishTitle)).toBe(false) }) @@ -43,12 +47,14 @@ describe('siteTree', () => { // Find a page in the tree that we know contains Liquid // TODO: use new findPageInSiteTree helper when it's available - const pageWithDynamicTitle = ghesSiteTree - .childPages.find(child => child.href === `/en/${ghesLatest}/admin`) - .childPages.find(child => child.href === `/en/${ghesLatest}/admin/enterprise-support`) + const pageWithDynamicTitle = ghesSiteTree.childPages + .find((child) => child.href === `/en/${ghesLatest}/admin`) + .childPages.find((child) => child.href === `/en/${ghesLatest}/admin/enterprise-support`) // Confirm the raw title contains Liquid - expect(pageWithDynamicTitle.page.title).toEqual('Working with {% data variables.contact.github_support %}') + expect(pageWithDynamicTitle.page.title).toEqual( + 'Working with {% data variables.contact.github_support %}' + ) // Confirm a new property contains the rendered title expect(pageWithDynamicTitle.renderedFullTitle).toEqual('Working with GitHub Support') @@ -63,8 +69,8 @@ describe('siteTree', () => { }) }) -function validate (currentPage) { - (currentPage.childPages || []).forEach(childPage => { +function validate(currentPage) { + ;(currentPage.childPages || []).forEach((childPage) => { const { valid, errors } = revalidator.validate(childPage, schema.childPage) const expectation = JSON.stringify(errors, null, 2) expect(valid, expectation).toBe(true) diff --git a/tests/content/webhooks.js b/tests/content/webhooks.js index 70eb2df32058..ae806acda205 100644 --- a/tests/content/webhooks.js +++ b/tests/content/webhooks.js @@ -6,32 +6,32 @@ import webhookPayloads from '../../lib/webhooks' import { jest } from '@jest/globals' const allVersions = Object.values(xAllVersions) -const payloadVersions = allVersions.map(v => v.miscVersionName) +const payloadVersions = allVersions.map((v) => v.miscVersionName) // grab some values for testing -const nonEnterpriseDefaultPayloadVersion = allVersions - .find(version => version.nonEnterpriseDefault) - .miscVersionName +const nonEnterpriseDefaultPayloadVersion = allVersions.find( + (version) => version.nonEnterpriseDefault +).miscVersionName -const latestGhesPayloadVersion = allVersions - .find(version => version.currentRelease === latest) - .miscVersionName +const latestGhesPayloadVersion = allVersions.find( + (version) => version.currentRelease === latest +).miscVersionName -const ghaePayloadVersion = allVersions - .find(version => version.plan === 'github-ae') - .miscVersionName +const ghaePayloadVersion = allVersions.find( + (version) => version.plan === 'github-ae' +).miscVersionName describe('webhook payloads', () => { jest.setTimeout(3 * 60 * 1000) test('have expected top-level keys', () => { - payloadVersions.forEach(version => { + payloadVersions.forEach((version) => { expect(version in webhookPayloads).toBe(true) }) }) test('have a reasonable number of payloads per version', () => { - payloadVersions.forEach(version => { + payloadVersions.forEach((version) => { const payloadsPerVersion = Object.keys(webhookPayloads[version]) expect(payloadsPerVersion.length).toBeGreaterThan(20) }) @@ -53,15 +53,22 @@ describe('webhook payloads', () => { test('on non-dotcom versions, dotcom-only payloads fall back to dotcom', async () => { const ghesPayloads = webhookPayloads[latestGhesPayloadVersion] const ghaePayloads = webhookPayloads[ghaePayloadVersion] - const dotcomOnlyPayloads = difference(Object.keys(webhookPayloads[nonEnterpriseDefaultPayloadVersion]), Object.keys(ghesPayloads)) + const dotcomOnlyPayloads = difference( + Object.keys(webhookPayloads[nonEnterpriseDefaultPayloadVersion]), + Object.keys(ghesPayloads) + ) // use the first one found for testing purposes const dotcomOnlyPayload = dotcomOnlyPayloads[0] expect(ghesPayloads[dotcomOnlyPayload]).toBeUndefined() expect(ghaePayloads[dotcomOnlyPayload]).toBeUndefined() // fallback handling is in middleware/contextualizers/webhooks.js - const ghesPayloadsWithFallbacks = await getJSON(`/en/enterprise-server@${latest}/developers/webhooks-and-events?json=webhookPayloadsForCurrentVersion`) - const ghaePayloadsWithFallbacks = await getJSON('/en/github-ae@latest/developers/webhooks-and-events?json=webhookPayloadsForCurrentVersion') + const ghesPayloadsWithFallbacks = await getJSON( + `/en/enterprise-server@${latest}/developers/webhooks-and-events?json=webhookPayloadsForCurrentVersion` + ) + const ghaePayloadsWithFallbacks = await getJSON( + '/en/github-ae@latest/developers/webhooks-and-events?json=webhookPayloadsForCurrentVersion' + ) expect(ghesPayloadsWithFallbacks[dotcomOnlyPayload]).toBeDefined() expect(ghaePayloadsWithFallbacks[dotcomOnlyPayload]).toBeDefined() @@ -75,8 +82,6 @@ describe('webhook payloads', () => { // accommodate two possible payload string locations // value of top-level key: `create` (in create.payload.json) // value of second-level key: `issues.opened` (in issues.opened.payload.json) -function getPayloadString (payload) { - return typeof payload === 'string' - ? payload - : payload[Object.keys(payload)[0]] +function getPayloadString(payload) { + return typeof payload === 'string' ? payload : payload[Object.keys(payload)[0]] } diff --git a/tests/graphql/build-changelog-test.js b/tests/graphql/build-changelog-test.js index ec5c698edeec..2f05ca38976f 100644 --- a/tests/graphql/build-changelog-test.js +++ b/tests/graphql/build-changelog-test.js @@ -1,5 +1,10 @@ import yaml from 'js-yaml' -import { createChangelogEntry, cleanPreviewTitle, previewAnchor, prependDatedEntry } from '../../script/graphql/build-changelog.js' +import { + createChangelogEntry, + cleanPreviewTitle, + previewAnchor, + prependDatedEntry, +} from '../../script/graphql/build-changelog.js' import xFs from 'fs' import MockDate from 'mockdate' import readFileAsync from '../../lib/readfile-async.js' @@ -77,7 +82,13 @@ upcoming_changes: date: '2021-01-01T00:00:00+00:00' `).upcoming_changes - const entry = await createChangelogEntry(oldSchemaString, newSchemaString, previews, oldUpcomingChanges, newUpcomingChanges) + const entry = await createChangelogEntry( + oldSchemaString, + newSchemaString, + previews, + oldUpcomingChanges, + newUpcomingChanges + ) expect(entry).toEqual(expectedChangelogEntry) }) diff --git a/tests/helpers/conditional-runs.js b/tests/helpers/conditional-runs.js index d8b5c3115e36..79d9d9d056d2 100644 --- a/tests/helpers/conditional-runs.js +++ b/tests/helpers/conditional-runs.js @@ -1,9 +1,10 @@ -const runningActionsOnInternalRepo = process.env.GITHUB_ACTIONS === 'true' && process.env.GITHUB_REPOSITORY === 'github/docs-internal' +const runningActionsOnInternalRepo = + process.env.GITHUB_ACTIONS === 'true' && process.env.GITHUB_REPOSITORY === 'github/docs-internal' export const testViaActionsOnly = runningActionsOnInternalRepo ? test : test.skip export const describeViaActionsOnly = runningActionsOnInternalRepo ? describe : describe.skip export default { testViaActionsOnly, - describeViaActionsOnly + describeViaActionsOnly, } diff --git a/tests/helpers/lint-translation-reporter.js b/tests/helpers/lint-translation-reporter.js index 9c4998c95631..dae6fc02b849 100644 --- a/tests/helpers/lint-translation-reporter.js +++ b/tests/helpers/lint-translation-reporter.js @@ -3,25 +3,28 @@ import stripAnsi from 'strip-ansi' import { groupBy } from 'lodash-es' // we don't want to print all the stack traces -const stackTraceRegExp = /^\s+at\s.+/img +const stackTraceRegExp = /^\s+at\s.+/gim class TranslationReporter { - constructor (globalConfig, options) { + constructor(globalConfig, options) { this._globalConfig = globalConfig this._options = options } - onRunComplete (contexts, results) { + onRunComplete(contexts, results) { const failures = results.testResults.reduce((fails, { testResults: assertionResults }) => { const formattedFails = assertionResults - .filter(result => result.status === 'failed') + .filter((result) => result.status === 'failed') .map(({ ancestorTitles, failureMessages, title }) => { return { fileName: ancestorTitles[1], failedTests: title, failureMessage: failureMessages.map((message) => { - return message.split('\n').filter(line => !stackTraceRegExp.test(stripAnsi(line))).join('\n') - }) + return message + .split('\n') + .filter((line) => !stackTraceRegExp.test(stripAnsi(line))) + .join('\n') + }), } }) return [...fails, ...formattedFails] @@ -33,7 +36,7 @@ class TranslationReporter { console.group(chalk.red.bold(`\n${fileName}`)) failuresByFile[fileName].forEach(({ failureMessage }, index) => { console.log(chalk.bold(`\n(${index + 1})`)) - failureMessage.forEach(msg => console.log(msg)) + failureMessage.forEach((msg) => console.log(msg)) }) console.groupEnd() } diff --git a/tests/helpers/schemas/feature-versions-schema.js b/tests/helpers/schemas/feature-versions-schema.js index 80d51562db55..fe060490c5d0 100644 --- a/tests/helpers/schemas/feature-versions-schema.js +++ b/tests/helpers/schemas/feature-versions-schema.js @@ -3,8 +3,8 @@ import { schema } from '../../../lib/frontmatter.js' // Copy the properties from the frontmatter schema. const featureVersions = { properties: { - versions: Object.assign({}, schema.properties.versions) - } + versions: Object.assign({}, schema.properties.versions), + }, } // Remove the feature versions properties. diff --git a/tests/helpers/schemas/ghae-release-notes-schema.js b/tests/helpers/schemas/ghae-release-notes-schema.js index 52ade09360d9..dc2c5c853d99 100644 --- a/tests/helpers/schemas/ghae-release-notes-schema.js +++ b/tests/helpers/schemas/ghae-release-notes-schema.js @@ -3,55 +3,55 @@ const section = { { type: 'array', items: { type: 'string' }, - minItems: 1 + minItems: 1, }, { type: 'object', properties: { heading: { type: 'string', - required: true + required: true, }, notes: { type: 'array', items: { type: 'string' }, required: true, - minItems: 1 - } - } - } - ] + minItems: 1, + }, + }, + }, + ], } export default { properties: { intro: { - type: 'string' + type: 'string', }, date: { type: 'string', format: 'date', - required: true + required: true, }, friendlyDate: { type: 'string', - required: true + required: true, }, title: { type: 'string', - required: true + required: true, }, currentWeek: { type: 'boolean', - required: true + required: true, }, release_candidate: { type: 'boolean', - default: false + default: false, }, deprecated: { type: 'boolean', - default: false + default: false, }, sections: { required: true, @@ -64,8 +64,8 @@ export default { 'changes', 'deprecations', 'security_fixes', - 'backups' - ].reduce((prev, curr) => ({ ...prev, [curr]: section }), {}) - } - } + 'backups', + ].reduce((prev, curr) => ({ ...prev, [curr]: section }), {}), + }, + }, } diff --git a/tests/helpers/schemas/ghes-release-notes-schema.js b/tests/helpers/schemas/ghes-release-notes-schema.js index 6169370921fe..c43bc62ee9e9 100644 --- a/tests/helpers/schemas/ghes-release-notes-schema.js +++ b/tests/helpers/schemas/ghes-release-notes-schema.js @@ -3,43 +3,43 @@ const section = { { type: 'array', items: { type: 'string' }, - minItems: 1 + minItems: 1, }, { type: 'object', properties: { heading: { type: 'string', - required: true + required: true, }, notes: { type: 'array', items: { type: 'string' }, required: true, - minItems: 1 - } - } - } - ] + minItems: 1, + }, + }, + }, + ], } export default { properties: { intro: { - type: 'string' + type: 'string', }, date: { type: 'string', format: 'date', - required: true + required: true, }, release_candidate: { type: 'boolean', - default: false + default: false, }, deprecated: { type: 'boolean', - default: false + default: false, }, sections: { required: true, @@ -52,8 +52,8 @@ export default { 'changes', 'deprecations', 'security_fixes', - 'backups' - ].reduce((prev, curr) => ({ ...prev, [curr]: section }), {}) - } - } + 'backups', + ].reduce((prev, curr) => ({ ...prev, [curr]: section }), {}), + }, + }, } diff --git a/tests/helpers/schemas/languages-schema.js b/tests/helpers/schemas/languages-schema.js index 60a375a1b6bc..73099ceedc03 100644 --- a/tests/helpers/schemas/languages-schema.js +++ b/tests/helpers/schemas/languages-schema.js @@ -3,12 +3,12 @@ export default { name: { required: true, description: 'the English name', - type: 'string' + type: 'string', }, nativeName: { description: 'the native name', - type: 'string' + type: 'string', }, code: { @@ -16,13 +16,13 @@ export default { description: 'the code used in the URL', type: 'string', minLength: 2, - maxLength: 2 + maxLength: 2, }, dir: { required: true, description: 'the local relative path to files in this language', - type: 'string' + type: 'string', }, // https://support.google.com/webmasters/answer/189077 @@ -32,17 +32,17 @@ export default { required: true, description: 'the ISO 639-1, ISO 3166-1 Alpha 2, or ISO 15924 language code', type: 'string', - minLength: 2 + minLength: 2, }, redirectPatterns: { description: 'array of regular expressions used for redirecting incorrect URLs', - type: 'array' + type: 'array', }, wip: { description: 'boolean indicating whether translations are incomplete', - type: 'boolean' - } - } + type: 'boolean', + }, + }, } diff --git a/tests/helpers/schemas/learning-tracks-schema.js b/tests/helpers/schemas/learning-tracks-schema.js index 9545c5065f00..c1579acfbc46 100644 --- a/tests/helpers/schemas/learning-tracks-schema.js +++ b/tests/helpers/schemas/learning-tracks-schema.js @@ -8,22 +8,22 @@ export default { properties: { title: { type: 'string', - required: true + required: true, }, description: { type: 'string', - required: true + required: true, }, guides: { type: 'array', items: { type: 'string' }, - required: true + required: true, }, featured_track: { - type: 'boolean' - } - } - } - } - } + type: 'boolean', + }, + }, + }, + }, + }, } diff --git a/tests/helpers/schemas/products-schema.js b/tests/helpers/schemas/products-schema.js index 1e7491488a54..9726949bff44 100644 --- a/tests/helpers/schemas/products-schema.js +++ b/tests/helpers/schemas/products-schema.js @@ -3,47 +3,47 @@ export default { name: { required: true, description: 'the product name', - type: 'string' + type: 'string', }, id: { required: true, description: 'an identifier for the product', - type: 'string' + type: 'string', }, href: { required: true, description: 'the href to the product landing page', type: 'string', - pattern: '^(/|http)' // if internal, must start with a slash; if external, must start with http + pattern: '^(/|http)', // if internal, must start with a slash; if external, must start with http }, dir: { description: 'the local relative path to the product directory', type: 'string', - pattern: '^content/.*?[^/]$' // must start with content, can't end with a slash + pattern: '^content/.*?[^/]$', // must start with content, can't end with a slash }, toc: { description: 'the local relative path to the product toc page', type: 'string', - pattern: '^content/.*?index.md$' // must start with content and end with index.md + pattern: '^content/.*?index.md$', // must start with content and end with index.md }, hasEnterpriseUserVersions: { description: 'boolean indicating whether the product has Enterprise User permalinks', - type: 'boolean' + type: 'boolean', }, external: { description: 'boolean indicating whether the product has external docs', - type: 'boolean' + type: 'boolean', }, wip: { description: 'boolean indicating whether the product should display in production', - type: 'boolean' - } - } + type: 'boolean', + }, + }, } diff --git a/tests/helpers/schemas/site-tree-schema.js b/tests/helpers/schemas/site-tree-schema.js index aba1da7a5c48..2656fa1f26e4 100644 --- a/tests/helpers/schemas/site-tree-schema.js +++ b/tests/helpers/schemas/site-tree-schema.js @@ -3,7 +3,7 @@ const childPage = { properties: { href: { type: 'string', - required: true + required: true, }, page: { type: 'object', @@ -11,20 +11,20 @@ const childPage = { properties: { title: { type: 'string', - required: true + required: true, }, relativePath: { type: 'string', - required: true + required: true, }, permalinks: { type: 'array', required: true, - minItems: 1 - } - } - } - } + minItems: 1, + }, + }, + }, + }, } export default { childPage } diff --git a/tests/helpers/schemas/versions-schema.js b/tests/helpers/schemas/versions-schema.js index 864f2ef5401d..ba74a2a3f06a 100644 --- a/tests/helpers/schemas/versions-schema.js +++ b/tests/helpers/schemas/versions-schema.js @@ -12,78 +12,79 @@ export default { required: true, description: 'the version string', type: 'string', - pattern: versionPattern + pattern: versionPattern, }, versionTitle: { required: true, description: 'the version title', - type: 'string' + type: 'string', }, latestVersion: { required: true, description: 'the version name that includes the latest release', type: 'string', - pattern: versionPattern + pattern: versionPattern, }, currentRelease: { required: true, description: 'the release substring in the version string', type: 'string', - pattern: releasePattern + pattern: releasePattern, }, plan: { description: 'the plan substring in the version string', type: 'string', - pattern: planPattern + pattern: planPattern, }, planTitle: { required: true, description: 'the plan title', // this is the same as the version title, sans numbered release - type: 'string' + type: 'string', }, shortName: { required: true, description: 'the short name for the version to be used in Liquid conditionals', - type: 'string' + type: 'string', }, releases: { required: true, description: 'an array of all supported releases for the version', - type: 'array' + type: 'array', }, latestRelease: { required: true, description: 'the value of the latest release', type: 'string', - pattern: releasePattern + pattern: releasePattern, }, hasNumberedReleases: { - description: 'boolean indicating whether the plan has numbered releases; if not, the release defalts to "latest"', - type: 'boolean' + description: + 'boolean indicating whether the plan has numbered releases; if not, the release defalts to "latest"', + type: 'boolean', }, nonEnterpriseDefault: { description: 'boolean indicating whether the plan is the default non-Enterprise version', // helper if the plan name changes - type: 'boolean' + type: 'boolean', }, openApiBaseName: { required: true, description: 'base name used to map an openAPI schema name to the current version', - type: 'string' + type: 'string', }, openApiVersionName: { required: true, description: 'final name used to map an openAPI schema name to the current version', - type: 'string' + type: 'string', }, miscBaseName: { required: true, description: 'base name used to map GraphQL and webhook schema names to the current version', - type: 'string' + type: 'string', }, miscVersionName: { required: true, description: 'final name used to map GraphQL and webhook schema names to the current version', - type: 'string' - } - } + type: 'string', + }, + }, } diff --git a/tests/helpers/supertest.js b/tests/helpers/supertest.js index a9e44784d8ba..06e8de2b16e8 100644 --- a/tests/helpers/supertest.js +++ b/tests/helpers/supertest.js @@ -10,8 +10,13 @@ const helpers = {} const request = (method, route) => supertest(app)[method](route) -export const get = helpers.get = async function (route, opts = { followRedirects: false, followAllRedirects: false, headers: {} }) { - let res = (opts.headers) ? await request('get', route).set(opts.headers) : await request('get', route) +export const get = (helpers.get = async function ( + route, + opts = { followRedirects: false, followAllRedirects: false, headers: {} } +) { + let res = opts.headers + ? await request('get', route).set(opts.headers) + : await request('get', route) // follow all redirects, or just follow one if (opts.followAllRedirects && [301, 302].includes(res.status)) { res = await helpers.get(res.headers.location, opts) @@ -20,27 +25,27 @@ export const get = helpers.get = async function (route, opts = { followRedirects } return res -} +}) -export const head = helpers.head = async function (route, opts = { followRedirects: false }) { +export const head = (helpers.head = async function (route, opts = { followRedirects: false }) { const res = await request('head', route).redirects(opts.followRedirects ? 10 : 0) return res -} +}) -export const post = helpers.post = route => request('post', route) +export const post = (helpers.post = (route) => request('post', route)) -export const getDOM = helpers.getDOM = async function (route, headers) { +export const getDOM = (helpers.getDOM = async function (route, headers) { const res = await helpers.get(route, { followRedirects: true, headers }) - const $ = cheerio.load((res.text || ''), { xmlMode: true }) + const $ = cheerio.load(res.text || '', { xmlMode: true }) $.res = Object.assign({}, res) return $ -} +}) // For use with the ?json query param // e.g. await getJSON('/en?json=breadcrumbs') -export const getJSON = helpers.getJSON = async function (route) { +export const getJSON = (helpers.getJSON = async function (route) { const res = await helpers.get(route, { followRedirects: true }) return JSON.parse(res.text) -} +}) export default helpers diff --git a/tests/javascripts/user-agent.js b/tests/javascripts/user-agent.js index 6a4dbf160273..f9cd1e81af60 100644 --- a/tests/javascripts/user-agent.js +++ b/tests/javascripts/user-agent.js @@ -2,35 +2,40 @@ import parseUserAgent from '../../javascripts/user-agent' describe('parseUserAgent', () => { it('android, chrome', () => { - const ua = 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36' + const ua = + 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36' const { os, browser } = parseUserAgent(ua) expect(os).toBe('android') expect(browser).toBe('chrome') }) it('ios, safari', () => { - const ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1' + const ua = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1' const { os, browser } = parseUserAgent(ua) expect(os).toBe('ios') expect(browser).toBe('safari') }) it('windows, edge', () => { - const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246' + const ua = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246' const { os, browser } = parseUserAgent(ua) expect(os).toBe('windows') expect(browser).toBe('edge') }) it('mac, safari', () => { - const ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9' + const ua = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9' const { os, browser } = parseUserAgent(ua) expect(os).toBe('mac') expect(browser).toBe('safari') }) it('windows, chrome', () => { - const ua = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36' + const ua = + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36' const { os, browser } = parseUserAgent(ua) expect(os).toBe('windows') expect(browser).toBe('chrome') diff --git a/tests/linting/lint-files.js b/tests/linting/lint-files.js index 3097c1ba9be1..112b71ea7504 100644 --- a/tests/linting/lint-files.js +++ b/tests/linting/lint-files.js @@ -23,8 +23,10 @@ import { supported, next } from '../../lib/enterprise-server-releases.js' import getLiquidConditionals from '../../script/helpers/get-liquid-conditionals.js' import allowedVersionOperators from '../../lib/liquid-tags/ifversion-supported-operators.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const enterpriseServerVersions = Object.keys(allVersions).filter(v => v.startsWith('enterprise-server@')) -const versionShortNames = Object.values(allVersions).map(v => v.shortName) +const enterpriseServerVersions = Object.keys(allVersions).filter((v) => + v.startsWith('enterprise-server@') +) +const versionShortNames = Object.values(allVersions).map((v) => v.shortName) const versionKeywords = versionShortNames.concat(['currentVersion', 'enterpriseServerReleases']) const rootDir = path.join(__dirname, '../..') @@ -67,7 +69,8 @@ const versionShortNameExceptions = ['ghae-next', 'ghae-issue-'] // - [link text][link-definition-ref] (other text) // - etc. // -const relativeArticleLinkRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?!\/|#|https?:\/\/|tel:|mailto:|\{[%{]\s*)[^)\s]+(?:(?:\s*[%}]\})?\)|\s+|$)/gm +const relativeArticleLinkRegex = + /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?!\/|#|https?:\/\/|tel:|mailto:|\{[%{]\s*)[^)\s]+(?:(?:\s*[%}]\})?\)|\s+|$)/gm // Things matched by this RegExp: // - [link text](/en/github/blah) @@ -79,7 +82,12 @@ const relativeArticleLinkRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(? // - [Node.js](https://nodejs.org/en/) // - etc. // -const languageLinkRegex = new RegExp(`(?=^|[^\\]]\\s*)\\[[^\\]]+\\](?::\\n?[ \\t]+|\\s*\\()(?:(?:https?://(?:help|docs|developer)\\.github\\.com)?/(?:${languageCodes.join('|')})(?:/[^)\\s]*)?)(?:\\)|\\s+|$)`, 'gm') +const languageLinkRegex = new RegExp( + `(?=^|[^\\]]\\s*)\\[[^\\]]+\\](?::\\n?[ \\t]+|\\s*\\()(?:(?:https?://(?:help|docs|developer)\\.github\\.com)?/(?:${languageCodes.join( + '|' + )})(?:/[^)\\s]*)?)(?:\\)|\\s+|$)`, + 'gm' +) // Things matched by this RegExp: // - [link text](/enterprise/2.19/admin/blah) @@ -89,7 +97,8 @@ const languageLinkRegex = new RegExp(`(?=^|[^\\]]\\s*)\\[[^\\]]+\\](?::\\n?[ \\t // Things intentionally NOT matched by this RegExp: // - [link text](https://someservice.com/enterprise/1.0/blah) // - [link text](/github/site-policy/enterprise/2.2/admin/blah) -const versionLinkRegEx = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:https?:\/\/(?:help|docs|developer)\.github\.com)?\/enterprise\/\d+(\.\d+)+(?:\/[^)\s]*)?)(?:\)|\s+|$)/gm +const versionLinkRegEx = + /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:https?:\/\/(?:help|docs|developer)\.github\.com)?\/enterprise\/\d+(\.\d+)+(?:\/[^)\s]*)?)(?:\)|\s+|$)/gm // Things matched by this RegExp: // - [link text](/early-access/github/blah) @@ -101,7 +110,8 @@ const versionLinkRegEx = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:http // - [Node.js](https://nodejs.org/early-access/) // - etc. // -const earlyAccessLinkRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:https?:\/\/(?:help|docs|developer)\.github\.com)?\/early-access(?:\/[^)\s]*)?)(?:\)|\s+|$)/gm +const earlyAccessLinkRegex = + /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:https?:\/\/(?:help|docs|developer)\.github\.com)?\/early-access(?:\/[^)\s]*)?)(?:\)|\s+|$)/gm // - [link text](https://docs.github.com/github/blah) // - [link text] (https://help.github.com/github/blah) @@ -114,7 +124,8 @@ const earlyAccessLinkRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?: // - [link text[(https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/) // - etc. // -const domainLinkRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:https?:)?\/\/(?:help|docs|developer)\.github\.com(?!\/changes\/)[^)\s]*(?:\)|\s+|$)/gm +const domainLinkRegex = + /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:https?:)?\/\/(?:help|docs|developer)\.github\.com(?!\/changes\/)[^)\s]*(?:\)|\s+|$)/gm // Things matched by this RegExp: // - ![image text](/assets/images/early-access/github/blah.gif) @@ -127,7 +138,8 @@ const domainLinkRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:https?:) // - [Node.js](https://nodejs.org/assets/images/early-access/blah.gif) // - etc. // -const earlyAccessImageRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:https?:\/\/(?:help|docs|developer)\.github\.com)?\/assets\/images\/early-access(?:\/[^)\s]*)?)(?:\)|\s+|$)/gm +const earlyAccessImageRegex = + /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:https?:\/\/(?:help|docs|developer)\.github\.com)?\/assets\/images\/early-access(?:\/[^)\s]*)?)(?:\)|\s+|$)/gm // Things matched by this RegExp: // - ![image text](/assets/early-access/images/github/blah.gif) @@ -141,7 +153,8 @@ const earlyAccessImageRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(? // - [Node.js](https://nodejs.org/assets/early-access/images/blah.gif) // - etc. // -const badEarlyAccessImageRegex = /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:https?:\/\/(?:help|docs|developer)\.github\.com)?\/(?:(?:assets|images)\/early-access|early-access\/(?:assets|images))(?:\/[^)\s]*)?)(?:\)|\s+|$)/gm +const badEarlyAccessImageRegex = + /(?=^|[^\]]\s*)\[[^\]]+\](?::\n?[ \t]+|\s*\()(?:(?:https?:\/\/(?:help|docs|developer)\.github\.com)?\/(?:(?:assets|images)\/early-access|early-access\/(?:assets|images))(?:\/[^)\s]*)?)(?:\)|\s+|$)/gm // {{ site.data.example.pizza }} const oldVariableRegex = /{{\s*?site\.data\..*?}}/g @@ -169,17 +182,22 @@ const versionLinkErrorText = 'Found article links with hard-coded version number const domainLinkErrorText = 'Found article links with hard-coded domain names:' const earlyAccessLinkErrorText = 'Found article links leaking Early Access docs:' const earlyAccessImageErrorText = 'Found article images/links leaking Early Access images:' -const badEarlyAccessImageErrorText = 'Found article images/links leaking incorrect Early Access images:' -const oldVariableErrorText = 'Found article uses old {{ site.data... }} syntax. Use {% data example.data.string %} instead!' -const oldOcticonErrorText = 'Found octicon variables with the old {{ octicon-name }} syntax. Use {% octicon "name" %} instead!' -const oldExtendedMarkdownErrorText = 'Found extended markdown tags with the old {{#note}} syntax. Use {% note %}/{% endnote %} instead!' -const stringInLiquidErrorText = 'Found Liquid conditionals that evaluate a string instead of a variable. Remove the quotes around the variable!' +const badEarlyAccessImageErrorText = + 'Found article images/links leaking incorrect Early Access images:' +const oldVariableErrorText = + 'Found article uses old {{ site.data... }} syntax. Use {% data example.data.string %} instead!' +const oldOcticonErrorText = + 'Found octicon variables with the old {{ octicon-name }} syntax. Use {% octicon "name" %} instead!' +const oldExtendedMarkdownErrorText = + 'Found extended markdown tags with the old {{#note}} syntax. Use {% note %}/{% endnote %} instead!' +const stringInLiquidErrorText = + 'Found Liquid conditionals that evaluate a string instead of a variable. Remove the quotes around the variable!' const mdWalkOptions = { globs: ['**/*.md'], ignore: ['**/README.md'], directories: false, - includeBasePath: true + includeBasePath: true, } // Also test the "data/variables/" YAML files @@ -187,70 +205,92 @@ const mdWalkOptions = { const yamlWalkOptions = { globs: ['**/*.yml'], directories: false, - includeBasePath: true + includeBasePath: true, } // different lint rules apply to different content types -let mdToLint, ymlToLint, ghesReleaseNotesToLint, ghaeReleaseNotesToLint, learningTracksToLint, featureVersionsToLint +let mdToLint, + ymlToLint, + ghesReleaseNotesToLint, + ghaeReleaseNotesToLint, + learningTracksToLint, + featureVersionsToLint if (!process.env.TEST_TRANSLATION) { // compile lists of all the files we want to lint const contentMarkdownAbsPaths = walk(contentDir, mdWalkOptions).sort() - const contentMarkdownRelPaths = contentMarkdownAbsPaths.map(p => slash(path.relative(rootDir, p))) + const contentMarkdownRelPaths = contentMarkdownAbsPaths.map((p) => + slash(path.relative(rootDir, p)) + ) const contentMarkdownTuples = zip(contentMarkdownRelPaths, contentMarkdownAbsPaths) const reusableMarkdownAbsPaths = walk(reusablesDir, mdWalkOptions).sort() - const reusableMarkdownRelPaths = reusableMarkdownAbsPaths.map(p => slash(path.relative(rootDir, p))) + const reusableMarkdownRelPaths = reusableMarkdownAbsPaths.map((p) => + slash(path.relative(rootDir, p)) + ) const reusableMarkdownTuples = zip(reusableMarkdownRelPaths, reusableMarkdownAbsPaths) mdToLint = [...contentMarkdownTuples, ...reusableMarkdownTuples] // data/variables const variableYamlAbsPaths = walk(variablesDir, yamlWalkOptions).sort() - const variableYamlRelPaths = variableYamlAbsPaths.map(p => slash(path.relative(rootDir, p))) + const variableYamlRelPaths = variableYamlAbsPaths.map((p) => slash(path.relative(rootDir, p))) const variableYamlTuples = zip(variableYamlRelPaths, variableYamlAbsPaths) // data/glossaries const glossariesYamlAbsPaths = walk(glossariesDir, yamlWalkOptions).sort() - const glossariesYamlRelPaths = glossariesYamlAbsPaths.map(p => slash(path.relative(rootDir, p))) + const glossariesYamlRelPaths = glossariesYamlAbsPaths.map((p) => slash(path.relative(rootDir, p))) const glossariesYamlTuples = zip(glossariesYamlRelPaths, glossariesYamlAbsPaths) ymlToLint = [...variableYamlTuples, ...glossariesYamlTuples] // GHES release notes const ghesReleaseNotesYamlAbsPaths = walk(ghesReleaseNotesDir, yamlWalkOptions).sort() - const ghesReleaseNotesYamlRelPaths = ghesReleaseNotesYamlAbsPaths.map(p => slash(path.relative(rootDir, p))) + const ghesReleaseNotesYamlRelPaths = ghesReleaseNotesYamlAbsPaths.map((p) => + slash(path.relative(rootDir, p)) + ) ghesReleaseNotesToLint = zip(ghesReleaseNotesYamlRelPaths, ghesReleaseNotesYamlAbsPaths) // GHAE release notes const ghaeReleaseNotesYamlAbsPaths = walk(ghaeReleaseNotesDir, yamlWalkOptions).sort() - const ghaeReleaseNotesYamlRelPaths = ghaeReleaseNotesYamlAbsPaths.map(p => slash(path.relative(rootDir, p))) + const ghaeReleaseNotesYamlRelPaths = ghaeReleaseNotesYamlAbsPaths.map((p) => + slash(path.relative(rootDir, p)) + ) ghaeReleaseNotesToLint = zip(ghaeReleaseNotesYamlRelPaths, ghaeReleaseNotesYamlAbsPaths) // Learning tracks const learningTracksYamlAbsPaths = walk(learningTracks, yamlWalkOptions).sort() - const learningTracksYamlRelPaths = learningTracksYamlAbsPaths.map(p => slash(path.relative(rootDir, p))) + const learningTracksYamlRelPaths = learningTracksYamlAbsPaths.map((p) => + slash(path.relative(rootDir, p)) + ) learningTracksToLint = zip(learningTracksYamlRelPaths, learningTracksYamlAbsPaths) // Feature versions const featureVersionsYamlAbsPaths = walk(featureVersionsDir, yamlWalkOptions).sort() - const featureVersionsYamlRelPaths = featureVersionsYamlAbsPaths.map(p => slash(path.relative(rootDir, p))) + const featureVersionsYamlRelPaths = featureVersionsYamlAbsPaths.map((p) => + slash(path.relative(rootDir, p)) + ) featureVersionsToLint = zip(featureVersionsYamlRelPaths, featureVersionsYamlAbsPaths) } else { // get all translated markdown or yaml files by comparing files changed to main branch - const changedFilesRelPaths = execSync('git -c diff.renameLimit=10000 diff --name-only origin/main | egrep "^translations/.*/.+.(yml|md)$"', { maxBuffer: 1024 * 1024 * 100 }).toString().split('\n') + const changedFilesRelPaths = execSync( + 'git -c diff.renameLimit=10000 diff --name-only origin/main | egrep "^translations/.*/.+.(yml|md)$"', + { maxBuffer: 1024 * 1024 * 100 } + ) + .toString() + .split('\n') if (changedFilesRelPaths === '') process.exit(0) console.log('testing translations.') console.log(`Found ${changedFilesRelPaths.length} translated files.`) - const { - mdRelPaths = [], - ymlRelPaths = [], - ghesReleaseNotesRelPaths = [], - ghaeReleaseNotesRelPaths = [], + const { + mdRelPaths = [], + ymlRelPaths = [], + ghesReleaseNotesRelPaths = [], + ghaeReleaseNotesRelPaths = [], learningTracksRelPaths = [], featureVersionsRelPaths = [], } = groupBy(changedFilesRelPaths, (path) => { @@ -281,16 +321,16 @@ if (!process.env.TEST_TRANSLATION) { ghesReleaseNotesTuples, ghaeReleaseNotesTuples, learningTracksTuples, - featureVersionsTuples + featureVersionsTuples, ] = [ mdRelPaths, ymlRelPaths, ghesReleaseNotesRelPaths, ghaeReleaseNotesRelPaths, learningTracksRelPaths, - featureVersionsRelPaths - ].map(relPaths => { - const absPaths = relPaths.map(p => path.join(rootDir, p)) + featureVersionsRelPaths, + ].map((relPaths) => { + const absPaths = relPaths.map((p) => path.join(rootDir, p)) return zip(relPaths, absPaths) }) @@ -316,722 +356,762 @@ function getContent(content) { describe('lint markdown content', () => { if (mdToLint.length < 1) return - describe.each(mdToLint)( - '%s', - (markdownRelPath, markdownAbsPath) => { - let content, ast, links, yamlScheduledWorkflows, isHidden, isEarlyAccess, isSitePolicy, frontmatterErrors, frontmatterData, - ifversionConditionals, ifConditionals - - beforeAll(async () => { - const fileContents = await readFileAsync(markdownAbsPath, 'utf8') - const { data, content: bodyContent, errors } = frontmatter(fileContents) - - content = bodyContent - frontmatterErrors = errors - frontmatterData = data - ast = generateMarkdownAST(content) - isHidden = data.hidden === true - isEarlyAccess = markdownRelPath.split('/').includes('early-access') - isSitePolicy = markdownRelPath.split('/').includes('site-policy-deprecated') - - links = [] - visit(ast, ['link', 'definition'], node => { - links.push(node.url) - }) - - yamlScheduledWorkflows = [] - visit(ast, 'code', node => { - if (/ya?ml/.test(node.lang) && node.value.includes('schedule') && node.value.includes('cron')) { - yamlScheduledWorkflows.push(node.value) - } - }) - - // visit is not async-friendly so we need to do an async map to parse the YML snippets - yamlScheduledWorkflows = (await Promise.all(yamlScheduledWorkflows.map(async (snippet) => { - // If we don't parse the Liquid first, yaml loading chokes on {% raw %} tags - const rendered = await renderContent.liquid.parseAndRender(snippet) - const parsed = yaml.load(rendered) - return parsed.on.schedule - }))) - .flat() - .map(schedule => schedule.cron) - - ifversionConditionals = getLiquidConditionals(data, ['ifversion', 'elsif']) - .concat(getLiquidConditionals(bodyContent, ['ifversion', 'elsif'])) - - ifConditionals = getLiquidConditionals(data, 'if') - .concat(getLiquidConditionals(bodyContent, 'if')) + describe.each(mdToLint)('%s', (markdownRelPath, markdownAbsPath) => { + let content, + ast, + links, + yamlScheduledWorkflows, + isHidden, + isEarlyAccess, + isSitePolicy, + frontmatterErrors, + frontmatterData, + ifversionConditionals, + ifConditionals + + beforeAll(async () => { + const fileContents = await readFileAsync(markdownAbsPath, 'utf8') + const { data, content: bodyContent, errors } = frontmatter(fileContents) + + content = bodyContent + frontmatterErrors = errors + frontmatterData = data + ast = generateMarkdownAST(content) + isHidden = data.hidden === true + isEarlyAccess = markdownRelPath.split('/').includes('early-access') + isSitePolicy = markdownRelPath.split('/').includes('site-policy-deprecated') + + links = [] + visit(ast, ['link', 'definition'], (node) => { + links.push(node.url) }) - // We need to support some non-Early Access hidden docs in Site Policy - test('hidden docs must be Early Access or Site Policy', async () => { - if (isHidden) { - expect(isEarlyAccess || isSitePolicy).toBe(true) + yamlScheduledWorkflows = [] + visit(ast, 'code', (node) => { + if ( + /ya?ml/.test(node.lang) && + node.value.includes('schedule') && + node.value.includes('cron') + ) { + yamlScheduledWorkflows.push(node.value) } }) - test('ifversion conditionals are valid in markdown', async () => { - const errors = validateIfversionConditionals(ifversionConditionals) - expect(errors.length, errors.join('\n')).toBe(0) - }) - - test('ifversion, not if, is used for versioning in markdown', async () => { - const ifsForVersioning = ifConditionals.filter(cond => versionKeywords.some(keyword => cond.includes(keyword))) - const errorMessage = `Found ${ifsForVersioning.length} "if" conditionals used for versioning! Use "ifversion" instead. + // visit is not async-friendly so we need to do an async map to parse the YML snippets + yamlScheduledWorkflows = ( + await Promise.all( + yamlScheduledWorkflows.map(async (snippet) => { + // If we don't parse the Liquid first, yaml loading chokes on {% raw %} tags + const rendered = await renderContent.liquid.parseAndRender(snippet) + const parsed = yaml.load(rendered) + return parsed.on.schedule + }) + ) + ) + .flat() + .map((schedule) => schedule.cron) + + ifversionConditionals = getLiquidConditionals(data, ['ifversion', 'elsif']).concat( + getLiquidConditionals(bodyContent, ['ifversion', 'elsif']) + ) + + ifConditionals = getLiquidConditionals(data, 'if').concat( + getLiquidConditionals(bodyContent, 'if') + ) + }) + + // We need to support some non-Early Access hidden docs in Site Policy + test('hidden docs must be Early Access or Site Policy', async () => { + if (isHidden) { + expect(isEarlyAccess || isSitePolicy).toBe(true) + } + }) + + test('ifversion conditionals are valid in markdown', async () => { + const errors = validateIfversionConditionals(ifversionConditionals) + expect(errors.length, errors.join('\n')).toBe(0) + }) + + test('ifversion, not if, is used for versioning in markdown', async () => { + const ifsForVersioning = ifConditionals.filter((cond) => + versionKeywords.some((keyword) => cond.includes(keyword)) + ) + const errorMessage = `Found ${ + ifsForVersioning.length + } "if" conditionals used for versioning! Use "ifversion" instead. ${ifsForVersioning.join('\n')}` - expect(ifsForVersioning.length, errorMessage).toBe(0) + expect(ifsForVersioning.length, errorMessage).toBe(0) + }) + + test('relative URLs must start with "/"', async () => { + const matches = links.filter((link) => { + if ( + link.startsWith('http://') || + link.startsWith('https://') || + link.startsWith('tel:') || + link.startsWith('mailto:') || + link.startsWith('#') || + link.startsWith('/') + ) + return false + + return true }) - test('relative URLs must start with "/"', async () => { - const matches = links.filter(link => { - if ( - link.startsWith('http://') || - link.startsWith('https://') || - link.startsWith('tel:') || - link.startsWith('mailto:') || - link.startsWith('#') || - link.startsWith('/') - ) return false - - return true - }) + const errorMessage = formatLinkError(relativeArticleLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - const errorMessage = formatLinkError(relativeArticleLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) + test('yaml snippets that include scheduled workflows must not run on the hour', async () => { + const hourlySchedules = yamlScheduledWorkflows.filter((schedule) => { + const hour = schedule.split(' ')[0] + // return any minute cron segments that equal 0, 00, 000, etc. + return !/[^0]/.test(hour) }) + expect(hourlySchedules).toEqual([]) + }) + + // Note this only ensures that scheduled workflow snippets are unique _per Markdown file_ + test('yaml snippets that include scheduled workflows run at unique times', () => { + expect(yamlScheduledWorkflows.length).toEqual(new Set(yamlScheduledWorkflows).size) + }) + + test('must not leak Early Access doc URLs', async () => { + // Only execute for docs that are NOT Early Access + if (!isEarlyAccess) { + const matches = content.match(earlyAccessLinkRegex) || [] + const errorMessage = formatLinkError(earlyAccessLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + } + }) - test('yaml snippets that include scheduled workflows must not run on the hour', async () => { - const hourlySchedules = yamlScheduledWorkflows.filter(schedule => { - const hour = schedule.split(' ')[0] - // return any minute cron segments that equal 0, 00, 000, etc. - return !/[^0]/.test(hour) + test('must not leak Early Access image URLs', async () => { + // Only execute for docs that are NOT Early Access + if (!isEarlyAccess) { + const matches = content.match(earlyAccessImageRegex) || [] + const errorMessage = formatLinkError(earlyAccessImageErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + } + }) + + test('must have correctly formatted Early Access image URLs', async () => { + // Execute for ALL docs (not just Early Access) to ensure non-EA docs + // are not leaking incorrectly formatted EA image URLs + const matches = content.match(badEarlyAccessImageRegex) || [] + const errorMessage = formatLinkError(badEarlyAccessImageErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) + + if (!process.env.TEST_TRANSLATION) { + test('does not use old site.data variable syntax', async () => { + const matches = content.match(oldVariableRegex) || [] + const matchesWithExample = matches.map((match) => { + const example = match.replace( + /{{\s*?site\.data\.([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)+)\s*?}}/g, + '{% data $1 %}' + ) + return `${match} => ${example}` }) - expect(hourlySchedules).toEqual([]) - }) - - // Note this only ensures that scheduled workflow snippets are unique _per Markdown file_ - test('yaml snippets that include scheduled workflows run at unique times', () => { - expect(yamlScheduledWorkflows.length).toEqual(new Set(yamlScheduledWorkflows).size) + const errorMessage = formatLinkError(oldVariableErrorText, matchesWithExample) + expect(matches.length, errorMessage).toBe(0) }) - test('must not leak Early Access doc URLs', async () => { - // Only execute for docs that are NOT Early Access - if (!isEarlyAccess) { - const matches = (content.match(earlyAccessLinkRegex) || []) - const errorMessage = formatLinkError(earlyAccessLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - } + test('does not use old octicon variable syntax', async () => { + const matches = content.match(oldOcticonRegex) || [] + const errorMessage = formatLinkError(oldOcticonErrorText, matches) + expect(matches.length, errorMessage).toBe(0) }) - test('must not leak Early Access image URLs', async () => { - // Only execute for docs that are NOT Early Access - if (!isEarlyAccess) { - const matches = (content.match(earlyAccessImageRegex) || []) - const errorMessage = formatLinkError(earlyAccessImageErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - } + test('does not use old extended markdown syntax', async () => { + Object.keys(tags).forEach((tag) => { + const reg = new RegExp(`{{\\s*?[#|/]${tag}`, 'g') + if (reg.test(content)) { + const matches = content.match(oldExtendedMarkdownRegex) || [] + const tagMessage = oldExtendedMarkdownErrorText + .replace('{{#note}}', `{{#${tag}}}`) + .replace('{% note %}', `{% ${tag} %}`) + .replace('{% endnote %}', `{% end${tag} %}`) + const errorMessage = formatLinkError(tagMessage, matches) + expect(matches.length, errorMessage).toBe(0) + } + }) }) - test('must have correctly formatted Early Access image URLs', async () => { - // Execute for ALL docs (not just Early Access) to ensure non-EA docs - // are not leaking incorrectly formatted EA image URLs - const matches = (content.match(badEarlyAccessImageRegex) || []) - const errorMessage = formatLinkError(badEarlyAccessImageErrorText, matches) + test('does not contain Liquid that evaluates strings (because they are always true)', async () => { + const matches = content.match(stringInLiquidRegex) || [] + const errorMessage = formatLinkError(stringInLiquidErrorText, matches) expect(matches.length, errorMessage).toBe(0) }) - if (!process.env.TEST_TRANSLATION) { - test('does not use old site.data variable syntax', async () => { - const matches = (content.match(oldVariableRegex) || []) - const matchesWithExample = matches.map(match => { - const example = match - .replace(/{{\s*?site\.data\.([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)+)\s*?}}/g, '{% data $1 %}') - return `${match} => ${example}` - }) - const errorMessage = formatLinkError(oldVariableErrorText, matchesWithExample) - expect(matches.length, errorMessage).toBe(0) - }) - - test('does not use old octicon variable syntax', async () => { - const matches = (content.match(oldOcticonRegex) || []) - const errorMessage = formatLinkError(oldOcticonErrorText, matches) - expect(matches.length, errorMessage).toBe(0) + test('URLs must not contain a hard-coded language code', async () => { + const matches = links.filter((link) => { + return /\/(?:${languageCodes.join('|')})\//.test(link) }) - test('does not use old extended markdown syntax', async () => { - Object.keys(tags).forEach(tag => { - const reg = new RegExp(`{{\\s*?[#|/]${tag}`, 'g') - if (reg.test(content)) { - const matches = (content.match(oldExtendedMarkdownRegex)) || [] - const tagMessage = oldExtendedMarkdownErrorText - .replace('{{#note}}', `{{#${tag}}}`) - .replace('{% note %}', `{% ${tag} %}`) - .replace('{% endnote %}', `{% end${tag} %}`) - const errorMessage = formatLinkError(tagMessage, matches) - expect(matches.length, errorMessage).toBe(0) - } - }) - }) - - test('does not contain Liquid that evaluates strings (because they are always true)', async () => { - const matches = (content.match(stringInLiquidRegex) || []) - const errorMessage = formatLinkError(stringInLiquidErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) - - test('URLs must not contain a hard-coded language code', async () => { - const matches = links.filter(link => { - return /\/(?:${languageCodes.join('|')})\//.test(link) - }) - - const errorMessage = formatLinkError(languageLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(languageLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('URLs must not contain a hard-coded version number', async () => { - const initialMatches = (content.match(versionLinkRegEx) || []) + test('URLs must not contain a hard-coded version number', async () => { + const initialMatches = content.match(versionLinkRegEx) || [] - // Filter out some very specific false positive matches - const matches = initialMatches.filter(() => { - if ( - markdownRelPath.endsWith('migrating-from-github-enterprise-1110x-to-2123.md') || - markdownRelPath.endsWith('all-releases.md') - ) { - return false - } - return true - }) - - const errorMessage = formatLinkError(versionLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) + // Filter out some very specific false positive matches + const matches = initialMatches.filter(() => { + if ( + markdownRelPath.endsWith('migrating-from-github-enterprise-1110x-to-2123.md') || + markdownRelPath.endsWith('all-releases.md') + ) { + return false + } + return true }) - test('URLs must not contain a hard-coded domain name', async () => { - const initialMatches = (content.match(domainLinkRegex) || []) + const errorMessage = formatLinkError(versionLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - // Filter out some very specific false positive matches - const matches = initialMatches.filter(() => { - if (markdownRelPath === 'content/admin/all-releases.md') { - return false - } - return true - }) + test('URLs must not contain a hard-coded domain name', async () => { + const initialMatches = content.match(domainLinkRegex) || [] - const errorMessage = formatLinkError(domainLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) + // Filter out some very specific false positive matches + const matches = initialMatches.filter(() => { + if (markdownRelPath === 'content/admin/all-releases.md') { + return false + } + return true }) - } - test('contains valid Liquid', async () => { - // If Liquid can't parse the file, it'll throw an error. - // For example, the following is invalid and will fail this test: - // {% if currentVersion ! "github-ae@latest" %} - expect(() => renderContent.liquid.parse(content)) - .not - .toThrow() + const errorMessage = formatLinkError(domainLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) }) + } - if (!markdownRelPath.includes('data/reusables')) { - test('contains valid frontmatter', () => { - const errorMessage = frontmatterErrors.map(error => `- [${error.property}]: ${error.actual}, ${error.message}`).join('\n') - expect(frontmatterErrors.length, errorMessage).toBe(0) - }) + test('contains valid Liquid', async () => { + // If Liquid can't parse the file, it'll throw an error. + // For example, the following is invalid and will fail this test: + // {% if currentVersion ! "github-ae@latest" %} + expect(() => renderContent.liquid.parse(content)).not.toThrow() + }) + + if (!markdownRelPath.includes('data/reusables')) { + test('contains valid frontmatter', () => { + const errorMessage = frontmatterErrors + .map((error) => `- [${error.property}]: ${error.actual}, ${error.message}`) + .join('\n') + expect(frontmatterErrors.length, errorMessage).toBe(0) + }) - test('frontmatter contains valid liquid', async () => { - const fmKeysWithLiquid = ['title', 'shortTitle', 'intro', 'product', 'permission'] - .filter(key => Boolean(frontmatterData[key])) + test('frontmatter contains valid liquid', async () => { + const fmKeysWithLiquid = ['title', 'shortTitle', 'intro', 'product', 'permission'].filter( + (key) => Boolean(frontmatterData[key]) + ) - for (const key of fmKeysWithLiquid) { - expect(() => renderContent.liquid.parse(frontmatterData[key])) - .not - .toThrow() - } - }) - } + for (const key of fmKeysWithLiquid) { + expect(() => renderContent.liquid.parse(frontmatterData[key])).not.toThrow() + } + }) } - ) + }) }) describe('lint yaml content', () => { if (ymlToLint.length < 1) return - describe.each(ymlToLint)( - '%s', - (yamlRelPath, yamlAbsPath) => { - let dictionary, isEarlyAccess, ifversionConditionals, ifConditionals + describe.each(ymlToLint)('%s', (yamlRelPath, yamlAbsPath) => { + let dictionary, isEarlyAccess, ifversionConditionals, ifConditionals - beforeAll(async () => { - const fileContents = await readFileAsync(yamlAbsPath, 'utf8') - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) + beforeAll(async () => { + const fileContents = await readFileAsync(yamlAbsPath, 'utf8') + dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - isEarlyAccess = yamlRelPath.split('/').includes('early-access') + isEarlyAccess = yamlRelPath.split('/').includes('early-access') - ifversionConditionals = getLiquidConditionals(fileContents, ['ifversion', 'elsif']) + ifversionConditionals = getLiquidConditionals(fileContents, ['ifversion', 'elsif']) - ifConditionals = getLiquidConditionals(fileContents, 'if') - }) + ifConditionals = getLiquidConditionals(fileContents, 'if') + }) - test('ifversion conditionals are valid in yaml', async () => { - const errors = validateIfversionConditionals(ifversionConditionals) - expect(errors.length, errors.join('\n')).toBe(0) - }) + test('ifversion conditionals are valid in yaml', async () => { + const errors = validateIfversionConditionals(ifversionConditionals) + expect(errors.length, errors.join('\n')).toBe(0) + }) - test('ifversion, not if, is used for versioning in markdown', async () => { - const ifsForVersioning = ifConditionals.filter(cond => versionKeywords.some(keyword => cond.includes(keyword))) - const errorMessage = `Found ${ifsForVersioning.length} "if" conditionals used for versioning! Use "ifversion" instead. + test('ifversion, not if, is used for versioning in markdown', async () => { + const ifsForVersioning = ifConditionals.filter((cond) => + versionKeywords.some((keyword) => cond.includes(keyword)) + ) + const errorMessage = `Found ${ + ifsForVersioning.length + } "if" conditionals used for versioning! Use "ifversion" instead. ${ifsForVersioning.join('\n')}` - expect(ifsForVersioning.length, errorMessage).toBe(0) - }) + expect(ifsForVersioning.length, errorMessage).toBe(0) + }) + + test('relative URLs must start with "/"', async () => { + const matches = [] + + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(relativeArticleLinkRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) + } + } + + const errorMessage = formatLinkError(relativeArticleLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('relative URLs must start with "/"', async () => { + test('must not leak Early Access doc URLs', async () => { + // Only execute for docs that are NOT Early Access + if (!isEarlyAccess) { const matches = [] for (const [key, content] of Object.entries(dictionary)) { const contentStr = getContent(content) if (!contentStr) continue - const valMatches = (contentStr.match(relativeArticleLinkRegex) || []) + const valMatches = contentStr.match(earlyAccessLinkRegex) || [] if (valMatches.length > 0) { matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } } - const errorMessage = formatLinkError(relativeArticleLinkErrorText, matches) + const errorMessage = formatLinkError(earlyAccessLinkErrorText, matches) expect(matches.length, errorMessage).toBe(0) - }) - - test('must not leak Early Access doc URLs', async () => { - // Only execute for docs that are NOT Early Access - if (!isEarlyAccess) { - const matches = [] - - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(earlyAccessLinkRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } - } + } + }) - const errorMessage = formatLinkError(earlyAccessLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - } - }) + test('must not leak Early Access image URLs', async () => { + // Only execute for docs that are NOT Early Access + if (!isEarlyAccess) { + const matches = [] - test('must not leak Early Access image URLs', async () => { - // Only execute for docs that are NOT Early Access - if (!isEarlyAccess) { - const matches = [] - - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(earlyAccessImageRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(earlyAccessImageRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } + } - const errorMessage = formatLinkError(earlyAccessImageErrorText, matches) - expect(matches.length, errorMessage).toBe(0) + const errorMessage = formatLinkError(earlyAccessImageErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + } + }) + + test('must have correctly formatted Early Access image URLs', async () => { + // Execute for ALL docs (not just Early Access) to ensure non-EA docs + // are not leaking incorrectly formatted EA image URLs + const matches = [] + + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(badEarlyAccessImageRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } - }) + } - test('must have correctly formatted Early Access image URLs', async () => { - // Execute for ALL docs (not just Early Access) to ensure non-EA docs - // are not leaking incorrectly formatted EA image URLs + const errorMessage = formatLinkError(badEarlyAccessImageErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) + + if (!process.env.TEST_TRANSLATION) { + test('URLs must not contain a hard-coded language code', async () => { const matches = [] for (const [key, content] of Object.entries(dictionary)) { const contentStr = getContent(content) if (!contentStr) continue - const valMatches = (contentStr.match(badEarlyAccessImageRegex) || []) + const valMatches = contentStr.match(languageLinkRegex) || [] if (valMatches.length > 0) { matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } } - const errorMessage = formatLinkError(badEarlyAccessImageErrorText, matches) + const errorMessage = formatLinkError(languageLinkErrorText, matches) expect(matches.length, errorMessage).toBe(0) }) - if (!process.env.TEST_TRANSLATION) { - test('URLs must not contain a hard-coded language code', async () => { - const matches = [] + test('URLs must not contain a hard-coded version number', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(languageLinkRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(versionLinkRegEx) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } + } - const errorMessage = formatLinkError(languageLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(versionLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('URLs must not contain a hard-coded version number', async () => { - const matches = [] + test('URLs must not contain a hard-coded domain name', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(versionLinkRegEx) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(domainLinkRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } + } - const errorMessage = formatLinkError(versionLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) - - test('URLs must not contain a hard-coded domain name', async () => { - const matches = [] - - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(domainLinkRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } - } + const errorMessage = formatLinkError(domainLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - const errorMessage = formatLinkError(domainLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + test('does not use old site.data variable syntax', async () => { + const matches = [] - test('does not use old site.data variable syntax', async () => { - const matches = [] - - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(oldVariableRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => { - const example = match - .replace(/{{\s*?site\.data\.([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)+)\s*?}}/g, '{% data $1 %}') + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(oldVariableRegex) || [] + if (valMatches.length > 0) { + matches.push( + ...valMatches.map((match) => { + const example = match.replace( + /{{\s*?site\.data\.([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)+)\s*?}}/g, + '{% data $1 %}' + ) return `Key "${key}": ${match} => ${example}` - })) - } + }) + ) } + } - const errorMessage = formatLinkError(oldVariableErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(oldVariableErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('does not use old octicon variable syntax', async () => { - const matches = [] + test('does not use old octicon variable syntax', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(oldOcticonRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(oldOcticonRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } + } - const errorMessage = formatLinkError(oldOcticonErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(oldOcticonErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('does not use old extended markdown syntax', async () => { - const matches = [] + test('does not use old extended markdown syntax', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(oldExtendedMarkdownRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(oldExtendedMarkdownRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } + } - const errorMessage = formatLinkError(oldExtendedMarkdownErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(oldExtendedMarkdownErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('does not contain Liquid that evaluates strings (because they are always true)', async () => { - const matches = [] + test('does not contain Liquid that evaluates strings (because they are always true)', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = (contentStr.match(stringInLiquidRegex) || []) - if (valMatches.length > 0) { - matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) - } + for (const [key, content] of Object.entries(dictionary)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(stringInLiquidRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match) => `Key "${key}": ${match}`)) } + } - const errorMessage = formatLinkError(stringInLiquidErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) - } + const errorMessage = formatLinkError(stringInLiquidErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) } - ) + }) }) describe('lint GHES release notes', () => { if (ghesReleaseNotesToLint.length < 1) return - describe.each(ghesReleaseNotesToLint)( - '%s', - (yamlRelPath, yamlAbsPath) => { - let dictionary - - beforeAll(async () => { - const fileContents = await readFileAsync(yamlAbsPath, 'utf8') - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - }) - - it('matches the schema', () => { - const { errors } = revalidator.validate(dictionary, ghesReleaseNotesSchema) - const errorMessage = errors.map(error => `- [${error.property}]: ${error.actual}, ${error.message}`).join('\n') - expect(errors.length, errorMessage).toBe(0) - }) - - it('contains valid liquid', () => { - const { intro, sections } = dictionary - let toLint = { intro } - for (const key in sections) { - const section = sections[key] - const label = `sections.${key}` - section.forEach((part) => { - if (Array.isArray(part)) { - toLint = { ...toLint, ...{ [label]: section.join('\n') } } - } else { - for (const prop in section) { - toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } } - } + describe.each(ghesReleaseNotesToLint)('%s', (yamlRelPath, yamlAbsPath) => { + let dictionary + + beforeAll(async () => { + const fileContents = await readFileAsync(yamlAbsPath, 'utf8') + dictionary = yaml.load(fileContents, { filename: yamlRelPath }) + }) + + it('matches the schema', () => { + const { errors } = revalidator.validate(dictionary, ghesReleaseNotesSchema) + const errorMessage = errors + .map((error) => `- [${error.property}]: ${error.actual}, ${error.message}`) + .join('\n') + expect(errors.length, errorMessage).toBe(0) + }) + + it('contains valid liquid', () => { + const { intro, sections } = dictionary + let toLint = { intro } + for (const key in sections) { + const section = sections[key] + const label = `sections.${key}` + section.forEach((part) => { + if (Array.isArray(part)) { + toLint = { ...toLint, ...{ [label]: section.join('\n') } } + } else { + for (const prop in section) { + toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } } } - }) - } + } + }) + } - for (const key in toLint) { - if (!toLint[key]) continue - expect(() => renderContent.liquid.parse(toLint[key]), `${key} contains invalid liquid`) - .not - .toThrow() - } - }) - } - ) + for (const key in toLint) { + if (!toLint[key]) continue + expect( + () => renderContent.liquid.parse(toLint[key]), + `${key} contains invalid liquid` + ).not.toThrow() + } + }) + }) }) describe('lint GHAE release notes', () => { if (ghaeReleaseNotesToLint.length < 1) return const currentWeeksFound = [] - describe.each(ghaeReleaseNotesToLint)( - '%s', - (yamlRelPath, yamlAbsPath) => { - let dictionary - - beforeAll(async () => { - const fileContents = await readFileAsync(yamlAbsPath, 'utf8') - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - }) - - it('matches the schema', () => { - const { errors } = revalidator.validate(dictionary, ghaeReleaseNotesSchema) - const errorMessage = errors.map(error => `- [${error.property}]: ${error.actual}, ${error.message}`).join('\n') - expect(errors.length, errorMessage).toBe(0) - }) - - it('does not have more than one yaml file with currentWeek set to true', () => { - if (dictionary.currentWeek) currentWeeksFound.push(yamlRelPath) - const errorMessage = `Found more than one file with currentWeek set to true: ${currentWeeksFound.join('\n')}` - expect(currentWeeksFound.length, errorMessage).not.toBeGreaterThan(1) - }) - - it('contains valid liquid', () => { - const { intro, sections } = dictionary - let toLint = { intro } - for (const key in sections) { - const section = sections[key] - const label = `sections.${key}` - section.forEach((part) => { - if (Array.isArray(part)) { - toLint = { ...toLint, ...{ [label]: section.join('\n') } } - } else { - for (const prop in section) { - toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } } - } + describe.each(ghaeReleaseNotesToLint)('%s', (yamlRelPath, yamlAbsPath) => { + let dictionary + + beforeAll(async () => { + const fileContents = await readFileAsync(yamlAbsPath, 'utf8') + dictionary = yaml.load(fileContents, { filename: yamlRelPath }) + }) + + it('matches the schema', () => { + const { errors } = revalidator.validate(dictionary, ghaeReleaseNotesSchema) + const errorMessage = errors + .map((error) => `- [${error.property}]: ${error.actual}, ${error.message}`) + .join('\n') + expect(errors.length, errorMessage).toBe(0) + }) + + it('does not have more than one yaml file with currentWeek set to true', () => { + if (dictionary.currentWeek) currentWeeksFound.push(yamlRelPath) + const errorMessage = `Found more than one file with currentWeek set to true: ${currentWeeksFound.join( + '\n' + )}` + expect(currentWeeksFound.length, errorMessage).not.toBeGreaterThan(1) + }) + + it('contains valid liquid', () => { + const { intro, sections } = dictionary + let toLint = { intro } + for (const key in sections) { + const section = sections[key] + const label = `sections.${key}` + section.forEach((part) => { + if (Array.isArray(part)) { + toLint = { ...toLint, ...{ [label]: section.join('\n') } } + } else { + for (const prop in section) { + toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } } } - }) - } + } + }) + } - for (const key in toLint) { - if (!toLint[key]) continue - expect(() => renderContent.liquid.parse(toLint[key]), `${key} contains invalid liquid`) - .not - .toThrow() - } - }) - } - ) + for (const key in toLint) { + if (!toLint[key]) continue + expect( + () => renderContent.liquid.parse(toLint[key]), + `${key} contains invalid liquid` + ).not.toThrow() + } + }) + }) }) describe('lint learning tracks', () => { if (learningTracksToLint.length < 1) return - describe.each(learningTracksToLint)( - '%s', - (yamlRelPath, yamlAbsPath) => { - let dictionary - - beforeAll(async () => { - const fileContents = await readFileAsync(yamlAbsPath, 'utf8') - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - }) - - it('matches the schema', () => { - const { errors } = revalidator.validate(dictionary, learningTracksSchema) - const errorMessage = errors.map(error => `- [${error.property}]: ${error.actual}, ${error.message}`).join('\n') - expect(errors.length, errorMessage).toBe(0) - }) - - it('has one and only one featured track per supported version', async () => { - // Use the YAML filename to determine which product this refers to, and then peek - // inside the product TOC frontmatter to see which versions the product is available in. - const product = path.posix.basename(yamlRelPath, '.yml') - const productTocPath = path.posix.join('content', product, 'index.md') - const productContents = await readFileAsync(productTocPath, 'utf8') - const { data } = frontmatter(productContents) - const productVersions = getApplicableVersions(data.versions, productTocPath) - - const featuredTracks = {} - const context = { enterpriseServerVersions } - - // For each of the product's versions, render the learning track data and look for a featured track. - await Promise.all(productVersions.map(async (version) => { + describe.each(learningTracksToLint)('%s', (yamlRelPath, yamlAbsPath) => { + let dictionary + + beforeAll(async () => { + const fileContents = await readFileAsync(yamlAbsPath, 'utf8') + dictionary = yaml.load(fileContents, { filename: yamlRelPath }) + }) + + it('matches the schema', () => { + const { errors } = revalidator.validate(dictionary, learningTracksSchema) + const errorMessage = errors + .map((error) => `- [${error.property}]: ${error.actual}, ${error.message}`) + .join('\n') + expect(errors.length, errorMessage).toBe(0) + }) + + it('has one and only one featured track per supported version', async () => { + // Use the YAML filename to determine which product this refers to, and then peek + // inside the product TOC frontmatter to see which versions the product is available in. + const product = path.posix.basename(yamlRelPath, '.yml') + const productTocPath = path.posix.join('content', product, 'index.md') + const productContents = await readFileAsync(productTocPath, 'utf8') + const { data } = frontmatter(productContents) + const productVersions = getApplicableVersions(data.versions, productTocPath) + + const featuredTracks = {} + const context = { enterpriseServerVersions } + + // For each of the product's versions, render the learning track data and look for a featured track. + await Promise.all( + productVersions.map(async (version) => { const featuredTracksPerVersion = [] for (const entry of Object.values(dictionary)) { if (!entry.featured_track) return context.currentVersion = version context[allVersions[version].shortName] = true - const isFeaturedLink = typeof entry.featured_track === 'boolean' || (await renderContent(entry.featured_track, context, { textOnly: true, encodeEntities: true }) === 'true') + const isFeaturedLink = + typeof entry.featured_track === 'boolean' || + (await renderContent(entry.featured_track, context, { + textOnly: true, + encodeEntities: true, + })) === 'true' featuredTracksPerVersion.push(isFeaturedLink) } featuredTracks[version] = featuredTracksPerVersion.length - })) - - Object.entries(featuredTracks).forEach(([version, numOfFeaturedTracks]) => { - const errorMessage = `Expected 1 featured learning track but found ${numOfFeaturedTracks} for ${version} in ${yamlAbsPath}` - expect(numOfFeaturedTracks, errorMessage).toBe(1) }) + ) + + Object.entries(featuredTracks).forEach(([version, numOfFeaturedTracks]) => { + const errorMessage = `Expected 1 featured learning track but found ${numOfFeaturedTracks} for ${version} in ${yamlAbsPath}` + expect(numOfFeaturedTracks, errorMessage).toBe(1) }) + }) - it('contains valid liquid', () => { - const toLint = [] - Object.values(dictionary).forEach(({ title, description }) => { - toLint.push(title) - toLint.push(description) - }) + it('contains valid liquid', () => { + const toLint = [] + Object.values(dictionary).forEach(({ title, description }) => { + toLint.push(title) + toLint.push(description) + }) - toLint.forEach(element => { - expect(() => renderContent.liquid.parse(element), `${element} contains invalid liquid`) - .not - .toThrow() - }) + toLint.forEach((element) => { + expect( + () => renderContent.liquid.parse(element), + `${element} contains invalid liquid` + ).not.toThrow() }) - } - ) + }) + }) }) describe('lint feature versions', () => { if (featureVersionsToLint.length < 1) return - describe.each(featureVersionsToLint)( - '%s', - (yamlRelPath, yamlAbsPath) => { - let dictionary - - beforeAll(async () => { - const fileContents = await readFileAsync(yamlAbsPath, 'utf8') - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - }) + describe.each(featureVersionsToLint)('%s', (yamlRelPath, yamlAbsPath) => { + let dictionary + + beforeAll(async () => { + const fileContents = await readFileAsync(yamlAbsPath, 'utf8') + dictionary = yaml.load(fileContents, { filename: yamlRelPath }) + }) - it('matches the schema', () => { - const { errors } = revalidator.validate(dictionary, featureVersionsSchema) + it('matches the schema', () => { + const { errors } = revalidator.validate(dictionary, featureVersionsSchema) - const errorMessage = errors.map(error => { + const errorMessage = errors + .map((error) => { // Make this one message a little more readable than the error we get from revalidator // when additionalProperties is set to false and an additional prop is found. - const errorToReport = error.message === 'must not exist' && error.actual.feature - ? `feature: '${error.actual.feature}'` - : JSON.stringify(error.actual, null, 2) + const errorToReport = + error.message === 'must not exist' && error.actual.feature + ? `feature: '${error.actual.feature}'` + : JSON.stringify(error.actual, null, 2) - return `- [${error.property}]: ${errorToReport}, ${error.message}` + return `- [${error.property}]: ${errorToReport}, ${error.message}` }) - .join('\n') + .join('\n') - expect(errors.length, errorMessage).toBe(0) - }) - } - ) + expect(errors.length, errorMessage).toBe(0) + }) + }) }) -function validateVersion (version) { - return versionShortNames.includes(version) || - versionShortNameExceptions.some(exception => version.startsWith(exception)) +function validateVersion(version) { + return ( + versionShortNames.includes(version) || + versionShortNameExceptions.some((exception) => version.startsWith(exception)) + ) } -function validateIfversionConditionals (conds) { +function validateIfversionConditionals(conds) { const errors = [] - conds.forEach(cond => { + conds.forEach((cond) => { // This will get us an array of strings, where each string may have these space-separated parts: // * Length 1: `<version>` (example: `fpt`) // * Length 2: `not <version>` (example: `not ghae`) // * Length 3: `<version> <operator> <release>` (example: `ghes > 3.0`) - const condParts = cond - .split(/ (or|and) /) - .filter(part => !(part === 'or' || part === 'and')) - - condParts - .forEach(str => { - const strParts = str.split(' ') - // if length = 1, this should be a valid short version name. - if (strParts.length === 1) { - const version = strParts[0] - const isValidVersion = validateVersion(version) - if (!isValidVersion) { - errors.push(`"${version}" is not a valid short version name`) - } + const condParts = cond.split(/ (or|and) /).filter((part) => !(part === 'or' || part === 'and')) + + condParts.forEach((str) => { + const strParts = str.split(' ') + // if length = 1, this should be a valid short version name. + if (strParts.length === 1) { + const version = strParts[0] + const isValidVersion = validateVersion(version) + if (!isValidVersion) { + errors.push(`"${version}" is not a valid short version name`) } + } - // if length = 2, this should be 'not' followed by a valid short version name. - if (strParts.length === 2) { - const [notKeyword, version] = strParts - const isValidVersion = validateVersion(version) - const isValid = notKeyword === 'not' && isValidVersion - if (!isValid) { - errors.push(`"${cond}" is not a valid conditional`) - } + // if length = 2, this should be 'not' followed by a valid short version name. + if (strParts.length === 2) { + const [notKeyword, version] = strParts + const isValidVersion = validateVersion(version) + const isValid = notKeyword === 'not' && isValidVersion + if (!isValid) { + errors.push(`"${cond}" is not a valid conditional`) } + } - // if length = 3, this should be a range in the format: ghes > 3.0 - // where the first item is `ghes` (currently the only version with numbered releases), - // the second item is a supported operator, and the third is a supported GHES release. - if (strParts.length === 3) { - const [version, operator, release] = strParts - if (version !== 'ghes') { - errors.push(`Found "${version}" inside "${cond}" with a "${operator}" operator; expected "ghes"`) - } - if (!allowedVersionOperators.includes(operator)) { - errors.push(`Found a "${operator}" operator inside "${cond}", but "${operator}" is not supported`) - } - // NOTE: The following will throw errors when we deprecate a version until we run the script to remove the - // deprecated versioning. If we deprecate a version before we have a working version of that script, - // we can comment out this part of the test temporarily and re-enable it once the script is ready. - if (!(supported.includes(release) || release === next)) { - errors.push(`Found ${release} inside "${cond}", but ${release} is not a supported GHES release`) - } + // if length = 3, this should be a range in the format: ghes > 3.0 + // where the first item is `ghes` (currently the only version with numbered releases), + // the second item is a supported operator, and the third is a supported GHES release. + if (strParts.length === 3) { + const [version, operator, release] = strParts + if (version !== 'ghes') { + errors.push( + `Found "${version}" inside "${cond}" with a "${operator}" operator; expected "ghes"` + ) } - }) + if (!allowedVersionOperators.includes(operator)) { + errors.push( + `Found a "${operator}" operator inside "${cond}", but "${operator}" is not supported` + ) + } + // NOTE: The following will throw errors when we deprecate a version until we run the script to remove the + // deprecated versioning. If we deprecate a version before we have a working version of that script, + // we can comment out this part of the test temporarily and re-enable it once the script is ready. + if (!(supported.includes(release) || release === next)) { + errors.push( + `Found ${release} inside "${cond}", but ${release} is not a supported GHES release` + ) + } + } + }) }) return errors diff --git a/tests/meta/orphan-tests.js b/tests/meta/orphan-tests.js index bf77115a71d8..2b80cde45c7e 100644 --- a/tests/meta/orphan-tests.js +++ b/tests/meta/orphan-tests.js @@ -13,17 +13,12 @@ describe('check for orphan tests', () => { const testDirectory = await fs.readdir(pathToTests) // Filter out our exceptions - let filteredList = testDirectory - .filter(item => !EXCEPTIONS.includes(item)) + let filteredList = testDirectory.filter((item) => !EXCEPTIONS.includes(item)) // Don't include directories filteredList = await asyncFilter( filteredList, - async item => !( - await fs.stat( - path.join(pathToTests, item) - ) - ).isDirectory() + async (item) => !(await fs.stat(path.join(pathToTests, item))).isDirectory() ) expect(filteredList).toHaveLength(0) diff --git a/tests/meta/repository-references.js b/tests/meta/repository-references.js index aaefbdc4d438..dfedb06de9c0 100644 --- a/tests/meta/repository-references.js +++ b/tests/meta/repository-references.js @@ -35,7 +35,7 @@ const ALLOW_LIST = new Set([ 'renaming', 'localization-support', 'docs', - 'securitylab' + 'securitylab', ]) describe('check if a GitHub-owned private repository is referenced', () => { @@ -69,15 +69,15 @@ describe('check if a GitHub-owned private repository is referenced', () => { '**/*.pdf', '**/*.ico', '**/*.woff', - 'script/deploy' - ] + 'script/deploy', + ], }) test.each(filenames)('in file %s', async (filename) => { const file = await readFileAsync(filename, 'utf8') const matches = Array.from(file.matchAll(REPO_REGEXP)) .map(([, repoName]) => repoName) - .filter(repoName => !ALLOW_LIST.has(repoName)) + .filter((repoName) => !ALLOW_LIST.has(repoName)) expect(matches).toHaveLength(0) }) }) diff --git a/tests/rendering/block-robots.js b/tests/rendering/block-robots.js index 8ca263d7d1cb..d7bc1e16c5c7 100644 --- a/tests/rendering/block-robots.js +++ b/tests/rendering/block-robots.js @@ -3,7 +3,7 @@ import languages from '../../lib/languages.js' import { productMap } from '../../lib/all-products.js' import enterpriseServerReleases from '../../lib/enterprise-server-releases.js' -function allowIndex (path) { +function allowIndex(path) { return !blockIndex(path) } @@ -16,8 +16,8 @@ describe('block robots', () => { it('allows crawling of generally available localized content', async () => { Object.values(languages) - .filter(language => !language.wip) - .forEach(language => { + .filter((language) => !language.wip) + .forEach((language) => { expect(allowIndex(`/${language.code}`)).toBe(true) expect(allowIndex(`/${language.code}/articles/verifying-your-email-address`)).toBe(true) }) @@ -25,8 +25,8 @@ describe('block robots', () => { it('disallows crawling of WIP localized content', async () => { Object.values(languages) - .filter(language => language.wip) - .forEach(language => { + .filter((language) => language.wip) + .forEach((language) => { expect(allowIndex(`/${language.code}`)).toBe(false) expect(allowIndex(`/${language.code}/articles/verifying-your-email-address`)).toBe(false) }) @@ -34,10 +34,10 @@ describe('block robots', () => { it('disallows crawling of WIP products', async () => { const wipProductIds = Object.values(productMap) - .filter(product => product.wip) - .map(product => product.id) + .filter((product) => product.wip) + .map((product) => product.id) - wipProductIds.forEach(id => { + wipProductIds.forEach((id) => { const { href } = productMap[id] const blockedPaths = [ // English @@ -52,10 +52,10 @@ describe('block robots', () => { `/ja${href}/overview`, `/ja${href}/overview/intro`, `/ja/enterprise/${enterpriseServerReleases.latest}/user${href}`, - `/ja/enterprise/${enterpriseServerReleases.oldestSupported}/user${href}` + `/ja/enterprise/${enterpriseServerReleases.oldestSupported}/user${href}`, ] - blockedPaths.forEach(path => { + blockedPaths.forEach((path) => { expect(allowIndex(path)).toBe(false) }) }) @@ -63,23 +63,25 @@ describe('block robots', () => { it('disallows crawling of early access "hidden" products', async () => { const hiddenProductIds = Object.values(productMap) - .filter(product => product.hidden) - .map(product => product.id) + .filter((product) => product.hidden) + .map((product) => product.id) - hiddenProductIds.forEach(id => { + hiddenProductIds.forEach((id) => { const { versions } = productMap[id] - const blockedPaths = versions.map(version => { - return [ - // English - `/en/${version}/${id}`, - `/en/${version}/${id}/some-early-access-article`, - // Japanese - `/ja/${version}/${id}`, - `/ja/${version}/${id}/some-early-access-article` - ] - }).flat() + const blockedPaths = versions + .map((version) => { + return [ + // English + `/en/${version}/${id}`, + `/en/${version}/${id}/some-early-access-article`, + // Japanese + `/ja/${version}/${id}`, + `/ja/${version}/${id}/some-early-access-article`, + ] + }) + .flat() - blockedPaths.forEach(path => { + blockedPaths.forEach((path) => { expect(allowIndex(path)).toBe(false) }) }) @@ -91,11 +93,13 @@ describe('block robots', () => { expect(allowIndex('/en/actions/overview')).toBe(true) expect(allowIndex('/en/actions/overview/intro')).toBe(true) expect(allowIndex(`/en/enterprise/${enterpriseServerReleases.latest}/user/actions`)).toBe(true) - expect(allowIndex(`/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/actions`)).toBe(true) + expect( + allowIndex(`/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/actions`) + ).toBe(true) }) it('disallows crawling of deprecated enterprise releases', async () => { - enterpriseServerReleases.deprecated.forEach(version => { + enterpriseServerReleases.deprecated.forEach((version) => { const blockedPaths = [ // English `/en/enterprise-server@${version}/actions`, @@ -106,10 +110,10 @@ describe('block robots', () => { `/ja/enterprise-server@${version}/actions`, `/ja/enterprise/${version}/actions`, `/ja/enterprise-server@${version}/actions/overview`, - `/ja/enterprise/${version}/actions/overview` + `/ja/enterprise/${version}/actions/overview`, ] - blockedPaths.forEach(path => { + blockedPaths.forEach((path) => { expect(allowIndex(path)).toBe(false) }) }) diff --git a/tests/rendering/breadcrumbs.js b/tests/rendering/breadcrumbs.js index 58ebeb633dd3..7e91c0e0f816 100644 --- a/tests/rendering/breadcrumbs.js +++ b/tests/rendering/breadcrumbs.js @@ -1,7 +1,8 @@ import { getDOM, getJSON } from '../helpers/supertest.js' import { jest } from '@jest/globals' -const describeInternalOnly = process.env.GITHUB_REPOSITORY === 'github/docs-internal' ? describe : describe.skip +const describeInternalOnly = + process.env.GITHUB_REPOSITORY === 'github/docs-internal' ? describe : describe.skip describe('breadcrumbs', () => { jest.setTimeout(300 * 1000) @@ -13,7 +14,9 @@ describe('breadcrumbs', () => { }) test('article pages have breadcrumbs with product, category, maptopic, and article', async () => { - const $ = await getDOM('/github/authenticating-to-github/troubleshooting-ssh/using-ssh-over-the-https-port') + const $ = await getDOM( + '/github/authenticating-to-github/troubleshooting-ssh/using-ssh-over-the-https-port' + ) const $breadcrumbs = $('.breadcrumbs a') expect($breadcrumbs).toHaveLength(4) @@ -24,7 +27,9 @@ describe('breadcrumbs', () => { }) test('maptopic pages include their own grayed-out breadcrumb', async () => { - const $ = await getDOM('/github/authenticating-to-github/keeping-your-account-and-data-secure') + const $ = await getDOM( + '/github/authenticating-to-github/keeping-your-account-and-data-secure' + ) const $breadcrumbs = $('.breadcrumbs a') expect($breadcrumbs).toHaveLength(3) @@ -35,7 +40,9 @@ describe('breadcrumbs', () => { }) test('works for enterprise user pages', async () => { - const $ = await getDOM('/en/enterprise-server/github/authenticating-to-github/troubleshooting-ssh/recovering-your-ssh-key-passphrase') + const $ = await getDOM( + '/en/enterprise-server/github/authenticating-to-github/troubleshooting-ssh/recovering-your-ssh-key-passphrase' + ) const $breadcrumbs = $('.breadcrumbs a') expect($breadcrumbs).toHaveLength(4) // The product is still GitHub.com on an Enterprise Server version @@ -69,7 +76,9 @@ describe('breadcrumbs', () => { }) test('early access article pages have breadcrumbs with product, category, and article', async () => { - const $ = await getDOM('/early-access/github/enforcing-best-practices-with-github-policies/about-github-policies') + const $ = await getDOM( + '/early-access/github/enforcing-best-practices-with-github-policies/about-github-policies' + ) const $breadcrumbSpans = $('.breadcrumbs span') const $breadcrumbLinks = $('.breadcrumbs a') @@ -77,7 +86,9 @@ describe('breadcrumbs', () => { expect($breadcrumbLinks).toHaveLength(2) expect($breadcrumbSpans[0].children[0].data).toBe('Early Access documentation') expect($breadcrumbSpans[1].children[0].data).toBe('GitHub.com') - expect($breadcrumbLinks[0].attribs.title).toBe('category: Enforcing best practices with GitHub Policies') + expect($breadcrumbLinks[0].attribs.title).toBe( + 'category: Enforcing best practices with GitHub Policies' + ) expect($breadcrumbLinks[1].attribs.title).toBe('article: About GitHub Policies') expect($breadcrumbLinks[1].attribs.class.includes('color-text-tertiary')).toBe(true) }) @@ -90,8 +101,8 @@ describe('breadcrumbs', () => { { documentType: 'product', href: '/en/github', - title: 'GitHub.com' - } + title: 'GitHub.com', + }, ] expect(breadcrumbs).toEqual(expected) }) @@ -102,84 +113,90 @@ describe('breadcrumbs', () => { { documentType: 'product', href: '/en/github', - title: 'GitHub.com' + title: 'GitHub.com', }, { documentType: 'category', href: '/en/github/authenticating-to-github', - title: 'Authentication' - } + title: 'Authentication', + }, ] expect(breadcrumbs).toEqual(expected) }) test('works on maptopic pages', async () => { - const breadcrumbs = await getJSON('/en/github/authenticating-to-github/keeping-your-account-and-data-secure?json=breadcrumbs') + const breadcrumbs = await getJSON( + '/en/github/authenticating-to-github/keeping-your-account-and-data-secure?json=breadcrumbs' + ) const expected = [ { documentType: 'product', href: '/en/github', - title: 'GitHub.com' + title: 'GitHub.com', }, { documentType: 'category', href: '/en/github/authenticating-to-github', - title: 'Authentication' + title: 'Authentication', }, { documentType: 'mapTopic', href: '/en/github/authenticating-to-github/keeping-your-account-and-data-secure', - title: 'Account security' - } + title: 'Account security', + }, ] expect(breadcrumbs).toEqual(expected) }) test('works on articles that DO have maptopics ', async () => { - const breadcrumbs = await getJSON('/en/github/authenticating-to-github/creating-a-strong-password?json=breadcrumbs') + const breadcrumbs = await getJSON( + '/en/github/authenticating-to-github/creating-a-strong-password?json=breadcrumbs' + ) const expected = [ { documentType: 'product', href: '/en/github', - title: 'GitHub.com' + title: 'GitHub.com', }, { documentType: 'category', href: '/en/github/authenticating-to-github', - title: 'Authentication' + title: 'Authentication', }, { documentType: 'mapTopic', href: '/en/github/authenticating-to-github/keeping-your-account-and-data-secure', - title: 'Account security' + title: 'Account security', }, { documentType: 'article', href: '/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-strong-password', - title: 'Create a strong password' - } + title: 'Create a strong password', + }, ] expect(breadcrumbs).toEqual(expected) }) test('works on articles that DO NOT have maptopics ', async () => { - const breadcrumbs = await getJSON('/github/site-policy/github-privacy-statement?json=breadcrumbs') + const breadcrumbs = await getJSON( + '/github/site-policy/github-privacy-statement?json=breadcrumbs' + ) const expected = [ { documentType: 'product', href: '/en/github', - title: 'GitHub.com' + title: 'GitHub.com', }, { documentType: 'category', href: '/en/github/site-policy', - title: 'Site policy' + title: 'Site policy', }, { documentType: 'article', href: '/en/github/site-policy/github-privacy-statement', - title: 'GitHub Privacy Statement' - } + title: 'GitHub Privacy Statement', + }, ] expect(breadcrumbs).toEqual(expected) }) diff --git a/tests/rendering/events.js b/tests/rendering/events.js index f0cb6b39e538..36399e3d81ee 100644 --- a/tests/rendering/events.js +++ b/tests/rendering/events.js @@ -20,9 +20,7 @@ describe('POST /events', () => { const csrfRes = await agent.get('/en') const $ = cheerio.load(csrfRes.text || '', { xmlMode: true }) csrfToken = $('meta[name="csrf-token"]').attr('content') - nock('http://example.com') - .post('/hydro') - .reply(200, {}) + nock('http://example.com').post('/hydro').reply(200, {}) }) afterEach(() => { @@ -33,7 +31,7 @@ describe('POST /events', () => { csrfToken = '' }) - async function checkEvent (data, code) { + async function checkEvent(data, code) { return agent .post('/events') .send(data) @@ -68,220 +66,256 @@ describe('POST /events', () => { // Location information timezone: -7, - user_language: 'en-US' - } + user_language: 'en-US', + }, } describe('page', () => { const pageExample = { ...baseExample, type: 'page' } - it('should record a page event', () => - checkEvent(pageExample, 200) - ) + it('should record a page event', () => checkEvent(pageExample, 200)) - it('should require a type', () => - checkEvent(baseExample, 400) - ) + it('should require a type', () => checkEvent(baseExample, 400)) it('should require an event_id in uuid', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - event_id: 'asdfghjkl' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + event_id: 'asdfghjkl', + }, + }, + 400 + )) it('should require a user in uuid', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - user: 'asdfghjkl' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + user: 'asdfghjkl', + }, + }, + 400 + )) it('should require a version', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - version: undefined - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + version: undefined, + }, + }, + 400 + )) it('should require created timestamp', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - timestamp: 1234 - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + timestamp: 1234, + }, + }, + 400 + )) it('should allow page_event_id', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - page_event_id: baseExample.context.event_id - } - }, 200) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + page_event_id: baseExample.context.event_id, + }, + }, + 200 + )) it('should not allow a honeypot token', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - token: 'zxcv' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + token: 'zxcv', + }, + }, + 400 + )) it('should path be uri-reference', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - path: ' ' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + path: ' ', + }, + }, + 400 + )) it('should hostname be uri-reference', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - hostname: ' ' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + hostname: ' ', + }, + }, + 400 + )) it('should referrer be uri-reference', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - referrer: ' ' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + referrer: ' ', + }, + }, + 400 + )) it('should search a string', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - search: 1234 - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + search: 1234, + }, + }, + 400 + )) it('should href be uri', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - href: '/example' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + href: '/example', + }, + }, + 400 + )) it('should site_language is a valid option', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - site_language: 'nl' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + site_language: 'nl', + }, + }, + 400 + )) it('should os a valid os option', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - os: 'ubuntu' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + os: 'ubuntu', + }, + }, + 400 + )) it('should os_version a string', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - os_version: 25 - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + os_version: 25, + }, + }, + 400 + )) it('should browser a valid option', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - browser: 'opera' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + browser: 'opera', + }, + }, + 400 + )) it('should browser_version a string', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - browser_version: 25 - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + browser_version: 25, + }, + }, + 400 + )) it('should viewport_width a number', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - viewport_width: -500 - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + viewport_width: -500, + }, + }, + 400 + )) it('should viewport_height a number', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - viewport_height: '53px' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + viewport_height: '53px', + }, + }, + 400 + )) it('should timezone in number', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - timezone: 'GMT-0700' - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + timezone: 'GMT-0700', + }, + }, + 400 + )) it('should user_language is a string', () => - checkEvent({ - ...pageExample, - context: { - ...pageExample.context, - user_language: true - } - }, 400) - ) + checkEvent( + { + ...pageExample, + context: { + ...pageExample.context, + user_language: true, + }, + }, + 400 + )) }) describe('exit', () => { @@ -293,51 +327,44 @@ describe('POST /events', () => { exit_dom_interactive: 0.2, exit_dom_complete: 0.3, exit_visit_duration: 5, - exit_scroll_length: 0.5 + exit_scroll_length: 0.5, } - it('should record an exit event', () => - checkEvent(exitExample, 200) - ) + it('should record an exit event', () => checkEvent(exitExample, 200)) it('should exit_render_duration is a positive number', () => - checkEvent({ - ...exitExample, - exit_render_duration: -0.5 - }, 400) - ) + checkEvent( + { + ...exitExample, + exit_render_duration: -0.5, + }, + 400 + )) it('exit_first_paint is a number', () => - checkEvent({ ...exitExample, exit_first_paint: 'afjdkl' }, 400) - ) + checkEvent({ ...exitExample, exit_first_paint: 'afjdkl' }, 400)) it('exit_dom_interactive is a number', () => - checkEvent({ ...exitExample, exit_dom_interactive: '202' }, 400) - ) + checkEvent({ ...exitExample, exit_dom_interactive: '202' }, 400)) it('exit_visit_duration is a number', () => - checkEvent({ ...exitExample, exit_visit_duration: '75' }, 400) - ) + checkEvent({ ...exitExample, exit_visit_duration: '75' }, 400)) it('exit_scroll_length is a number between 0 and 1', () => - checkEvent({ ...exitExample, exit_scroll_length: 1.1 }, 400) - ) + checkEvent({ ...exitExample, exit_scroll_length: 1.1 }, 400)) }) describe('link', () => { const linkExample = { ...baseExample, type: 'link', - link_url: 'https://example.com' + link_url: 'https://example.com', } - it('should send a link event', () => - checkEvent(linkExample, 200) - ) + it('should send a link event', () => checkEvent(linkExample, 200)) it('link_url is a required uri formatted string', () => - checkEvent({ ...linkExample, link_url: 'foo' }, 400) - ) + checkEvent({ ...linkExample, link_url: 'foo' }, 400)) }) describe('search', () => { @@ -345,36 +372,29 @@ describe('POST /events', () => { ...baseExample, type: 'search', search_query: 'github private instances', - search_context: 'private' + search_context: 'private', } - it('should record a search event', () => - checkEvent(searchExample, 200) - ) + it('should record a search event', () => checkEvent(searchExample, 200)) it('search_query is required string', () => - checkEvent({ ...searchExample, search_query: undefined }, 400) - ) + checkEvent({ ...searchExample, search_query: undefined }, 400)) it('search_context is optional string', () => - checkEvent({ ...searchExample, search_context: undefined }, 200) - ) + checkEvent({ ...searchExample, search_context: undefined }, 200)) }) describe('navigate', () => { const navigateExample = { ...baseExample, type: 'navigate', - navigate_label: 'drop down' + navigate_label: 'drop down', } - it('should record a navigate event', () => - checkEvent(navigateExample, 200) - ) + it('should record a navigate event', () => checkEvent(navigateExample, 200)) it('navigate_label is optional string', () => - checkEvent({ ...navigateExample, navigate_label: undefined }, 200) - ) + checkEvent({ ...navigateExample, navigate_label: undefined }, 200)) }) describe('survey', () => { @@ -383,16 +403,13 @@ describe('POST /events', () => { type: 'survey', survey_vote: true, survey_comment: 'I love this site.', - survey_email: 'daisy@example.com' + survey_email: 'daisy@example.com', } - it('should record a survey event', () => - checkEvent(surveyExample, 200) - ) + it('should record a survey event', () => checkEvent(surveyExample, 200)) it('survey_vote is boolean', () => - checkEvent({ ...surveyExample, survey_vote: undefined }, 400) - ) + checkEvent({ ...surveyExample, survey_vote: undefined }, 400)) it('survey_comment is string', () => { checkEvent({ ...surveyExample, survey_comment: 1234 }, 400) @@ -409,24 +426,19 @@ describe('POST /events', () => { type: 'experiment', experiment_name: 'change-button-copy', experiment_variation: 'treatment', - experiment_success: true + experiment_success: true, } - it('should record an experiment event', () => - checkEvent(experimentExample, 200) - ) + it('should record an experiment event', () => checkEvent(experimentExample, 200)) it('experiment_name is required string', () => - checkEvent({ ...experimentExample, experiment_name: undefined }, 400) - ) + checkEvent({ ...experimentExample, experiment_name: undefined }, 400)) it('experiment_variation is required string', () => - checkEvent({ ...experimentExample, experiment_variation: undefined }, 400) - ) + checkEvent({ ...experimentExample, experiment_variation: undefined }, 400)) it('experiment_success is optional boolean', () => - checkEvent({ ...experimentExample, experiment_success: undefined }, 200) - ) + checkEvent({ ...experimentExample, experiment_success: undefined }, 200)) }) describe('redirect', () => { @@ -434,47 +446,38 @@ describe('POST /events', () => { ...baseExample, type: 'redirect', redirect_from: 'http://example.com/a', - redirect_to: 'http://example.com/b' + redirect_to: 'http://example.com/b', } - it('should record an redirect event', () => - checkEvent(redirectExample, 200) - ) + it('should record an redirect event', () => checkEvent(redirectExample, 200)) it('redirect_from is required url', () => - checkEvent({ ...redirectExample, redirect_from: ' ' }, 400) - ) + checkEvent({ ...redirectExample, redirect_from: ' ' }, 400)) it('redirect_to is required url', () => - checkEvent({ ...redirectExample, redirect_to: undefined }, 400) - ) + checkEvent({ ...redirectExample, redirect_to: undefined }, 400)) }) describe('clipboard', () => { const clipboardExample = { ...baseExample, type: 'clipboard', - clipboard_operation: 'copy' + clipboard_operation: 'copy', } - it('should record an clipboard event', () => - checkEvent(clipboardExample, 200) - ) + it('should record an clipboard event', () => checkEvent(clipboardExample, 200)) it('clipboard_operation is required copy, paste, cut', () => - checkEvent({ ...clipboardExample, clipboard_operation: 'destroy' }, 400) - ) + checkEvent({ ...clipboardExample, clipboard_operation: 'destroy' }, 400)) }) describe('print', () => { const printExample = { ...baseExample, - type: 'print' + type: 'print', } - it('should record a print event', () => - checkEvent(printExample, 200) - ) + it('should record a print event', () => checkEvent(printExample, 200)) }) describe('preference', () => { @@ -482,12 +485,10 @@ describe('POST /events', () => { ...baseExample, type: 'preference', preference_name: 'application', - preference_value: 'cli' + preference_value: 'cli', } - it('should record an application event', () => - checkEvent(preferenceExample, 200) - ) + it('should record an application event', () => checkEvent(preferenceExample, 200)) it('preference_name is string', () => { checkEvent({ ...preferenceExample, preference_name: null }, 400) diff --git a/tests/rendering/head.js b/tests/rendering/head.js index 71d0283292e5..f2d306dc7d51 100644 --- a/tests/rendering/head.js +++ b/tests/rendering/head.js @@ -24,8 +24,14 @@ describe('<head>', () => { const $ = await getDOM('/en/articles/about-pull-request-merges') const $description = $('meta[name="description"]') // plain text intro - expect($description.attr('content').startsWith('You can merge pull requests by retaining')).toBe(true) + expect( + $description.attr('content').startsWith('You can merge pull requests by retaining') + ).toBe(true) // HTML intro - expect($('div.lead-mktg').html().startsWith('<p>You can <a href="/articles/merging-a-pull-request">merge pull requests</a>')) + expect( + $('div.lead-mktg') + .html() + .startsWith('<p>You can <a href="/articles/merging-a-pull-request">merge pull requests</a>') + ) }) }) diff --git a/tests/rendering/header.js b/tests/rendering/header.js index 44aa9aba7a71..d5e38de98371 100644 --- a/tests/rendering/header.js +++ b/tests/rendering/header.js @@ -10,7 +10,7 @@ describe('header', () => { expect($('meta[name="site.data.ui.search.placeholder"]').length).toBe(1) }) - test('includes a link to the homepage (in the current page\'s language)', async () => { + test("includes a link to the homepage (in the current page's language)", async () => { let $ = await getDOM('/en') expect($('#github-logo a[href="/en"]').length).toBe(2) @@ -22,13 +22,19 @@ describe('header', () => { describe('language links', () => { test('lead to the same page in a different language', async () => { const $ = await getDOM('/github/administering-a-repository/managing-a-branch-protection-rule') - expect($('#languages-selector a[href="/ja/github/administering-a-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule"]').length).toBe(1) + expect( + $( + '#languages-selector a[href="/ja/github/administering-a-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule"]' + ).length + ).toBe(1) }) test('display the native name and the English name for each translated language', async () => { const $ = await getDOM('/en') expect($('#languages-selector a[href="/en"]').text().trim()).toBe('English') - expect($('#languages-selector a[href="/cn"]').text().trim()).toBe('็ฎ€ไฝ“ไธญๆ–‡ (Simplified Chinese)') + expect($('#languages-selector a[href="/cn"]').text().trim()).toBe( + '็ฎ€ไฝ“ไธญๆ–‡ (Simplified Chinese)' + ) expect($('#languages-selector a[href="/ja"]').text().trim()).toBe('ๆ—ฅๆœฌ่ชž (Japanese)') }) @@ -60,50 +66,54 @@ describe('header', () => { test('displays translation disclaimer notice on localized site-policy pages', async () => { const $ = await getDOM('/ja/github/site-policy/github-logo-policy') - expect($('.header-notifications.translation_notice a[href="https://github.com/github/site-policy/issues"]').length).toBe(1) + expect( + $( + '.header-notifications.translation_notice a[href="https://github.com/github/site-policy/issues"]' + ).length + ).toBe(1) }) - test('renders a link to the same page in user\'s preferred language, if available', async () => { + test("renders a link to the same page in user's preferred language, if available", async () => { const headers = { 'accept-language': 'ja' } const $ = await getDOM('/en', headers) expect($('.header-notifications.translation_notice').length).toBe(1) expect($('.header-notifications a[href*="/ja"]').length).toBe(1) }) - test('renders a link to the same page if user\'s preferred language is Chinese - PRC', async () => { + test("renders a link to the same page if user's preferred language is Chinese - PRC", async () => { const headers = { 'accept-language': 'zh-CN' } const $ = await getDOM('/en', headers) expect($('.header-notifications.translation_notice').length).toBe(1) expect($('.header-notifications a[href*="/cn"]').length).toBe(1) }) - test('does not render a link when user\'s preferred language is Chinese - Taiwan', async () => { + test("does not render a link when user's preferred language is Chinese - Taiwan", async () => { const headers = { 'accept-language': 'zh-TW' } const $ = await getDOM('/en', headers) expect($('.header-notifications').length).toBe(0) }) - test('does not render a link when user\'s preferred language is English', async () => { + test("does not render a link when user's preferred language is English", async () => { const headers = { 'accept-language': 'en' } const $ = await getDOM('/en', headers) expect($('.header-notifications').length).toBe(0) }) - test('renders a link to the same page in user\'s preferred language from multiple, if available', async () => { + test("renders a link to the same page in user's preferred language from multiple, if available", async () => { const headers = { 'accept-language': 'ja, *;q=0.9' } const $ = await getDOM('/en', headers) expect($('.header-notifications.translation_notice').length).toBe(1) expect($('.header-notifications a[href*="/ja"]').length).toBe(1) }) - test('renders a link to the same page in user\'s preferred language with weights, if available', async () => { + test("renders a link to the same page in user's preferred language with weights, if available", async () => { const headers = { 'accept-language': 'ja;q=1.0, *;q=0.9' } const $ = await getDOM('/en', headers) expect($('.header-notifications.translation_notice').length).toBe(1) expect($('.header-notifications a[href*="/ja"]').length).toBe(1) }) - test('renders a link to the user\'s 2nd preferred language if 1st is not available', async () => { + test("renders a link to the user's 2nd preferred language if 1st is not available", async () => { const headers = { 'accept-language': 'zh-TW,zh;q=0.9,ja *;q=0.8' } const $ = await getDOM('/en', headers) expect($('.header-notifications.translation_notice').length).toBe(1) @@ -131,14 +141,18 @@ describe('header', () => { expect(ghe.attr('class').includes('active')).toBe(false) }) - test('point to homepages in the current page\'s language', async () => { - const $ = await getDOM('/ja/github/administering-a-repository/defining-the-mergeability-of-pull-requests') + test("point to homepages in the current page's language", async () => { + const $ = await getDOM( + '/ja/github/administering-a-repository/defining-the-mergeability-of-pull-requests' + ) expect($('#homepages a.active[href="/ja/github"]').length).toBe(1) expect($(`#homepages a[href="/ja/enterprise-server@${latest}/admin"]`).length).toBe(1) }) test('emphasizes the product that corresponds to the current page', async () => { - const $ = await getDOM(`/en/enterprise/${oldestSupported}/user/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address`) + const $ = await getDOM( + `/en/enterprise/${oldestSupported}/user/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address` + ) expect($(`#homepages a.active[href="/en/enterprise-server@${latest}/admin"]`).length).toBe(0) expect($('#homepages a[href="/en/github"]').length).toBe(1) expect($('#homepages a.active[href="/en/github"]').length).toBe(1) diff --git a/tests/rendering/learning-tracks.js b/tests/rendering/learning-tracks.js index 14997bb94278..11de87e2722d 100644 --- a/tests/rendering/learning-tracks.js +++ b/tests/rendering/learning-tracks.js @@ -28,16 +28,20 @@ describe('learning tracks', () => { const trackName = found[1] // check all the links contain track name - $(trackElem).find('a.Box-row').each((i, elem) => { - expect($(elem).attr('href')).toEqual(expect.stringContaining(`?learn=${trackName}`)) - }) + $(trackElem) + .find('a.Box-row') + .each((i, elem) => { + expect($(elem).attr('href')).toEqual(expect.stringContaining(`?learn=${trackName}`)) + }) }) }) }) describe('navigation banner', () => { test('render navigation banner when url includes correct learning track name', async () => { - const $ = await getDOM('/en/actions/guides/setting-up-continuous-integration-using-workflow-templates?learn=continuous_integration') + const $ = await getDOM( + '/en/actions/guides/setting-up-continuous-integration-using-workflow-templates?learn=continuous_integration' + ) expect($('.learning-track-nav')).toHaveLength(1) const $navLinks = $('.learning-track-nav a') expect($navLinks).toHaveLength(2) @@ -47,17 +51,23 @@ describe('navigation banner', () => { }) test('does not include banner when url does not include `learn` param', async () => { - const $ = await getDOM('/en/actions/guides/setting-up-continuous-integration-using-workflow-templates') + const $ = await getDOM( + '/en/actions/guides/setting-up-continuous-integration-using-workflow-templates' + ) expect($('.learning-track-nav')).toHaveLength(0) }) test('does not include banner when url has incorrect `learn` param', async () => { - const $ = await getDOM('/en/actions/guides/setting-up-continuous-integration-using-workflow-templates?learn=not_real') + const $ = await getDOM( + '/en/actions/guides/setting-up-continuous-integration-using-workflow-templates?learn=not_real' + ) expect($('.learning-track-nav')).toHaveLength(0) }) test('does not include banner when url is not part of the learning track', async () => { - const $ = await getDOM('/en/actions/learn-github-actions/introduction-to-github-actions?learn=continuous_integration') + const $ = await getDOM( + '/en/actions/learn-github-actions/introduction-to-github-actions?learn=continuous_integration' + ) expect($('.learning-track-nav')).toHaveLength(0) }) }) diff --git a/tests/rendering/octicon.js b/tests/rendering/octicon.js index 1f432593bc9c..9ae70e8c778d 100644 --- a/tests/rendering/octicon.js +++ b/tests/rendering/octicon.js @@ -30,12 +30,14 @@ describe('octicon tag', () => { }) it('throws an error with invalid syntax', async () => { - await expect(renderContent('{% octicon 123 %}')).rejects - .toThrowError('Syntax Error in tag \'octicon\' - Valid syntax: octicon "<name>" <key="value">') + await expect(renderContent('{% octicon 123 %}')).rejects.toThrowError( + 'Syntax Error in tag \'octicon\' - Valid syntax: octicon "<name>" <key="value">' + ) }) it('throws an error with a non-existant octicon', async () => { - await expect(renderContent('{% octicon "pizza-patrol" %}')).rejects - .toThrowError('Octicon pizza-patrol does not exist') + await expect(renderContent('{% octicon "pizza-patrol" %}')).rejects.toThrowError( + 'Octicon pizza-patrol does not exist' + ) }) }) diff --git a/tests/rendering/page-titles.js b/tests/rendering/page-titles.js index 4c5621ae744e..ae0082da8230 100644 --- a/tests/rendering/page-titles.js +++ b/tests/rendering/page-titles.js @@ -16,7 +16,9 @@ describe('page titles', () => { }) test('enterprise English article', async () => { - const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/user/github/authenticating-to-github/authorizing-oauth-apps`) + const $ = await getDOM( + `/en/enterprise/${enterpriseServerReleases.latest}/user/github/authenticating-to-github/authorizing-oauth-apps` + ) expect($('title').text()).toBe('Authorizing OAuth Apps - GitHub Docs') }) diff --git a/tests/rendering/rest.js b/tests/rendering/rest.js index f54850e9fb46..8df4e75fe585 100644 --- a/tests/rendering/rest.js +++ b/tests/rendering/rest.js @@ -14,7 +14,7 @@ const fs = xFs.promises const excludeFromResourceNameCheck = [ 'endpoints-available-for-github-apps.md', 'permissions-required-for-github-apps.md', - 'index.md' + 'index.md', ] describe('REST references docs', () => { @@ -24,13 +24,18 @@ describe('REST references docs', () => { const { categories } = rest const referenceDir = path.join(__dirname, '../../content/rest/reference') const filenames = (await fs.readdir(referenceDir)) - .filter(filename => !excludeFromResourceNameCheck.find(excludedFile => filename.endsWith(excludedFile))) - .map(filename => filename.replace('.md', '')) + .filter( + (filename) => + !excludeFromResourceNameCheck.find((excludedFile) => filename.endsWith(excludedFile)) + ) + .map((filename) => filename.replace('.md', '')) - const missingResource = 'Found a markdown file in content/rest/reference that is not represented by an OpenAPI REST operation category.' + const missingResource = + 'Found a markdown file in content/rest/reference that is not represented by an OpenAPI REST operation category.' expect(difference(filenames, categories), missingResource).toEqual([]) - const missingFile = 'Found an OpenAPI REST operation category that is not represented by a markdown file in content/rest/reference.' + const missingFile = + 'Found an OpenAPI REST operation category that is not represented by a markdown file in content/rest/reference.' expect(difference(categories, filenames), missingFile).toEqual([]) }) @@ -40,14 +45,18 @@ describe('REST references docs', () => { }) test('loads Enterprise OpenAPI schema data', async () => { - const operations = await getJSON(`/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/rest/reference/emojis?json=currentRestOperations`) - const operation = operations.find(operation => operation.operationId === 'emojis/get') + const operations = await getJSON( + `/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/rest/reference/emojis?json=currentRestOperations` + ) + const operation = operations.find((operation) => operation.operationId === 'emojis/get') expect(isPlainObject(operation)).toBe(true) expect(operation.description).toContain('GitHub Enterprise') }) test('loads operations enabled for GitHub Apps', async () => { - const operations = await getJSON('/en/free-pro-team@latest/rest/overview/endpoints-available-for-github-apps?json=rest.operationsEnabledForGitHubApps') + const operations = await getJSON( + '/en/free-pro-team@latest/rest/overview/endpoints-available-for-github-apps?json=rest.operationsEnabledForGitHubApps' + ) expect(operations['free-pro-team@latest'].actions.length).toBeGreaterThan(0) expect(operations['enterprise-server@2.22'].actions.length).toBeGreaterThan(0) }) diff --git a/tests/rendering/robots-txt.js b/tests/rendering/robots-txt.js index ca57b97f5116..d7319d663f91 100644 --- a/tests/rendering/robots-txt.js +++ b/tests/rendering/robots-txt.js @@ -17,25 +17,33 @@ describe('robots.txt', () => { it('allows indexing of the homepage and English content', async () => { expect(robots.isAllowed('https://docs.github.com/')).toBe(true) expect(robots.isAllowed('https://docs.github.com/en')).toBe(true) - expect(robots.isAllowed('https://docs.github.com/en/articles/verifying-your-email-address')).toBe(true) + expect( + robots.isAllowed('https://docs.github.com/en/articles/verifying-your-email-address') + ).toBe(true) }) it('allows indexing of generally available localized content', async () => { Object.values(languages) - .filter(language => !language.wip) - .forEach(language => { + .filter((language) => !language.wip) + .forEach((language) => { expect(robots.isAllowed(`https://docs.github.com/${language.code}`)).toBe(true) - expect(robots.isAllowed(`https://docs.github.com/${language.code}/articles/verifying-your-email-address`)).toBe(true) + expect( + robots.isAllowed( + `https://docs.github.com/${language.code}/articles/verifying-your-email-address` + ) + ).toBe(true) }) }) it('disallows indexing of herokuapp.com domains', async () => { const req = { hostname: 'docs-internal-12345--my-branch.herokuapp.com', - path: '/robots.txt' + path: '/robots.txt', } const res = new MockExpressResponse() - const next = () => { /* no op */ } + const next = () => { + /* no op */ + } await robotsMiddleware(req, res, next) expect(res._getString()).toEqual('User-agent: *\nDisallow: /') diff --git a/tests/rendering/server.js b/tests/rendering/server.js index 3c9585880e11..7ed15a4bf7ee 100644 --- a/tests/rendering/server.js +++ b/tests/rendering/server.js @@ -10,7 +10,9 @@ import { productMap } from '../../lib/all-products.js' import { jest } from '@jest/globals' const AZURE_STORAGE_URL = 'githubdocs.azureedge.net' -const activeProducts = Object.values(productMap).filter(product => !product.wip && !product.hidden) +const activeProducts = Object.values(productMap).filter( + (product) => !product.wip && !product.hidden +) jest.useFakeTimers() @@ -39,11 +41,13 @@ describe('server', () => { test('renders the homepage with links to exptected products in both the sidebar and page body', async () => { const $ = await getDOM('/en') const sidebarItems = $('.sidebar-products li a').get() - const sidebarTitles = sidebarItems.map(el => $(el).text().trim()) - const sidebarHrefs = sidebarItems.map(el => $(el).attr('href')) + const sidebarTitles = sidebarItems.map((el) => $(el).text().trim()) + const sidebarHrefs = sidebarItems.map((el) => $(el).attr('href')) - const productTitles = activeProducts.map(prod => prod.name) - const productHrefs = activeProducts.map(prod => prod.external ? prod.href : `/en${prod.href}`) + const productTitles = activeProducts.map((prod) => prod.name) + const productHrefs = activeProducts.map((prod) => + prod.external ? prod.href : `/en${prod.href}` + ) const titlesInSidebarButNotProducts = lodash.difference(sidebarTitles, productTitles) const titlesInProductsButNotSidebar = lodash.difference(productTitles, sidebarTitles) @@ -51,23 +55,47 @@ describe('server', () => { const hrefsInSidebarButNotProducts = lodash.difference(sidebarHrefs, productHrefs) const hrefsInProductsButNotSidebar = lodash.difference(productHrefs, sidebarHrefs) - expect(titlesInSidebarButNotProducts.length, `Found unexpected titles in sidebar: ${titlesInSidebarButNotProducts.join(', ')}`).toBe(0) - expect(titlesInProductsButNotSidebar.length, `Found titles missing from sidebar: ${titlesInProductsButNotSidebar.join(', ')}`).toBe(0) - expect(hrefsInSidebarButNotProducts.length, `Found unexpected hrefs in sidebar: ${hrefsInSidebarButNotProducts.join(', ')}`).toBe(0) - expect(hrefsInProductsButNotSidebar.length, `Found hrefs missing from sidebar: ${hrefsInProductsButNotSidebar.join(', ')}`).toBe(0) + expect( + titlesInSidebarButNotProducts.length, + `Found unexpected titles in sidebar: ${titlesInSidebarButNotProducts.join(', ')}` + ).toBe(0) + expect( + titlesInProductsButNotSidebar.length, + `Found titles missing from sidebar: ${titlesInProductsButNotSidebar.join(', ')}` + ).toBe(0) + expect( + hrefsInSidebarButNotProducts.length, + `Found unexpected hrefs in sidebar: ${hrefsInSidebarButNotProducts.join(', ')}` + ).toBe(0) + expect( + hrefsInProductsButNotSidebar.length, + `Found hrefs missing from sidebar: ${hrefsInProductsButNotSidebar.join(', ')}` + ).toBe(0) }) test('renders the Enterprise homepage with links to exptected products in both the sidebar and page body', async () => { const $ = await getDOM(`/en/enterprise-server@${enterpriseServerReleases.latest}`) const sidebarItems = $('.sidebar-products li a').get() - const sidebarTitles = sidebarItems.map(el => $(el).text().trim()) - const sidebarHrefs = sidebarItems.map(el => $(el).attr('href')) - - const ghesProducts = activeProducts - .filter(prod => prod.versions && prod.versions.includes(`enterprise-server@${enterpriseServerReleases.latest}`) || prod.external) - - const ghesProductTitles = ghesProducts.map(prod => prod.name) - const ghesProductHrefs = ghesProducts.map(prod => prod.external ? prod.href : `/en${prod.href.includes('enterprise-server') ? prod.href : `/enterprise-server@${enterpriseServerReleases.latest}${prod.href}`}`) + const sidebarTitles = sidebarItems.map((el) => $(el).text().trim()) + const sidebarHrefs = sidebarItems.map((el) => $(el).attr('href')) + + const ghesProducts = activeProducts.filter( + (prod) => + (prod.versions && + prod.versions.includes(`enterprise-server@${enterpriseServerReleases.latest}`)) || + prod.external + ) + + const ghesProductTitles = ghesProducts.map((prod) => prod.name) + const ghesProductHrefs = ghesProducts.map((prod) => + prod.external + ? prod.href + : `/en${ + prod.href.includes('enterprise-server') + ? prod.href + : `/enterprise-server@${enterpriseServerReleases.latest}${prod.href}` + }` + ) const firstSidebarTitle = sidebarTitles.shift() const firstSidebarHref = sidebarHrefs.shift() @@ -80,13 +108,24 @@ describe('server', () => { expect(firstSidebarTitle).toBe('All products') expect(firstSidebarHref).toBe('/en') - expect(titlesInSidebarButNotProducts.length, `Found unexpected titles in sidebar: ${titlesInSidebarButNotProducts.join(', ')}`).toBe(0) - expect(titlesInProductsButNotSidebar.length, `Found titles missing from sidebar: ${titlesInProductsButNotSidebar.join(', ')}`).toBe(0) - expect(hrefsInSidebarButNotProducts.length, `Found unexpected hrefs in sidebar: ${hrefsInSidebarButNotProducts.join(', ')}`).toBe(0) - expect(hrefsInProductsButNotSidebar.length, `Found hrefs missing from sidebar: ${hrefsInProductsButNotSidebar.join(', ')}`).toBe(0) + expect( + titlesInSidebarButNotProducts.length, + `Found unexpected titles in sidebar: ${titlesInSidebarButNotProducts.join(', ')}` + ).toBe(0) + expect( + titlesInProductsButNotSidebar.length, + `Found titles missing from sidebar: ${titlesInProductsButNotSidebar.join(', ')}` + ).toBe(0) + expect( + hrefsInSidebarButNotProducts.length, + `Found unexpected hrefs in sidebar: ${hrefsInSidebarButNotProducts.join(', ')}` + ).toBe(0) + expect( + hrefsInProductsButNotSidebar.length, + `Found hrefs missing from sidebar: ${hrefsInProductsButNotSidebar.join(', ')}` + ).toBe(0) }) - test('uses gzip compression', async () => { const res = await get('/en') expect(res.headers['content-encoding']).toBe('gzip') @@ -163,15 +202,15 @@ describe('server', () => { const $ = await getDOM('/_500') expect($('h1').text()).toBe('Ooops!') expect($('code').text().startsWith('Error: Intentional error')).toBe(true) - expect($('code').text().includes(path.join('node_modules', 'express', 'lib', 'router'))).toBe(true) + expect($('code').text().includes(path.join('node_modules', 'express', 'lib', 'router'))).toBe( + true + ) expect($.text().includes('Still need help?')).toBe(true) expect($.res.statusCode).toBe(500) }) test('returns a 400 when POST-ed invalid JSON', async () => { - const res = await post('/') - .send('not real JSON') - .set('Content-Type', 'application/json') + const res = await post('/').send('not real JSON').set('Content-Type', 'application/json') expect(res.statusCode).toBe(400) }) @@ -191,15 +230,21 @@ describe('server', () => { test('injects site variables into rendered permissions statements frontmatter', async () => { // markdown source: {% data variables.product.prodname_pages %} site - const $ = await getDOM('/en/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site') + const $ = await getDOM( + '/en/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site' + ) expect($('div.permissions-statement').text()).toContain('GitHub Pages site') }) // see issue 9678 test('does not use cached intros in map topics', async () => { - let $ = await getDOM('/en/github/importing-your-projects-to-github/importing-a-git-repository-using-the-command-line') + let $ = await getDOM( + '/en/github/importing-your-projects-to-github/importing-a-git-repository-using-the-command-line' + ) const articleIntro = $('.lead-mktg').text() - $ = await getDOM('/en/enterprise/2.16/user/importing-your-projects-to-github/importing-source-code-to-github') + $ = await getDOM( + '/en/enterprise/2.16/user/importing-your-projects-to-github/importing-source-code-to-github' + ) const mapTopicIntro = $('.map-topic').first().next().text() expect(articleIntro).not.toEqual(mapTopicIntro) }) @@ -220,7 +265,7 @@ describe('server', () => { const categories = JSON.parse(res.text) expect(Array.isArray(categories)).toBe(true) expect(categories.length).toBeGreaterThan(1) - categories.forEach(category => { + categories.forEach((category) => { expect('name' in category).toBe(true) expect('published_articles' in category).toBe(true) }) @@ -229,19 +274,25 @@ describe('server', () => { test('renders Markdown links that have Liquid hrefs', async () => { // example from markdown source: // 1. Go to {{ site.data.variables.product.product_name }}'s [Pricing]({{ site.data.variables.dotcom_billing.plans_url }}) page. - const $ = await getDOM('/en/github/getting-started-with-github/signing-up-for-a-new-github-account') + const $ = await getDOM( + '/en/github/getting-started-with-github/signing-up-for-a-new-github-account' + ) expect($.text()).toContain("Go to GitHub's Pricing page.") expect($('a[href="https://github.com/pricing"]').first().text()).toBe('Pricing') }) test('renders liquid within liquid within liquid in body text', async () => { const $ = await getDOM('/en/github/administering-a-repository/enabling-required-status-checks') - expect($('ol li').first().text().trim()).toBe('On GitHub, navigate to the main page of the repository.') + expect($('ol li').first().text().trim()).toBe( + 'On GitHub, navigate to the main page of the repository.' + ) }) test('renders liquid within liquid within liquid in intros', async () => { const $ = await getDOM('/en/github/administering-a-repository/about-merge-methods-on-github') - expect($('div.lead-mktg').first().text().includes('merge their pull requests on GitHub')).toBe(true) + expect($('div.lead-mktg').first().text().includes('merge their pull requests on GitHub')).toBe( + true + ) }) test('renders product frontmatter callouts', async () => { @@ -254,7 +305,13 @@ describe('server', () => { test('renders liquid within liquid within product frontmatter callouts', async () => { const $ = await getDOM('/en/articles/about-branch-restrictions') const note = $('.product-callout').eq(0) - expect(note.first().text().trim().startsWith('Protected branches are available in public repositories with GitHub Free')).toBe(true) + expect( + note + .first() + .text() + .trim() + .startsWith('Protected branches are available in public repositories with GitHub Free') + ).toBe(true) }) test('handles whitespace control in liquid tags', async () => { @@ -267,11 +324,15 @@ describe('server', () => { test('renders liquid within liquid within liquid', async () => { const $ = await getDOM('/en/articles/enabling-required-status-checks') - expect($('ol li').first().text().trim()).toBe('On GitHub, navigate to the main page of the repository.') + expect($('ol li').first().text().trim()).toBe( + 'On GitHub, navigate to the main page of the repository.' + ) }) test('preserves liquid statements with liquid raw tags in page output', async () => { - const $ = await getDOM('/en/pages/setting-up-a-github-pages-site-with-jekyll/troubleshooting-jekyll-build-errors-for-github-pages-sites') + const $ = await getDOM( + '/en/pages/setting-up-a-github-pages-site-with-jekyll/troubleshooting-jekyll-build-errors-for-github-pages-sites' + ) expect($.text().includes('{{ page.title }}')).toBe(true) }) @@ -289,14 +350,18 @@ describe('server', () => { }) test('renders mini TOC in articles that includes h4s when specified by frontmatter', async () => { - const $ = await getDOM('/en/github/setting-up-and-managing-your-enterprise/enforcing-security-settings-in-your-enterprise-account') + const $ = await getDOM( + '/en/github/setting-up-and-managing-your-enterprise/enforcing-security-settings-in-your-enterprise-account' + ) expect($('h2#in-this-article').length).toBe(1) expect($('h2#in-this-article + ul li.ml-0').length).toBeGreaterThan(0) // non-indented items expect($('h2#in-this-article + ul li.ml-3').length).toBeGreaterThan(0) // indented items }) test('does not render mini TOC in articles with only one heading', async () => { - const $ = await getDOM('/en/github/visualizing-repository-data-with-graphs/about-repository-graphs') + const $ = await getDOM( + '/en/github/visualizing-repository-data-with-graphs/about-repository-graphs' + ) expect($('h2#in-this-article').length).toBe(0) }) @@ -311,12 +376,16 @@ describe('server', () => { }) test('renders mini TOC with correct links when headings contain markup', async () => { - const $ = await getDOM('/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates') + const $ = await getDOM( + '/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates' + ) expect($('h2#in-this-article + ul li a[href="#package-ecosystem"]').length).toBe(1) }) test('renders mini TOC with correct links when headings contain markup in localized content', async () => { - const $ = await getDOM('/ja/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates') + const $ = await getDOM( + '/ja/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates' + ) expect($('h2#in-this-article + ul li a[href="#package-ecosystem"]').length).toBe(1) }) }) @@ -328,37 +397,57 @@ describe('server', () => { const oldestEnterprisePath = `/en/enterprise/${enterpriseServerReleases.oldestSupported}` test('github articles on dotcom have images that point to local assets dir', async () => { - const $ = await getDOM('/en/github/authenticating-to-github/configuring-two-factor-authentication') + const $ = await getDOM( + '/en/github/authenticating-to-github/configuring-two-factor-authentication' + ) expect($('img').first().attr('src').startsWith(localImageBasePath)).toBe(true) }) test('github articles on GHE have images that point to local assets dir', async () => { - const $ = await getDOM(`${latestEnterprisePath}/user/github/authenticating-to-github/configuring-two-factor-authentication`) + const $ = await getDOM( + `${latestEnterprisePath}/user/github/authenticating-to-github/configuring-two-factor-authentication` + ) const imageSrc = $('img').first().attr('src') - expect(imageSrc.startsWith(localImageBasePath) || imageSrc.startsWith(legacyImageBasePath)).toBe(true) + expect( + imageSrc.startsWith(localImageBasePath) || imageSrc.startsWith(legacyImageBasePath) + ).toBe(true) }) test('admin articles on latest version of GHE have images that point to local assets dir', async () => { - const $ = await getDOM(`${latestEnterprisePath}/admin/user-management/using-built-in-authentication`) + const $ = await getDOM( + `${latestEnterprisePath}/admin/user-management/using-built-in-authentication` + ) const imageSrc = $('img').first().attr('src') - expect(imageSrc.startsWith(localImageBasePath) || imageSrc.startsWith(legacyImageBasePath)).toBe(true) + expect( + imageSrc.startsWith(localImageBasePath) || imageSrc.startsWith(legacyImageBasePath) + ).toBe(true) }) test('admin articles on older GHE versions have images that point to local assets dir', async () => { - const $ = await getDOM(`${oldestEnterprisePath}/admin/user-management/using-built-in-authentication`) + const $ = await getDOM( + `${oldestEnterprisePath}/admin/user-management/using-built-in-authentication` + ) const imageSrc = $('img').first().attr('src') - expect(imageSrc.startsWith(localImageBasePath) || imageSrc.startsWith(legacyImageBasePath)).toBe(true) + expect( + imageSrc.startsWith(localImageBasePath) || imageSrc.startsWith(legacyImageBasePath) + ).toBe(true) }) test('links that point to /assets are not rewritten with a language code', async () => { const $ = await getDOM('/en/github/site-policy/github-privacy-statement') - expect($('#french').next().children('a').attr('href').startsWith(localImageBasePath)).toBe(true) + expect($('#french').next().children('a').attr('href').startsWith(localImageBasePath)).toBe( + true + ) }) test('github articles on GHAE have images that point to local assets dir', async () => { - const $ = await getDOM('/en/github-ae@latest/github/administering-a-repository/changing-the-default-branch') + const $ = await getDOM( + '/en/github-ae@latest/github/administering-a-repository/changing-the-default-branch' + ) const imageSrc = $('img').first().attr('src') - expect(imageSrc.startsWith(localImageBasePath) || imageSrc.startsWith(legacyImageBasePath)).toBe(true) + expect( + imageSrc.startsWith(localImageBasePath) || imageSrc.startsWith(legacyImageBasePath) + ).toBe(true) }) test('admin articles on GHAE have images that point to local assets dir', async () => { @@ -377,54 +466,95 @@ describe('server', () => { test('dotcom articles on dotcom have Enterprise Admin links with latest GHE version', async () => { const $ = await getDOM('/en/articles/setting-up-a-trial-of-github-enterprise-server') - expect($(`a[href="${latestEnterprisePath}/admin/installation/setting-up-a-github-enterprise-server-instance"]`).length).toBe(2) + expect( + $( + `a[href="${latestEnterprisePath}/admin/installation/setting-up-a-github-enterprise-server-instance"]` + ).length + ).toBe(2) }) test('dotcom articles on GHE have Enterprise user links', async () => { - const $ = await getDOM(`${latestEnterprisePath}/github/getting-started-with-github/set-up-git`) - expect($(`a[href="${latestEnterprisePath}/articles/managing-files-on-github"]`).length).toBe(1) + const $ = await getDOM( + `${latestEnterprisePath}/github/getting-started-with-github/set-up-git` + ) + expect($(`a[href="${latestEnterprisePath}/articles/managing-files-on-github"]`).length).toBe( + 1 + ) }) test('dotcom categories on GHE have Enterprise user links', async () => { const $ = await getDOM(`${latestEnterprisePath}/github/managing-large-files`) - expect($(`article a[href="${latestEnterprisePath}/github/managing-large-files/working-with-large-files/conditions-for-large-files"]`).length).toBe(1) + expect( + $( + `article a[href="${latestEnterprisePath}/github/managing-large-files/working-with-large-files/conditions-for-large-files"]` + ).length + ).toBe(1) }) test('dotcom-only links on GHE are dotcom-only', async () => { - const $ = await getDOM(`${latestEnterprisePath}/github/setting-up-and-managing-your-github-profile/sending-your-github-enterprise-server-contributions-to-your-githubcom-profile`) + const $ = await getDOM( + `${latestEnterprisePath}/github/setting-up-and-managing-your-github-profile/sending-your-github-enterprise-server-contributions-to-your-githubcom-profile` + ) expect($('article a[href="/en/articles/github-privacy-statement"]').length).toBe(1) }) test('desktop links on GHE are dotcom-only', async () => { - const $ = await getDOM(`${latestEnterprisePath}/github/getting-started-with-github/set-up-git`) - expect($('article a[href="/en/desktop/installing-and-configuring-github-desktop"]').length).toBe(1) + const $ = await getDOM( + `${latestEnterprisePath}/github/getting-started-with-github/set-up-git` + ) + expect( + $('article a[href="/en/desktop/installing-and-configuring-github-desktop"]').length + ).toBe(1) }) test('admin articles that link to non-admin articles have Enterprise user links', async () => { - const $ = await getDOM(`${latestEnterprisePath}/admin/installation/configuring-the-default-visibility-of-new-repositories-on-your-appliance`) - expect($(`article a[href="${latestEnterprisePath}/github/creating-cloning-and-archiving-repositories/about-repository-visibility"]`).length).toBeGreaterThan(0) + const $ = await getDOM( + `${latestEnterprisePath}/admin/installation/configuring-the-default-visibility-of-new-repositories-on-your-appliance` + ) + expect( + $( + `article a[href="${latestEnterprisePath}/github/creating-cloning-and-archiving-repositories/about-repository-visibility"]` + ).length + ).toBeGreaterThan(0) }) test('admin articles that link to Enterprise user articles have Enterprise user links', async () => { - const $ = await getDOM(`${latestEnterprisePath}/admin/user-management/customizing-user-messages-for-your-enterprise`) + const $ = await getDOM( + `${latestEnterprisePath}/admin/user-management/customizing-user-messages-for-your-enterprise` + ) expect($('article a[href*="about-writing-and-formatting-on-github"]').length).toBe(1) }) test('articles that link to external links that contain /articles/ are not rewritten', async () => { - const $ = await getDOM(`${latestEnterprisePath}/admin/installation/upgrading-github-enterprise-server`) - expect($('article a[href="https://docs.microsoft.com/azure/backup/backup-azure-vms-first-look-arm"]').length).toBe(1) + const $ = await getDOM( + `${latestEnterprisePath}/admin/installation/upgrading-github-enterprise-server` + ) + expect( + $( + 'article a[href="https://docs.microsoft.com/azure/backup/backup-azure-vms-first-look-arm"]' + ).length + ).toBe(1) }) }) describe('article versions', () => { test('includes links to all versions of each article', async () => { - const articlePath = 'github/setting-up-and-managing-your-github-user-account/managing-user-account-settings/about-your-personal-dashboard' - const $ = await getDOM(`/en/enterprise-server@${enterpriseServerReleases.latest}/${articlePath}`) - expect($(`.article-versions a.active[href="/en/enterprise-server@${enterpriseServerReleases.latest}/${articlePath}"]`).length).toBe(2) + const articlePath = + 'github/setting-up-and-managing-your-github-user-account/managing-user-account-settings/about-your-personal-dashboard' + const $ = await getDOM( + `/en/enterprise-server@${enterpriseServerReleases.latest}/${articlePath}` + ) + expect( + $( + `.article-versions a.active[href="/en/enterprise-server@${enterpriseServerReleases.latest}/${articlePath}"]` + ).length + ).toBe(2) expect($(`.article-versions a.active[href="/en/${articlePath}"]`).length).toBe(0) // 2.13 predates this feature, so it should be excluded: - expect($(`.article-versions a[href="/en/enterprise/2.13/user/${articlePath}"]`).length).toBe(0) + expect($(`.article-versions a[href="/en/enterprise/2.13/user/${articlePath}"]`).length).toBe( + 0 + ) }) test('is not displayed if article has only one version', async () => { @@ -438,10 +568,12 @@ describe('server', () => { beforeAll(async () => { const $ = await getDOM('/early-access') - hiddenPageHrefs = $('#article-contents ul > li > a').map((i, el) => $(el).attr('href')).get() + hiddenPageHrefs = $('#article-contents ul > li > a') + .map((i, el) => $(el).attr('href')) + .get() const allPages = await loadPages() - hiddenPages = allPages.filter(page => page.languageCode === 'en' && page.hidden) + hiddenPages = allPages.filter((page) => page.languageCode === 'en' && page.hidden) }) test('exist in the set of English pages', async () => { @@ -479,7 +611,9 @@ describe('server', () => { test('redirects old articles to their slugified URL', async () => { const res = await get('/articles/about-github-s-ip-addresses') - expect(res.text).toBe('Moved Permanently. Redirecting to /en/github/authenticating-to-github/keeping-your-account-and-data-secure/about-githubs-ip-addresses') + expect(res.text).toBe( + 'Moved Permanently. Redirecting to /en/github/authenticating-to-github/keeping-your-account-and-data-secure/about-githubs-ip-addresses' + ) }) test('redirects / to /en', async () => { @@ -501,9 +635,13 @@ describe('server', () => { }) test('redirects /insights/foo paths to /enterprise/user/insights/foo', async () => { - const res = await get('/en/insights/installing-and-configuring-github-insights/about-github-insights') + const res = await get( + '/en/insights/installing-and-configuring-github-insights/about-github-insights' + ) expect(res.statusCode).toBe(301) - expect(res.headers.location).toBe(`/en/enterprise-server@${enterpriseServerReleases.latest}/insights/installing-and-configuring-github-insights/installing-and-updating-github-insights/about-github-insights`) + expect(res.headers.location).toBe( + `/en/enterprise-server@${enterpriseServerReleases.latest}/insights/installing-and-configuring-github-insights/installing-and-updating-github-insights/about-github-insights` + ) }) // this oneoff redirect is temporarily disabled because it introduces too much complexity @@ -511,7 +649,9 @@ describe('server', () => { test.skip('redirects versioned category page', async () => { const res = await get('/en/github/receiving-notifications-about-activity-on-github') expect(res.statusCode).toBe(301) - expect(res.headers.location).toBe('/en/github/managing-subscriptions-and-notifications-on-github') + expect(res.headers.location).toBe( + '/en/github/managing-subscriptions-and-notifications-on-github' + ) }) }) @@ -524,7 +664,11 @@ describe('server', () => { test('adds links to map topics on a category homepage', async () => { const $ = await getDOM('/en/github/setting-up-and-managing-your-github-user-account') - expect($('article a[href="/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings"]').length).toBe(1) + expect( + $( + 'article a[href="/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings"]' + ).length + ).toBe(1) expect($('article a[href="#managing-user-account-settings"]').length).toBe(0) }) @@ -534,12 +678,20 @@ describe('server', () => { }) test('map topic renders with h2 links to articles', async () => { - const $ = await getDOM('/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings') - expect($('a[href="/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings/changing-your-github-username"] h2').length).toBe(1) + const $ = await getDOM( + '/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings' + ) + expect( + $( + 'a[href="/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings/changing-your-github-username"] h2' + ).length + ).toBe(1) }) test('map topic renders with one intro for every h2', async () => { - const $ = await getDOM('/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings') + const $ = await getDOM( + '/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings' + ) const $h2s = $('article a.link-with-intro') expect($h2s.length).toBeGreaterThan(3) $h2s.each((i, el) => { @@ -548,8 +700,12 @@ describe('server', () => { }) test('map topic intros are parsed', async () => { - const $ = await getDOM('/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings') - const $intro = $('a.link-with-intro[href*="what-does-the-available-for-hire-checkbox-do"] + p') + const $ = await getDOM( + '/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings' + ) + const $intro = $( + 'a.link-with-intro[href*="what-does-the-available-for-hire-checkbox-do"] + p' + ) expect($intro.length).toBe(1) expect($intro.html()).toContain('Use the <strong>Available for hire</strong>') }) @@ -559,7 +715,9 @@ describe('server', () => { describe('URLs by language', () => { // TODO re-enable this test once TOCs are auto-generated (after PR 11731 has landed) test('heading IDs and links on translated pages are in English', async () => { - const $ = await getDOM('/ja/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-strong-password') + const $ = await getDOM( + '/ja/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-strong-password' + ) expect($.res.statusCode).toBe(200) expect($('h3[id="further-reading"]').length).toBe(1) expect($('h3[id="ๅ‚่€ƒใƒชใƒณใ‚ฏ"]').length).toBe(0) @@ -570,13 +728,25 @@ describe('URLs by language', () => { describe('GitHub Enterprise URLs', () => { test('renders the GHE user docs homepage', async () => { const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/user/github`) - expect($(`article a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/github/authenticating-to-github"]`).length).toBe(1) + expect( + $( + `article a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/github/authenticating-to-github"]` + ).length + ).toBe(1) }) test('renders the Enterprise Server homepage with correct links', async () => { const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}`) - expect($(`section.container-xl a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/admin"]`).length).toBe(1) - expect($(`section.container-xl a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/github"]`).length).toBe(1) + expect( + $( + `section.container-xl a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/admin"]` + ).length + ).toBe(1) + expect( + $( + `section.container-xl a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/github"]` + ).length + ).toBe(1) }) test('renders the Enterprise Admin category homepage', async () => { @@ -599,17 +769,27 @@ describe('GitHub Enterprise URLs', () => { }) test('renders an Enterprise Admin category article', async () => { - const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/admin/installation/system-overview`) - expect($.text()).toContain('your organization\'s private copy of GitHub') + const $ = await getDOM( + `/en/enterprise/${enterpriseServerReleases.latest}/admin/installation/system-overview` + ) + expect($.text()).toContain("your organization's private copy of GitHub") }) test('renders an Enterprise Admin map topic', async () => { - const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/admin/enterprise-management/updating-the-virtual-machine-and-physical-resources`) - expect($(`article a[href^="/en/enterprise-server@${enterpriseServerReleases.latest}/admin/enterprise-management/"]`).length).toBeGreaterThan(1) + const $ = await getDOM( + `/en/enterprise/${enterpriseServerReleases.latest}/admin/enterprise-management/updating-the-virtual-machine-and-physical-resources` + ) + expect( + $( + `article a[href^="/en/enterprise-server@${enterpriseServerReleases.latest}/admin/enterprise-management/"]` + ).length + ).toBeGreaterThan(1) }) test('renders an Enterprise Admin category article within a map topic', async () => { - const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/admin/installation/upgrade-requirements`) + const $ = await getDOM( + `/en/enterprise/${enterpriseServerReleases.latest}/admin/installation/upgrade-requirements` + ) expect($.text()).toContain('Before upgrading GitHub Enterprise') }) @@ -652,16 +832,24 @@ describe('GitHub Desktop URLs', () => { test('renders a Desktop category with expected links', async () => { const $ = await getDOM('/en/desktop/installing-and-configuring-github-desktop') - expect($('article a[href^="/en/desktop/installing-and-configuring-github-desktop/"]').length).toBeGreaterThan(1) + expect( + $('article a[href^="/en/desktop/installing-and-configuring-github-desktop/"]').length + ).toBeGreaterThan(1) }) test('renders a Desktop map topic', async () => { - const $ = await getDOM('/en/desktop/installing-and-configuring-github-desktop/installing-and-authenticating-to-github-desktop') - expect($('article a[href^="/en/desktop/installing-and-configuring-github-desktop/"]').length).toBeGreaterThan(1) + const $ = await getDOM( + '/en/desktop/installing-and-configuring-github-desktop/installing-and-authenticating-to-github-desktop' + ) + expect( + $('article a[href^="/en/desktop/installing-and-configuring-github-desktop/"]').length + ).toBeGreaterThan(1) }) test('renders a Desktop article within a map topic', async () => { - const res = await get('/en/desktop/installing-and-configuring-github-desktop/installing-and-authenticating-to-github-desktop/installing-github-desktop') + const res = await get( + '/en/desktop/installing-and-configuring-github-desktop/installing-and-authenticating-to-github-desktop/installing-github-desktop' + ) expect(res.statusCode).toBe(200) }) @@ -718,7 +906,7 @@ describe('extended Markdown', () => { }) describe('search', () => { - function findDupesInArray (arr) { + function findDupesInArray(arr) { return lodash.filter(arr, (val, i, iteratee) => lodash.includes(iteratee, val, i + 1)) } @@ -806,8 +994,17 @@ describe('static routes', () => { it('serves schema files from the /data/graphql directory at /public', async () => { expect((await get('/public/schema.docs.graphql')).statusCode).toBe(200) - expect((await get(`/public/ghes-${enterpriseServerReleases.latest}/schema.docs-enterprise.graphql`)).statusCode).toBe(200) - expect((await get(`/public/ghes-${enterpriseServerReleases.oldestSupported}/schema.docs-enterprise.graphql`)).statusCode).toBe(200) + expect( + (await get(`/public/ghes-${enterpriseServerReleases.latest}/schema.docs-enterprise.graphql`)) + .statusCode + ).toBe(200) + expect( + ( + await get( + `/public/ghes-${enterpriseServerReleases.oldestSupported}/schema.docs-enterprise.graphql` + ) + ).statusCode + ).toBe(200) expect((await get('/public/ghae/schema.docs-ghae.graphql')).statusCode).toBe(200) }) @@ -819,7 +1016,8 @@ describe('static routes', () => { }) describe('index pages', () => { - const nonEnterpriseOnlyPath = '/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings' + const nonEnterpriseOnlyPath = + '/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings' test('includes dotcom-only links in dotcom TOC', async () => { const $ = await getDOM('/en/github/setting-up-and-managing-your-github-user-account') @@ -827,7 +1025,9 @@ describe('index pages', () => { }) test('excludes dotcom-only from GHE TOC', async () => { - const $ = await getDOM(`/en/enterprise/${enterpriseServerReleases.latest}/user/github/setting-up-and-managing-your-github-user-account`) + const $ = await getDOM( + `/en/enterprise/${enterpriseServerReleases.latest}/user/github/setting-up-and-managing-your-github-user-account` + ) expect($(`a[href="${nonEnterpriseOnlyPath}"]`).length).toBe(0) }) diff --git a/tests/rendering/sidebar.js b/tests/rendering/sidebar.js index dc17449e0f21..7142fe282fd2 100644 --- a/tests/rendering/sidebar.js +++ b/tests/rendering/sidebar.js @@ -7,16 +7,18 @@ describe('sidebar', () => { let $homePage, $githubPage, $enterprisePage beforeAll(async () => { - [$homePage, $githubPage, $enterprisePage] = await Promise.all([ + ;[$homePage, $githubPage, $enterprisePage] = await Promise.all([ getDOM('/en'), getDOM('/en/github'), - getDOM('/en/enterprise/admin') + getDOM('/en/enterprise/admin'), ]) }) test('highlights active product on Enterprise pages', async () => { expect($enterprisePage('.sidebar-products li.sidebar-product').length).toBe(1) - expect($enterprisePage('.sidebar-products li.sidebar-product > a').text().trim()).toBe('Enterprise administrators') + expect($enterprisePage('.sidebar-products li.sidebar-product > a').text().trim()).toBe( + 'Enterprise administrators' + ) }) test('highlights active product on GitHub pages', async () => { @@ -32,7 +34,8 @@ describe('sidebar', () => { }) test('adds an `is-current-page` class to the sidebar link to the current page', async () => { - const url = '/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings' + const url = + '/en/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings' const $ = await getDOM(url) expect($('.sidebar-products .is-current-page').length).toBe(1) expect($('.sidebar-products .is-current-page a').attr('href')).toContain(url) diff --git a/tests/routing/deprecated-enterprise-versions.js b/tests/routing/deprecated-enterprise-versions.js index 69525fa6c782..32700139e582 100644 --- a/tests/routing/deprecated-enterprise-versions.js +++ b/tests/routing/deprecated-enterprise-versions.js @@ -28,15 +28,21 @@ describe('enterprise deprecation', () => { }) test('workaround for lost frontmatter redirects works in deprecated enterprise content >=2.13', async () => { - const res = await get('/en/enterprise/2.15/user/articles/viewing-contributions-on-your-profile-page') + const res = await get( + '/en/enterprise/2.15/user/articles/viewing-contributions-on-your-profile-page' + ) expect(res.statusCode).toBe(301) - expect(res.headers.location).toBe('/en/enterprise/2.15/user/articles/viewing-contributions-on-your-profile') + expect(res.headers.location).toBe( + '/en/enterprise/2.15/user/articles/viewing-contributions-on-your-profile' + ) }) test('can access redirects from redirects.json in deprecated enterprise content >2.17', async () => { const res = await get('/enterprise/2.19/admin/categories/time') expect(res.statusCode).toBe(301) - expect(res.headers.location).toBe('/en/enterprise-server@2.19/admin/configuration/configuring-time-synchronization') + expect(res.headers.location).toBe( + '/en/enterprise-server@2.19/admin/configuration/configuring-time-synchronization' + ) }) test('handles requests for deprecated Enterprise pages ( >=2.13 )', async () => { diff --git a/tests/routing/developer-site-redirects.js b/tests/routing/developer-site-redirects.js index e95567a51bf1..3424e046b1ab 100644 --- a/tests/routing/developer-site-redirects.js +++ b/tests/routing/developer-site-redirects.js @@ -31,7 +31,7 @@ describe('developer redirects', () => { test('graphql enterprise homepage', async () => { const res = await get('/enterprise/v4', { followAllRedirects: true }) expect(res.statusCode).toBe(200) - const finalPath = (new URL(res.request.url)).pathname + const finalPath = new URL(res.request.url).pathname const expectedFinalPath = `/en/enterprise-server@${enterpriseServerReleases.latest}/graphql` expect(finalPath).toBe(expectedFinalPath) }) @@ -45,8 +45,12 @@ describe('developer redirects', () => { const enterpriseRes = await get(`/enterprise${oldPath}`, { followAllRedirects: true }) expect(enterpriseRes.statusCode).toBe(200) - const finalPath = (new URL(enterpriseRes.request.url)).pathname - const expectedFinalPath = path.join('/', `enterprise-server@${enterpriseServerReleases.latest}`, newPath) + const finalPath = new URL(enterpriseRes.request.url).pathname + const expectedFinalPath = path.join( + '/', + `enterprise-server@${enterpriseServerReleases.latest}`, + newPath + ) expect(finalPath).toBe(`/en${expectedFinalPath}`) }) @@ -113,18 +117,17 @@ describe('developer redirects', () => { // this fixtures file includes /v3 and /enterprise/v3 paths test('rest reference redirects', async () => { - await eachOfLimit( - restRedirectFixtures, - MAX_CONCURRENT_REQUESTS, - async (newPath, oldPath) => { - // REST and GraphQL developer Enterprise paths with a version are only supported up to 2.21. - // We make an exception to always redirect versionless paths to the latest version. - newPath = newPath.replace('/enterprise-server/', `/enterprise-server@${enterpriseServerReleases.latest}/`) - const res = await get(oldPath) - expect(res.statusCode, `${oldPath} did not redirect to ${newPath}`).toBe(301) - expect(res.headers.location, `${oldPath} did not redirect to ${newPath}`).toBe(newPath) - } - ) + await eachOfLimit(restRedirectFixtures, MAX_CONCURRENT_REQUESTS, async (newPath, oldPath) => { + // REST and GraphQL developer Enterprise paths with a version are only supported up to 2.21. + // We make an exception to always redirect versionless paths to the latest version. + newPath = newPath.replace( + '/enterprise-server/', + `/enterprise-server@${enterpriseServerReleases.latest}/` + ) + const res = await get(oldPath) + expect(res.statusCode, `${oldPath} did not redirect to ${newPath}`).toBe(301) + expect(res.headers.location, `${oldPath} did not redirect to ${newPath}`).toBe(newPath) + }) }) // this fixtures file includes /v4 and /enterprise/v4 paths @@ -135,7 +138,10 @@ describe('developer redirects', () => { async (newPath, oldPath) => { // REST and GraphQL developer Enterprise paths with a version are only supported up to 2.21. // We make an exception to always redirect versionless paths to the latest version. - newPath = newPath.replace('/enterprise-server/', `/enterprise-server@${enterpriseServerReleases.latest}/`) + newPath = newPath.replace( + '/enterprise-server/', + `/enterprise-server@${enterpriseServerReleases.latest}/` + ) const res = await get(oldPath) expect(res.statusCode, `${oldPath} did not redirect to ${newPath}`).toBe(301) expect(res.headers.location, `${oldPath} did not redirect to ${newPath}`).toBe(newPath) diff --git a/tests/routing/middleware/redirects/help-to-docs.js b/tests/routing/middleware/redirects/help-to-docs.js index 2f827660f630..362c790d4bd1 100644 --- a/tests/routing/middleware/redirects/help-to-docs.js +++ b/tests/routing/middleware/redirects/help-to-docs.js @@ -7,49 +7,61 @@ describe('help.github.com redirect middleware', () => { const req = { hostname: 'help.github.com', protocol: 'https', - originalUrl: '/' + originalUrl: '/', } const res = new MockExpressResponse() - const next = () => { /* no op */ } + const next = () => { + /* no op */ + } await middleware(req, res, next) - expect(res._getString()).toEqual('<p>Moved Permanently. Redirecting to <a href="https://docs.github.com/">https://docs.github.com/</a></p>') + expect(res._getString()).toEqual( + '<p>Moved Permanently. Redirecting to <a href="https://docs.github.com/">https://docs.github.com/</a></p>' + ) }) it('redirects help.github.com requests to deep pages', async () => { const req = { hostname: 'help.github.com', protocol: 'https', - originalUrl: '/en/actions/configuring-and-managing-workflows/using-environment-variables' + originalUrl: '/en/actions/configuring-and-managing-workflows/using-environment-variables', } const res = new MockExpressResponse() - const next = () => { /* no op */ } + const next = () => { + /* no op */ + } await middleware(req, res, next) - expect(res._getString()).toEqual('<p>Moved Permanently. Redirecting to <a href="https://docs.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables">https://docs.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables</a></p>') + expect(res._getString()).toEqual( + '<p>Moved Permanently. Redirecting to <a href="https://docs.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables">https://docs.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables</a></p>' + ) }) it('preserves query params', async () => { const req = { hostname: 'help.github.com', protocol: 'https', - originalUrl: '/en?foo=bar' + originalUrl: '/en?foo=bar', } const res = new MockExpressResponse() - const next = () => { /* no op */ } + const next = () => { + /* no op */ + } await middleware(req, res, next) - expect(res._getString()).toEqual('<p>Moved Permanently. Redirecting to <a href="https://docs.github.com/en?foo=bar">https://docs.github.com/en?foo=bar</a></p>') + expect(res._getString()).toEqual( + '<p>Moved Permanently. Redirecting to <a href="https://docs.github.com/en?foo=bar">https://docs.github.com/en?foo=bar</a></p>' + ) }) it('does not redirect docs.github.com requests', async () => { const req = { hostname: 'docs.github.com', protocol: 'https', - originalUrl: '/' + originalUrl: '/', } const res = new MockExpressResponse() const next = jest.fn() @@ -61,12 +73,14 @@ describe('help.github.com redirect middleware', () => { const req = { hostname: 'help.github.com', protocol: 'https', - originalUrl: '//evil.com//' + originalUrl: '//evil.com//', } const res = new MockExpressResponse() const next = jest.fn() await middleware(req, res, next) const expectedRedirect = 'https://docs.github.com/evil.com//' - expect(res._getString()).toEqual(`<p>Moved Permanently. Redirecting to <a href="${expectedRedirect}">${expectedRedirect}</a></p>`) + expect(res._getString()).toEqual( + `<p>Moved Permanently. Redirecting to <a href="${expectedRedirect}">${expectedRedirect}</a></p>` + ) }) }) diff --git a/tests/routing/redirects.js b/tests/routing/redirects.js index 1937f67508a7..5d8b7efa8502 100644 --- a/tests/routing/redirects.js +++ b/tests/routing/redirects.js @@ -23,9 +23,10 @@ describe('redirects', () => { test('page.redirects is an array', async () => { const page = await Page.init({ - relativePath: 'github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches.md', + relativePath: + 'github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) page.buildRedirects() expect(isPlainObject(page.redirects)).toBe(true) @@ -35,7 +36,7 @@ describe('redirects', () => { const page = await Page.init({ relativePath: 'github/index.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) page.buildRedirects() expect(page.redirects[`/en/${nonEnterpriseDefaultVersion}/github`]).toBe('/en/github') @@ -44,15 +45,21 @@ describe('redirects', () => { expect(page.redirects[`/en/${nonEnterpriseDefaultVersion}/articles`]).toBe('/en/github') expect(page.redirects['/common-issues-and-questions']).toBe('/en/github') expect(page.redirects['/en/common-issues-and-questions']).toBe('/en/github') - expect(page.redirects[`/en/enterprise/${enterpriseServerReleases.latest}/user/articles`]).toBe(`/en/enterprise-server@${enterpriseServerReleases.latest}/github`) - expect(page.redirects[`/en/enterprise/${enterpriseServerReleases.latest}/user/common-issues-and-questions`]).toBe(`/en/enterprise-server@${enterpriseServerReleases.latest}/github`) + expect(page.redirects[`/en/enterprise/${enterpriseServerReleases.latest}/user/articles`]).toBe( + `/en/enterprise-server@${enterpriseServerReleases.latest}/github` + ) + expect( + page.redirects[ + `/en/enterprise/${enterpriseServerReleases.latest}/user/common-issues-and-questions` + ] + ).toBe(`/en/enterprise-server@${enterpriseServerReleases.latest}/github`) }) test('converts single `redirect_from` strings values into arrays', async () => { const page = await Page.init({ relativePath: 'article-with-redirect-from-string.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) page.buildRedirects() expect(page.redirects['/redirect-string']).toBe('/en/article-with-redirect-from-string') @@ -96,13 +103,13 @@ describe('redirects', () => { test('are absent from all redirected URLs', async () => { const keys = Object.keys(redirects) expect(keys.length).toBeGreaterThan(100) - expect(keys.every(key => !key.endsWith('/') || key === '/')).toBe(true) + expect(keys.every((key) => !key.endsWith('/') || key === '/')).toBe(true) }) test('are absent from all destination URLs', async () => { const values = Object.values(redirects) expect(values.length).toBeGreaterThan(100) - expect(values.every(value => !value.endsWith('/'))).toBe(true) + expect(values.every((value) => !value.endsWith('/'))).toBe(true) }) test('are redirected for HEAD requests (not just GET requests)', async () => { @@ -128,9 +135,12 @@ describe('redirects', () => { describe('localized redirects', () => { test('redirect_from for renamed pages', async () => { - const { res } = await get('/ja/desktop/contributing-to-projects/changing-a-remote-s-url-from-github-desktop') + const { res } = await get( + '/ja/desktop/contributing-to-projects/changing-a-remote-s-url-from-github-desktop' + ) expect(res.statusCode).toBe(301) - const expected = '/ja/desktop/contributing-and-collaborating-using-github-desktop/working-with-your-remote-repository-on-github-or-github-enterprise/changing-a-remotes-url-from-github-desktop' + const expected = + '/ja/desktop/contributing-and-collaborating-using-github-desktop/working-with-your-remote-repository-on-github-or-github-enterprise/changing-a-remotes-url-from-github-desktop' expect(res.headers.location).toBe(expected) }) }) @@ -222,7 +232,9 @@ describe('redirects', () => { test('admin/guides redirects to admin on <2.21', async () => { const res = await get(`/en/enterprise-server@${lastBeforeRestoredAdminGuides}/admin/guides`) expect(res.statusCode).toBe(301) - expect(res.headers.location).toBe(enterpriseAdmin.replace(latest, lastBeforeRestoredAdminGuides)) + expect(res.headers.location).toBe( + enterpriseAdmin.replace(latest, lastBeforeRestoredAdminGuides) + ) }) test('admin/guides does not redirect to admin on >=2.21', async () => { @@ -238,7 +250,9 @@ describe('redirects', () => { }) test('admin/guides redirects to admin in deep links on <2.21', async () => { - const res = await get(`/en/enterprise-server@${lastBeforeRestoredAdminGuides}/admin/guides/installation/upgrading-github-enterprise`) + const res = await get( + `/en/enterprise-server@${lastBeforeRestoredAdminGuides}/admin/guides/installation/upgrading-github-enterprise` + ) expect(res.statusCode).toBe(301) const redirectRes = await get(res.headers.location) expect(redirectRes.statusCode).toBe(200) @@ -247,7 +261,9 @@ describe('redirects', () => { }) test('admin/guides still redirects to admin in deep links on >=2.21', async () => { - const res = await get(`/en/enterprise-server@${firstRestoredAdminGuides}/admin/guides/installation/upgrading-github-enterprise`) + const res = await get( + `/en/enterprise-server@${firstRestoredAdminGuides}/admin/guides/installation/upgrading-github-enterprise` + ) expect(res.statusCode).toBe(301) const redirectRes = await get(res.headers.location) expect(redirectRes.statusCode).toBe(200) @@ -264,7 +280,9 @@ describe('redirects', () => { test('admin/guides redirects to admin on <2.21 (japanese)', async () => { const res = await get(`/ja/enterprise-server@${lastBeforeRestoredAdminGuides}/admin/guides`) expect(res.statusCode).toBe(301) - expect(res.headers.location).toBe(japaneseEnterpriseAdmin.replace(latest, lastBeforeRestoredAdminGuides)) + expect(res.headers.location).toBe( + japaneseEnterpriseAdmin.replace(latest, lastBeforeRestoredAdminGuides) + ) }) test('admin/guides does not redirect to admin on >=2.21 (japanese)', async () => { @@ -307,7 +325,9 @@ describe('redirects', () => { const japaneseUserArticle = userArticle.replace('/en/', '/ja/') test('no product redirects to GitHub.com product on the latest version', async () => { - const res = await get(`/en/enterprise/${enterpriseServerReleases.latest}/user/articles/creating-a-strong-password`) + const res = await get( + `/en/enterprise/${enterpriseServerReleases.latest}/user/articles/creating-a-strong-password` + ) expect(res.statusCode).toBe(301) expect(res.headers.location).toBe(userArticle) }) @@ -319,7 +339,9 @@ describe('redirects', () => { }) test('no language code redirects to english', async () => { - const res = await get(`/enterprise/${enterpriseServerReleases.latest}/user/articles/creating-a-strong-password`) + const res = await get( + `/enterprise/${enterpriseServerReleases.latest}/user/articles/creating-a-strong-password` + ) expect(res.statusCode).toBe(301) expect(res.headers.location).toBe(userArticle) }) @@ -343,13 +365,17 @@ describe('redirects', () => { const japaneseUserArticle = userArticle.replace('/en/', '/ja/') test('redirects to expected article', async () => { - const res = await get(`/en/enterprise/${enterpriseServerReleases.latest}/user${redirectFromPath}`) + const res = await get( + `/en/enterprise/${enterpriseServerReleases.latest}/user${redirectFromPath}` + ) expect(res.statusCode).toBe(301) expect(res.headers.location).toBe(userArticle) }) test('no language code redirects to english', async () => { - const res = await get(`/enterprise/${enterpriseServerReleases.latest}/user${redirectFromPath}`) + const res = await get( + `/enterprise/${enterpriseServerReleases.latest}/user${redirectFromPath}` + ) expect(res.statusCode).toBe(301) expect(res.headers.location).toBe(userArticle) }) @@ -368,23 +394,30 @@ describe('redirects', () => { }) describe('desktop guide', () => { - const desktopGuide = '/en/desktop/contributing-and-collaborating-using-github-desktop/working-with-your-remote-repository-on-github-or-github-enterprise/creating-an-issue-or-pull-request' + const desktopGuide = + '/en/desktop/contributing-and-collaborating-using-github-desktop/working-with-your-remote-repository-on-github-or-github-enterprise/creating-an-issue-or-pull-request' const japaneseDesktopGuides = desktopGuide.replace('/en/', '/ja/') test('no language code redirects to english', async () => { - const res = await get('/desktop/contributing-and-collaborating-using-github-desktop/creating-an-issue-or-pull-request') + const res = await get( + '/desktop/contributing-and-collaborating-using-github-desktop/creating-an-issue-or-pull-request' + ) expect(res.statusCode).toBe(301) expect(res.headers.location).toBe(desktopGuide) }) test('desktop/guides redirects to desktop', async () => { - const res = await get('/en/desktop/guides/contributing-and-collaborating-using-github-desktop/creating-an-issue-or-pull-request') + const res = await get( + '/en/desktop/guides/contributing-and-collaborating-using-github-desktop/creating-an-issue-or-pull-request' + ) expect(res.statusCode).toBe(301) expect(res.headers.location).toBe(desktopGuide) }) test('desktop/guides redirects to desktop (japanese)', async () => { - const res = await get('/ja/desktop/guides/contributing-and-collaborating-using-github-desktop/creating-an-issue-or-pull-request') + const res = await get( + '/ja/desktop/guides/contributing-and-collaborating-using-github-desktop/creating-an-issue-or-pull-request' + ) expect(res.statusCode).toBe(301) expect(res.headers.location).toBe(japaneseDesktopGuides) }) diff --git a/tests/routing/release-notes.js b/tests/routing/release-notes.js index 2e47c0591733..e33067ae9f1b 100644 --- a/tests/routing/release-notes.js +++ b/tests/routing/release-notes.js @@ -17,7 +17,7 @@ describe('release notes', () => { expect(res.headers.location).toBe('https://enterprise.github.com/releases/2.19.0/notes') }) - it('renders the release-notes layout if this version\'s release notes are in this repo', async () => { + it("renders the release-notes layout if this version's release notes are in this repo", async () => { const res = await get('/en/enterprise-server@2.22/admin/release-notes') expect(res.statusCode).toBe(200) const $ = await getDOM('/en/enterprise-server@2.22/admin/release-notes') diff --git a/tests/routing/top-developer-site-path-redirects.js b/tests/routing/top-developer-site-path-redirects.js index 3202e050fd77..63de24ad40e8 100644 --- a/tests/routing/top-developer-site-path-redirects.js +++ b/tests/routing/top-developer-site-path-redirects.js @@ -17,13 +17,13 @@ describe('developer.github.com redirects', () => { /^\/partnerships/, '2.17', '2.16', - '2.15' + '2.15', ] // test a subset of the top paths const pathsToCheck = 50 const paths = topOldDeveloperSitePaths - .filter(path => !ignoredPatterns.some(pattern => path.match(pattern))) + .filter((path) => !ignoredPatterns.some((pattern) => path.match(pattern))) .slice(0, pathsToCheck) const non200s = [] @@ -36,10 +36,14 @@ describe('developer.github.com redirects', () => { } // generate an object with empty values as the error message - const errorMessage = JSON.stringify(non200s.reduce((acc, path) => { - acc[path] = '' - return acc - }, {}), null, 2) + const errorMessage = JSON.stringify( + non200s.reduce((acc, path) => { + acc[path] = '' + return acc + }, {}), + null, + 2 + ) expect(non200s, errorMessage).toEqual([]) }) diff --git a/tests/unit/actions-workflows.js b/tests/unit/actions-workflows.js index 8dc67d284609..c9e79837e7ad 100644 --- a/tests/unit/actions-workflows.js +++ b/tests/unit/actions-workflows.js @@ -7,32 +7,28 @@ import { chain, difference, get } from 'lodash-es' import allowedActions from '../../.github/allowed-actions.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const workflowsDir = path.join(__dirname, '../../.github/workflows') -const workflows = fs.readdirSync(workflowsDir) - .filter(filename => filename.endsWith('.yml') || filename.endsWith('.yaml')) - .map(filename => { +const workflows = fs + .readdirSync(workflowsDir) + .filter((filename) => filename.endsWith('.yml') || filename.endsWith('.yaml')) + .map((filename) => { const fullpath = path.join(workflowsDir, filename) const data = yaml.load(fs.readFileSync(fullpath, 'utf8'), { fullpath }) return { filename, fullpath, data } }) -function actionsUsedInWorkflow (workflow) { +function actionsUsedInWorkflow(workflow) { return Object.keys(flat(workflow)) - .filter(key => key.endsWith('.uses')) - .map(key => get(workflow, key)) + .filter((key) => key.endsWith('.uses')) + .map((key) => get(workflow, key)) } const scheduledWorkflows = workflows - .map(workflow => workflow.data.on.schedule) + .map((workflow) => workflow.data.on.schedule) .filter(Boolean) .flat() - .map(schedule => schedule.cron) + .map((schedule) => schedule.cron) -const allUsedActions = chain(workflows) - .map(actionsUsedInWorkflow) - .flatten() - .uniq() - .sort() - .value() +const allUsedActions = chain(workflows).map(actionsUsedInWorkflow).flatten().uniq().sort().value() describe('GitHub Actions workflows', () => { test('all used actions are allowed in .github/allowed-actions.js', () => { @@ -48,7 +44,7 @@ describe('GitHub Actions workflows', () => { }) test('no scheduled workflows run on the hour', () => { - const hourlySchedules = scheduledWorkflows.filter(schedule => { + const hourlySchedules = scheduledWorkflows.filter((schedule) => { const hour = schedule.split(' ')[0] // return any minute cron segments that equal 0, 00, 000, etc. return !/[^0]/.test(hour) diff --git a/tests/unit/data-directory/index.js b/tests/unit/data-directory/index.js index 82c84325fd32..fe4a376ec69f 100644 --- a/tests/unit/data-directory/index.js +++ b/tests/unit/data-directory/index.js @@ -10,7 +10,7 @@ describe('data-directory', () => { const expected = { bar: { another_markup_language: 'yes' }, foo: { meaningOfLife: 42 }, - nested: { baz: 'I am markdown!' } + nested: { baz: 'I am markdown!' }, } expect(data).toEqual(expected) }) @@ -34,9 +34,9 @@ describe('data-directory', () => { const ignorePatterns = [] // README is ignored by default - expect('README' in await dataDirectory(fixturesDir)).toBe(false) + expect('README' in (await dataDirectory(fixturesDir))).toBe(false) // README can be included by setting empty ignorePatterns array - expect('README' in await dataDirectory(fixturesDir, { ignorePatterns })).toBe(true) + expect('README' in (await dataDirectory(fixturesDir, { ignorePatterns }))).toBe(true) }) }) diff --git a/tests/unit/early-access.js b/tests/unit/early-access.js index ea2ff77ee556..60c8a22ad90f 100644 --- a/tests/unit/early-access.js +++ b/tests/unit/early-access.js @@ -28,12 +28,14 @@ describe('rendering early-access', () => { testViaActionsOnly('the top-level TOC renders locally', async () => { const $ = await getDOM('/en/early-access') - expect($.html().includes('Hello, local developer! This page is not visible on production.')).toBe(true) + expect( + $.html().includes('Hello, local developer! This page is not visible on production.') + ).toBe(true) expect($('ul a').length).toBeGreaterThan(5) }) testViaActionsOnly('the top-level TOC does not render on production', async () => { - async function getEarlyAccess () { + async function getEarlyAccess() { return await got('https://docs.github.com/en/early-access') } await expect(getEarlyAccess).rejects.toThrowError('Response code 404 (Not Found)') diff --git a/tests/unit/enterprise-versions.js b/tests/unit/enterprise-versions.js index 1438aee43d5d..2034c751a24f 100644 --- a/tests/unit/enterprise-versions.js +++ b/tests/unit/enterprise-versions.js @@ -1,13 +1,7 @@ import patterns from '../../lib/patterns.js' import xEnterpriseServerReleases from '../../lib/enterprise-server-releases.js' -const { - supported, - deprecated, - all, - latest, - oldestSupported, - nextDeprecationDate -} = xEnterpriseServerReleases +const { supported, deprecated, all, latest, oldestSupported, nextDeprecationDate } = + xEnterpriseServerReleases describe('enterpriseServerReleases module', () => { test('includes an array of `supported` versions', async () => { diff --git a/tests/unit/failbot.js b/tests/unit/failbot.js index 87e99cd0e5af..e13c33670aaf 100644 --- a/tests/unit/failbot.js +++ b/tests/unit/failbot.js @@ -3,9 +3,11 @@ import nock from 'nock' describe('FailBot', () => { beforeEach(() => { - nock('https://haystack.com').post('/').reply(200, (uri, requestBody) => { - return requestBody - }) + nock('https://haystack.com') + .post('/') + .reply(200, (uri, requestBody) => { + return requestBody + }) }) afterEach(() => { @@ -38,7 +40,7 @@ describe('FailBot', () => { created_at: expect.any(String), js_environment: expect.stringMatching(/^Node\.js\sv[\d.]+/), message: 'Kaboom', - rollup: expect.any(String) + rollup: expect.any(String), }) }) }) diff --git a/tests/unit/feature-flags.js b/tests/unit/feature-flags.js index da2a1debe6a8..e822589d166a 100644 --- a/tests/unit/feature-flags.js +++ b/tests/unit/feature-flags.js @@ -3,7 +3,7 @@ import readJsonFile from '../../lib/read-json-file.js' const ffs = readJsonFile('./feature-flags.json') describe('feature flags', () => { - Object.keys(ffs).forEach(featureName => { + Object.keys(ffs).forEach((featureName) => { expect(featureName.startsWith('FEATURE_')).toBe(true) }) diff --git a/tests/unit/find-page.js b/tests/unit/find-page.js index 00e32dd4fd23..9692558849fe 100644 --- a/tests/unit/find-page.js +++ b/tests/unit/find-page.js @@ -8,11 +8,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) describe('find page', () => { jest.setTimeout(1000 * 1000) - test('falls back to the English page if it can\'t find a localized page', async () => { + test("falls back to the English page if it can't find a localized page", async () => { const page = await Page.init({ relativePath: 'page-that-does-not-exist-in-translations-dir.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) const englishPermalink = page.permalinks[0].href @@ -20,7 +20,7 @@ describe('find page', () => { // add named keys const pageMap = { - [englishPermalink]: page + [englishPermalink]: page, } const localizedPage = findPage(japanesePermalink, pageMap, {}) @@ -31,7 +31,7 @@ describe('find page', () => { const page = await Page.init({ relativePath: 'page-with-redirects.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) const englishPermalink = page.permalinks[0].href @@ -39,7 +39,7 @@ describe('find page', () => { // add named keys const pageMap = { - [englishPermalink]: page + [englishPermalink]: page, } const redirectedPage = findPage(redirectToFind, pageMap, page.buildRedirects()) diff --git a/tests/unit/get-rss-feeds.js b/tests/unit/get-rss-feeds.js index 2825d433b312..80f880fd6422 100644 --- a/tests/unit/get-rss-feeds.js +++ b/tests/unit/get-rss-feeds.js @@ -3,7 +3,10 @@ import { getChangelogItems } from '../../lib/changelog.js' import fs from 'fs' import path from 'path' const parser = new Parser({ timeout: 5000 }) -const rssFeedContent = fs.readFileSync(path.join(process.cwd(), 'tests/fixtures/rss-feed.xml'), 'utf8') +const rssFeedContent = fs.readFileSync( + path.join(process.cwd(), 'tests/fixtures/rss-feed.xml'), + 'utf8' +) describe('getChangelogItems module', () => { let changelog @@ -21,18 +24,18 @@ describe('getChangelogItems module', () => { { title: 'Authentication token format updates are generally available', date: '2021-03-31T22:22:03.000Z', - href: 'https://github.blog/changelog/2021-03-31-authentication-token-format-updates-are-generally-available' + href: 'https://github.blog/changelog/2021-03-31-authentication-token-format-updates-are-generally-available', }, { title: 'Compare REST API now supports pagination', date: '2021-03-23T02:49:54.000Z', - href: 'https://github.blog/changelog/2021-03-22-compare-rest-api-now-supports-pagination' + href: 'https://github.blog/changelog/2021-03-22-compare-rest-api-now-supports-pagination', }, { title: 'GitHub Discussions GraphQL API public beta', date: '2021-02-23T18:21:40.000Z', - href: 'https://github.blog/changelog/2021-02-23-github-discussions-graphql-api-public-beta' - } + href: 'https://github.blog/changelog/2021-02-23-github-discussions-graphql-api-public-beta', + }, ] for (let i = 0; i < 3; i++) { diff --git a/tests/unit/hydro.js b/tests/unit/hydro.js index 1866738a67c8..993c6538a4cc 100644 --- a/tests/unit/hydro.js +++ b/tests/unit/hydro.js @@ -11,22 +11,27 @@ describe('hydro', () => { reqheaders: { Authorization: /^Hydro [\d\w]{64}$/, 'Content-Type': 'application/json', - 'X-Hydro-App': 'docs-production' - } + 'X-Hydro-App': 'docs-production', + }, }) // Respond with a 200 and store the body we sent - .post('/').reply(200, (_, body) => { params = body }) + .post('/') + .reply(200, (_, body) => { + params = body + }) }) describe('#publish', () => { it('publishes a single event to Hydro', async () => { await hydro.publish('event-name', { pizza: true }) expect(params).toEqual({ - events: [{ - schema: 'event-name', - value: JSON.stringify({ pizza: true }), - cluster: 'potomac' - }] + events: [ + { + schema: 'event-name', + value: JSON.stringify({ pizza: true }), + cluster: 'potomac', + }, + ], }) }) }) @@ -35,19 +40,22 @@ describe('hydro', () => { it('publishes multiple events to Hydro', async () => { await hydro.publishMany([ { schema: 'event-name', value: { pizza: true } }, - { schema: 'other-name', value: { salad: false } } + { schema: 'other-name', value: { salad: false } }, ]) expect(params).toEqual({ - events: [{ - schema: 'event-name', - value: JSON.stringify({ pizza: true }), - cluster: 'potomac' - }, { - schema: 'other-name', - value: JSON.stringify({ salad: false }), - cluster: 'potomac' - }] + events: [ + { + schema: 'event-name', + value: JSON.stringify({ pizza: true }), + cluster: 'potomac', + }, + { + schema: 'other-name', + value: JSON.stringify({ salad: false }), + cluster: 'potomac', + }, + ], }) }) }) diff --git a/tests/unit/languages.js b/tests/unit/languages.js index d4f4c41a4242..e10fb5acedb5 100644 --- a/tests/unit/languages.js +++ b/tests/unit/languages.js @@ -10,7 +10,7 @@ describe('languages module', () => { }) test('every language is valid', () => { - Object.values(languages).forEach(language => { + Object.values(languages).forEach((language) => { const { valid, errors } = revalidator.validate(language, schema) const expectation = JSON.stringify(errors, null, 2) expect(valid, expectation).toBe(true) diff --git a/tests/unit/liquid-helpers.js b/tests/unit/liquid-helpers.js index 000e19e52265..c116096bd883 100644 --- a/tests/unit/liquid-helpers.js +++ b/tests/unit/liquid-helpers.js @@ -19,23 +19,24 @@ describe('liquid helper tags', () => { '/en/desktop/contributing-and-collaborating-using-github-desktop': `/en/${nonEnterpriseDefaultVersion}/desktop/contributing-and-collaborating-using-github-desktop`, '/ja/desktop/contributing-and-collaborating-using-github-desktop': `/ja/${nonEnterpriseDefaultVersion}/desktop/contributing-and-collaborating-using-github-desktop`, '/en/desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories': `/en/${nonEnterpriseDefaultVersion}/desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories`, - '/en/github/writing-on-github/basic-writing-and-formatting-syntax': `/en/${nonEnterpriseDefaultVersion}/github/writing-on-github/basic-writing-and-formatting-syntax` + '/en/github/writing-on-github/basic-writing-and-formatting-syntax': `/en/${nonEnterpriseDefaultVersion}/github/writing-on-github/basic-writing-and-formatting-syntax`, } context.site = { data: { reusables: { - example: 'a rose by any other name\nwould smell as sweet' - } - } + example: 'a rose by any other name\nwould smell as sweet', + }, + }, } context.page = { - relativePath: 'desktop/index.md' + relativePath: 'desktop/index.md', } }) test('link tag with relative path (English)', async () => { const template = '{% link /contributing-and-collaborating-using-github-desktop %}' - const expected = '<a class="link-title Bump-link--hover no-underline" href="/en/desktop/contributing-and-collaborating-using-github-desktop">Contributing and collaborating using GitHub Desktop</a>' + const expected = + '<a class="link-title Bump-link--hover no-underline" href="/en/desktop/contributing-and-collaborating-using-github-desktop">Contributing and collaborating using GitHub Desktop</a>' const output = await liquid.parseAndRender(template, context) expect(output).toBe(expected) }) @@ -43,7 +44,8 @@ describe('liquid helper tags', () => { test('link tag with relative path (translated)', async () => { context.currentLanguage = 'ja' const template = '{% link /contributing-and-collaborating-using-github-desktop %}' - const expected = '<a class="link-title Bump-link--hover no-underline" href="/ja/desktop/contributing-and-collaborating-using-github-desktop">' + const expected = + '<a class="link-title Bump-link--hover no-underline" href="/ja/desktop/contributing-and-collaborating-using-github-desktop">' const output = await liquid.parseAndRender(template, context) expect(output.includes(expected)).toBe(true) // set this back to english @@ -53,15 +55,18 @@ describe('liquid helper tags', () => { test('link tag with local variable', async () => { const template = `{% assign href = "/contributing-and-collaborating-using-github-desktop" %} {% link {{ href }} %}` - const expected = '<a class="link-title Bump-link--hover no-underline" href="/en/desktop/contributing-and-collaborating-using-github-desktop">' + const expected = + '<a class="link-title Bump-link--hover no-underline" href="/en/desktop/contributing-and-collaborating-using-github-desktop">' const output = await liquid.parseAndRender(template, context) expect(output.includes(expected)).toBe(true) }) test('link tag with absolute path', async () => { context.currentLanguage = 'en' - const template = '{% link /desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories %}' - const expected = '<a class="link-title Bump-link--hover no-underline" href="/en/desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories">Adding and cloning repositories</a>' + const template = + '{% link /desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories %}' + const expected = + '<a class="link-title Bump-link--hover no-underline" href="/en/desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories">Adding and cloning repositories</a>' const output = await liquid.parseAndRender(template, context) expect(output).toBe(expected) }) @@ -79,13 +84,15 @@ describe('liquid helper tags', () => { test('link_in_list tag', async () => { const template = '{% link_in_list /contributing-and-collaborating-using-github-desktop %}' - const expected = '- <a class="article-link link Bump-link--hover no-underline" href="/en/desktop/contributing-and-collaborating-using-github-desktop">Contributing and collaborating using GitHub Desktop</a>' + const expected = + '- <a class="article-link link Bump-link--hover no-underline" href="/en/desktop/contributing-and-collaborating-using-github-desktop">Contributing and collaborating using GitHub Desktop</a>' const output = await liquid.parseAndRender(template, context) expect(output).toBe(expected) }) test('link_as_article_card', async () => { - const template = '{% link_as_article_card /contributing-and-collaborating-using-github-desktop %}' + const template = + '{% link_as_article_card /contributing-and-collaborating-using-github-desktop %}' const expected = `<div class="d-flex col-12 col-md-4 pr-0 pr-md-6 pr-lg-8 <display condition> js-filter-card" data-type="" data-topics=""> <a class="no-underline d-flex flex-column py-3 border-bottom" href="/en/desktop/contributing-and-collaborating-using-github-desktop"> <h4 class="h4 color-text-primary mb-1">Contributing and collaborating using GitHub Desktop</h4> @@ -130,32 +137,30 @@ would smell as sweet` }) describe('data tag', () => { - test( - 'handles bracketed array access within for-in loop', - async () => { - const template = ` + test('handles bracketed array access within for-in loop', async () => { + const template = ` {% for term in site.data.glossaries.external %} ### {% data glossaries.external[forloop.index0].term %} {% data glossaries.external[forloop.index0].description %} --- {% endfor %}` - const localContext = { ...context } - localContext.site = { - data: { - variables: { - fire_emoji: ':fire:' - }, - glossaries: { - external: [ - { term: 'lit', description: 'Awesome things. {% data variables.fire_emoji %}' }, - { term: 'Zhu Li', description: '_"Zhu Li, do the thing!"_ :point_up:' } - ] - } - } - } - - const expected = ` + const localContext = { ...context } + localContext.site = { + data: { + variables: { + fire_emoji: ':fire:', + }, + glossaries: { + external: [ + { term: 'lit', description: 'Awesome things. {% data variables.fire_emoji %}' }, + { term: 'Zhu Li', description: '_"Zhu Li, do the thing!"_ :point_up:' }, + ], + }, + }, + } + + const expected = ` ### lit Awesome things. :fire: @@ -166,9 +171,8 @@ _"Zhu Li, do the thing!"_ :point_up: --- ` - const output = await liquid.parseAndRender(template, localContext) - expect(output).toBe(expected) - } - ) + const output = await liquid.parseAndRender(template, localContext) + expect(output).toBe(expected) + }) }) }) diff --git a/tests/unit/liquid.js b/tests/unit/liquid.js index df630c93d68f..8066618786b5 100644 --- a/tests/unit/liquid.js +++ b/tests/unit/liquid.js @@ -72,7 +72,7 @@ describe('liquid template parser', () => { currentVersion: 'free-pro-team@latest', page: {}, allVersions, - enterpriseServerReleases + enterpriseServerReleases, } await shortVersionsMiddleware(req, null, () => {}) const output = await liquid.parseAndRender(shortVersionsTemplate, req.context) @@ -85,7 +85,7 @@ describe('liquid template parser', () => { currentVersion: 'github-ae@latest', page: {}, allVersions, - enterpriseServerReleases + enterpriseServerReleases, } await shortVersionsMiddleware(req, null, () => {}) const output = await liquid.parseAndRender(shortVersionsTemplate, req.context) @@ -97,11 +97,13 @@ describe('liquid template parser', () => { currentVersion: 'enterprise-server@2.22', page: {}, allVersions, - enterpriseServerReleases + enterpriseServerReleases, } await shortVersionsMiddleware(req, null, () => {}) const output = await liquid.parseAndRender(shortVersionsTemplate, req.context) - expect(output.replace(/\s\s+/g, ' ').trim()).toBe('I am GHES I am GHES < 3.1 I am FTP or GHES < 3.0') + expect(output.replace(/\s\s+/g, ' ').trim()).toBe( + 'I am GHES I am GHES < 3.1 I am FTP or GHES < 3.0' + ) }) test('AND statements work as expected', async () => { @@ -109,7 +111,7 @@ describe('liquid template parser', () => { currentVersion: 'enterprise-server@3.0', page: {}, allVersions, - enterpriseServerReleases + enterpriseServerReleases, } await shortVersionsMiddleware(req, null, () => {}) const output = await liquid.parseAndRender(shortVersionsTemplate, req.context) @@ -121,7 +123,7 @@ describe('liquid template parser', () => { currentVersion: 'github-ae@latest', page: {}, allVersions, - enterpriseServerReleases + enterpriseServerReleases, } await shortVersionsMiddleware(req, null, () => {}) const output = await liquid.parseAndRender(negativeVersionsTemplate, req.context) @@ -133,7 +135,7 @@ describe('liquid template parser', () => { currentVersion: 'enterprise-server@3.0', page: {}, allVersions, - enterpriseServerReleases + enterpriseServerReleases, } await shortVersionsMiddleware(req, null, () => {}) const output = await liquid.parseAndRender(negativeVersionsTemplate, req.context) @@ -145,7 +147,7 @@ describe('liquid template parser', () => { currentVersion: 'enterprise-server@3.1', page: {}, allVersions, - enterpriseServerReleases + enterpriseServerReleases, } await shortVersionsMiddleware(req, null, () => {}) const output = await liquid.parseAndRender(negativeVersionsTemplate, req.context) @@ -169,7 +171,7 @@ describe('liquid template parser', () => { page: {}, allVersions, enterpriseServerReleases, - site: siteData + site: siteData, } await featureVersionsMiddleware(req, null, () => {}) const outputFpt = await liquid.parseAndRender(featureVersionsTemplate, req.context) @@ -182,7 +184,7 @@ describe('liquid template parser', () => { page: {}, allVersions, enterpriseServerReleases, - site: siteData + site: siteData, } await featureVersionsMiddleware(req, null, () => {}) const outputFpt = await liquid.parseAndRender(featureVersionsTemplate, req.context) @@ -195,7 +197,7 @@ describe('liquid template parser', () => { page: {}, allVersions, enterpriseServerReleases, - site: siteData + site: siteData, } await featureVersionsMiddleware(req, null, () => {}) const outputFpt = await liquid.parseAndRender(featureVersionsTemplate, req.context) diff --git a/tests/unit/openapi-schema.js b/tests/unit/openapi-schema.js index cc8e408c19b4..19373a620ed4 100644 --- a/tests/unit/openapi-schema.js +++ b/tests/unit/openapi-schema.js @@ -20,12 +20,11 @@ describe('OpenAPI schema validation', () => { // is not yet defined in allVersions (e.g., a GHEC static file can exist // even though the version is not yet supported in the docs) test('every OpenAPI version must have a schema file in the docs', () => { - const decoratedFilenames = walk(schemasPath) - .map(filename => path.basename(filename, '.json')) + const decoratedFilenames = walk(schemasPath).map((filename) => path.basename(filename, '.json')) Object.values(allVersions) - .map(version => version.openApiVersionName) - .forEach(openApiBaseName => { + .map((version) => version.openApiVersionName) + .forEach((openApiBaseName) => { expect(decoratedFilenames.includes(openApiBaseName)).toBe(true) }) }) @@ -42,8 +41,8 @@ describe('OpenAPI schema validation', () => { }) }) -function findOperation (method, path) { - return nonEnterpriseDefaultVersionSchema.find(operation => { +function findOperation(method, path) { + return nonEnterpriseDefaultVersionSchema.find((operation) => { return operation.requestPath === path && operation.verb.toLowerCase() === method.toLowerCase() }) } @@ -52,22 +51,34 @@ describe('x-codeSamples for curl', () => { test('GET', () => { const operation = findOperation('GET', '/repos/{owner}/{repo}') expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find(sample => sample.lang === 'Shell') - const expected = 'curl \\\n -H "Accept: application/vnd.github.v3+json" \\\n https://api.github.com/repos/octocat/hello-world' + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'Shell') + const expected = + 'curl \\\n -H "Accept: application/vnd.github.v3+json" \\\n https://api.github.com/repos/octocat/hello-world' expect(source).toEqual(expected) }) test('operations with required preview headers', () => { - const operationsWithRequiredPreviewHeaders = nonEnterpriseDefaultVersionSchema.filter(operation => { - const previews = get(operation, 'x-github.previews', []) - return previews.some(preview => preview.required) - }) + const operationsWithRequiredPreviewHeaders = nonEnterpriseDefaultVersionSchema.filter( + (operation) => { + const previews = get(operation, 'x-github.previews', []) + return previews.some((preview) => preview.required) + } + ) expect(operationsWithRequiredPreviewHeaders.length).toBeGreaterThan(0) - const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter(operation => { - const { source: codeSample } = operation['x-codeSamples'].find(sample => sample.lang === 'Shell') - return codeSample.includes('-H "Accept: application/vnd.github') && !codeSample.includes('application/vnd.github.v3+json') - }) - expect(operationsWithRequiredPreviewHeaders.length).toEqual(operationsWithHeadersInCodeSample.length) + const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter( + (operation) => { + const { source: codeSample } = operation['x-codeSamples'].find( + (sample) => sample.lang === 'Shell' + ) + return ( + codeSample.includes('-H "Accept: application/vnd.github') && + !codeSample.includes('application/vnd.github.v3+json') + ) + } + ) + expect(operationsWithRequiredPreviewHeaders.length).toEqual( + operationsWithHeadersInCodeSample.length + ) }) }) @@ -75,7 +86,7 @@ describe('x-codeSamples for @octokit/core.js', () => { test('GET', () => { const operation = findOperation('GET', '/repos/{owner}/{repo}') expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find(sample => sample.lang === 'JavaScript') + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') const expected = dedent`await octokit.request('GET /repos/{owner}/{repo}', { owner: 'octocat', repo: 'hello-world' @@ -86,7 +97,7 @@ describe('x-codeSamples for @octokit/core.js', () => { test('POST', () => { const operation = findOperation('POST', '/repos/{owner}/{repo}/git/trees') expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find(sample => sample.lang === 'JavaScript') + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') const expected = dedent`await octokit.request('POST /repos/{owner}/{repo}/git/trees', { owner: 'octocat', repo: 'hello-world', @@ -106,7 +117,7 @@ describe('x-codeSamples for @octokit/core.js', () => { test('PUT', () => { const operation = findOperation('PUT', '/authorizations/clients/{client_id}/{fingerprint}') expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find(sample => sample.lang === 'JavaScript') + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') const expected = dedent`await octokit.request('PUT /authorizations/clients/{client_id}/{fingerprint}', { client_id: 'client_id', fingerprint: 'fingerprint', @@ -116,10 +127,12 @@ describe('x-codeSamples for @octokit/core.js', () => { }) test('operations with required preview headers', () => { - const operationsWithRequiredPreviewHeaders = nonEnterpriseDefaultVersionSchema.filter(operation => { - const previews = get(operation, 'x-github.previews', []) - return previews.some(preview => preview.required) - }) + const operationsWithRequiredPreviewHeaders = nonEnterpriseDefaultVersionSchema.filter( + (operation) => { + const previews = get(operation, 'x-github.previews', []) + return previews.some((preview) => preview.required) + } + ) expect(operationsWithRequiredPreviewHeaders.length).toBeGreaterThan(0) // Find something that looks like the following in each code sample: @@ -130,11 +143,17 @@ describe('x-codeSamples for @octokit/core.js', () => { ] } */ - const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter(operation => { - const { source: codeSample } = operation['x-codeSamples'].find(sample => sample.lang === 'JavaScript') - return codeSample.match(/mediaType: \{\s+previews: /g) - }) - expect(operationsWithRequiredPreviewHeaders.length).toEqual(operationsWithHeadersInCodeSample.length) + const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter( + (operation) => { + const { source: codeSample } = operation['x-codeSamples'].find( + (sample) => sample.lang === 'JavaScript' + ) + return codeSample.match(/mediaType: \{\s+previews: /g) + } + ) + expect(operationsWithRequiredPreviewHeaders.length).toEqual( + operationsWithHeadersInCodeSample.length + ) }) // skipped because the definition is current missing the `content-type` parameter @@ -142,7 +161,7 @@ describe('x-codeSamples for @octokit/core.js', () => { test.skip('operation with content-type parameter', () => { const operation = findOperation('POST', '/markdown/raw') expect(isPlainObject(operation)).toBe(true) - const { source } = operation['x-codeSamples'].find(sample => sample.lang === 'JavaScript') + const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript') const expected = dedent`await octokit.request('POST /markdown/raw', { data: 'data', headers: { diff --git a/tests/unit/page.js b/tests/unit/page.js index a7fd8fb71104..27a6e700a9e1 100644 --- a/tests/unit/page.js +++ b/tests/unit/page.js @@ -9,15 +9,18 @@ import nonEnterpriseDefaultVersion from '../../lib/non-enterprise-default-versio // import getLinkData from '../../lib/get-link-data.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const prerenderedObjects = readJsonFile('./lib/graphql/static/prerendered-objects.json') -const enterpriseServerVersions = Object.keys(allVersions).filter(v => v.startsWith('enterprise-server@')) +const enterpriseServerVersions = Object.keys(allVersions).filter((v) => + v.startsWith('enterprise-server@') +) // get the `free-pro-team` segment of `free-pro-team@latest` const nonEnterpriseDefaultPlan = nonEnterpriseDefaultVersion.split('@')[0] const opts = { - relativePath: 'github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches.md', + relativePath: + 'github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', } describe('Page class', () => { @@ -31,7 +34,7 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'translated-toc-with-no-links-index.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'ja' + languageCode: 'ja', }) expect(typeof page.title).toBe('string') }) @@ -43,20 +46,20 @@ describe('Page class', () => { article = await Page.init({ relativePath: 'sample-article.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) articleWithFM = await Page.init({ showMiniToc: false, relativePath: article.relativePath, basePath: article.basePath, - languageCode: article.languageCode + languageCode: article.languageCode, }) tocPage = await Page.init({ relativePath: 'sample-toc-index.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) }) @@ -80,22 +83,32 @@ describe('Page class', () => { const context = { page: { version: `enterprise-server@${enterpriseServerReleases.latest}` }, currentVersion: `enterprise-server@${enterpriseServerReleases.latest}`, - currentPath: '/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches', - currentLanguage: 'en' + currentPath: + '/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches', + currentLanguage: 'en', } const rendered = await page.render(context) const $ = cheerio.load(rendered) expect(page.markdown.includes('(/articles/about-pull-requests)')).toBe(true) expect(page.markdown.includes('(/en/articles/about-pull-requests)')).toBe(false) expect($('a[href="/articles/about-pull-requests"]').length).toBe(0) - expect($(`a[href="/en/${`enterprise-server@${enterpriseServerReleases.latest}`}/articles/about-pull-requests"]`).length).toBeGreaterThan(0) + expect( + $( + `a[href="/en/${`enterprise-server@${enterpriseServerReleases.latest}`}/articles/about-pull-requests"]` + ).length + ).toBeGreaterThan(0) }) test('rewrites links on prerendered GraphQL page include the current language prefix and version', async () => { - const graphqlVersion = allVersions[`enterprise-server@${enterpriseServerReleases.latest}`].miscVersionName + const graphqlVersion = + allVersions[`enterprise-server@${enterpriseServerReleases.latest}`].miscVersionName const $ = cheerio.load(prerenderedObjects[graphqlVersion].html) expect($('a[href^="/graphql/reference/input-objects"]').length).toBe(0) - expect($(`a[href^="/en/enterprise-server@${enterpriseServerReleases.latest}/graphql/reference/input-objects"]`).length).toBeGreaterThan(0) + expect( + $( + `a[href^="/en/enterprise-server@${enterpriseServerReleases.latest}/graphql/reference/input-objects"]` + ).length + ).toBeGreaterThan(0) }) test('rewrites links in the intro to include the current language prefix and version', async () => { @@ -104,8 +117,9 @@ describe('Page class', () => { const context = { page: { version: nonEnterpriseDefaultVersion }, currentVersion: nonEnterpriseDefaultVersion, - currentPath: '/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches', - currentLanguage: 'en' + currentPath: + '/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches', + currentLanguage: 'en', } await page.render(context) const $ = cheerio.load(page.intro) @@ -115,21 +129,33 @@ describe('Page class', () => { test('does not rewrite links that include deprecated enterprise release numbers', async () => { const page = await Page.init({ - relativePath: 'admin/enterprise-management/updating-the-virtual-machine-and-physical-resources/migrating-from-github-enterprise-1110x-to-2123.md', + relativePath: + 'admin/enterprise-management/updating-the-virtual-machine-and-physical-resources/migrating-from-github-enterprise-1110x-to-2123.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) const context = { page: { version: `enterprise-server@${enterpriseServerReleases.latest}` }, currentVersion: `enterprise-server@${enterpriseServerReleases.latest}`, currentPath: `/en/enterprise-server@${enterpriseServerReleases.latest}/admin/enterprise-management/migrating-from-github-enterprise-1110x-to-2123`, - currentLanguage: 'en' + currentLanguage: 'en', } const rendered = await page.render(context) const $ = cheerio.load(rendered) - expect(page.markdown.includes('(/enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release/)')).toBe(true) - expect($(`a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/11.10.340/admin/articles/upgrading-to-the-latest-release"]`).length).toBe(0) - expect($('a[href="/en/enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release"]').length).toBeGreaterThan(0) + expect( + page.markdown.includes( + '(/enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release/)' + ) + ).toBe(true) + expect( + $( + `a[href="/en/enterprise-server@${enterpriseServerReleases.latest}/11.10.340/admin/articles/upgrading-to-the-latest-release"]` + ).length + ).toBe(0) + expect( + $('a[href="/en/enterprise/11.10.340/admin/articles/upgrading-to-the-latest-release"]') + .length + ).toBeGreaterThan(0) }) test('does not rewrite links to external redirects', async () => { @@ -139,7 +165,7 @@ describe('Page class', () => { page: { version: nonEnterpriseDefaultVersion }, currentVersion: nonEnterpriseDefaultVersion, currentPath: `/en/${nonEnterpriseDefaultVersion}/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches`, - currentLanguage: 'en' + currentLanguage: 'en', } const rendered = await page.render(context) const $ = cheerio.load(rendered) @@ -153,17 +179,19 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'page-versioned-for-all-enterprise-releases.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) // set version to the latest enterprise version const context = { currentVersion: `enterprise-server@${enterpriseServerReleases.latest}`, currentLanguage: 'en', - enterpriseServerVersions + enterpriseServerVersions, } let rendered = await page.render(context) let $ = cheerio.load(rendered) - expect($.text()).toBe('This text should render on any actively supported version of Enterprise Server') + expect($.text()).toBe( + 'This text should render on any actively supported version of Enterprise Server' + ) expect($.text()).not.toBe('This text should only render on non-Enterprise') // change version to the oldest enterprise version, re-render, and test again; @@ -171,7 +199,9 @@ describe('Page class', () => { context.currentVersion = `enterprise-server@${enterpriseServerReleases.oldestSupported}` rendered = await page.render(context) $ = cheerio.load(rendered) - expect($.text()).toBe('This text should render on any actively supported version of Enterprise Server') + expect($.text()).toBe( + 'This text should render on any actively supported version of Enterprise Server' + ) expect($.text()).not.toBe('This text should only render on non-Enterprise') // change version to non-enterprise, re-render, and test again; @@ -179,7 +209,9 @@ describe('Page class', () => { context.currentVersion = nonEnterpriseDefaultVersion rendered = await page.render(context) $ = cheerio.load(rendered) - expect($.text()).not.toBe('This text should render on any actively supported version of Enterprise Server') + expect($.text()).not.toBe( + 'This text should render on any actively supported version of Enterprise Server' + ) expect($.text()).toBe('This text should only render on non-Enterprise') }) @@ -188,14 +220,16 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'page-versioned-for-next-enterprise-release.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) // set version to 3.0 const context = { currentVersion: 'enterprise-server@3.0', - currentLanguage: 'en' + currentLanguage: 'en', } - await expect(() => { return page.render(context) }).not.toThrow() + await expect(() => { + return page.render(context) + }).not.toThrow() }) test('support next GitHub AE version in frontmatter', async () => { @@ -203,14 +237,16 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'page-versioned-for-ghae-next.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) // set version to @latest const context = { currentVersion: 'github-ae@latest', - currentLanguage: 'en' + currentLanguage: 'en', } - await expect(() => { return page.render(context) }).not.toThrow() + await expect(() => { + return page.render(context) + }).not.toThrow() }) }) @@ -223,21 +259,21 @@ describe('Page class', () => { let page = await Page.init({ relativePath: 'github/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures/products'), - languageCode: 'en' + languageCode: 'en', }) expect(page.parentProductId).toBe('github') page = await Page.init({ relativePath: 'actions/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures/products'), - languageCode: 'en' + languageCode: 'en', }) expect(page.parentProductId).toBe('actions') page = await Page.init({ relativePath: 'admin/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures/products'), - languageCode: 'en' + languageCode: 'en', }) expect(page.parentProductId).toBe('admin') }) @@ -250,36 +286,68 @@ describe('Page class', () => { test('has a key for every supported enterprise version (and no deprecated versions)', async () => { const page = await Page.init(opts) - const pageVersions = page.permalinks.map(permalink => permalink.pageVersion) - expect(enterpriseServerReleases.supported.every(version => pageVersions.includes(`enterprise-server@${version}`))).toBe(true) - expect(enterpriseServerReleases.deprecated.every(version => !pageVersions.includes(`enterprise-server@${version}`))).toBe(true) + const pageVersions = page.permalinks.map((permalink) => permalink.pageVersion) + expect( + enterpriseServerReleases.supported.every((version) => + pageVersions.includes(`enterprise-server@${version}`) + ) + ).toBe(true) + expect( + enterpriseServerReleases.deprecated.every( + (version) => !pageVersions.includes(`enterprise-server@${version}`) + ) + ).toBe(true) }) test('sets versioned values', async () => { const page = await Page.init(opts) - const expectedPath = 'github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches' - expect(page.permalinks.find(permalink => permalink.pageVersion === nonEnterpriseDefaultVersion).href).toBe(`/en/${expectedPath}`) - expect(page.permalinks.find(permalink => permalink.pageVersion === `enterprise-server@${enterpriseServerReleases.oldestSupported}`).href).toBe(`/en/enterprise-server@${enterpriseServerReleases.oldestSupported}/${expectedPath}`) + const expectedPath = + 'github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches' + expect( + page.permalinks.find((permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion) + .href + ).toBe(`/en/${expectedPath}`) + expect( + page.permalinks.find( + (permalink) => + permalink.pageVersion === + `enterprise-server@${enterpriseServerReleases.oldestSupported}` + ).href + ).toBe(`/en/enterprise-server@${enterpriseServerReleases.oldestSupported}/${expectedPath}`) }) test('homepage permalinks', async () => { const page = await Page.init({ relativePath: 'index.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) - expect(page.permalinks.find(permalink => permalink.pageVersion === nonEnterpriseDefaultVersion).href).toBe('/en') - expect(page.permalinks.find(permalink => permalink.pageVersion === `enterprise-server@${enterpriseServerReleases.oldestSupported}`).href).toBe(`/en/enterprise-server@${enterpriseServerReleases.oldestSupported}`) + expect( + page.permalinks.find((permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion) + .href + ).toBe('/en') + expect( + page.permalinks.find( + (permalink) => + permalink.pageVersion === + `enterprise-server@${enterpriseServerReleases.oldestSupported}` + ).href + ).toBe(`/en/enterprise-server@${enterpriseServerReleases.oldestSupported}`) }) test('permalinks for dotcom-only pages', async () => { const page = await Page.init({ - relativePath: 'github/authenticating-to-github/troubleshooting-ssh/using-ssh-over-the-https-port.md', + relativePath: + 'github/authenticating-to-github/troubleshooting-ssh/using-ssh-over-the-https-port.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) - const expectedPath = '/en/github/authenticating-to-github/troubleshooting-ssh/using-ssh-over-the-https-port' - expect(page.permalinks.find(permalink => permalink.pageVersion === nonEnterpriseDefaultVersion).href).toBe(expectedPath) + const expectedPath = + '/en/github/authenticating-to-github/troubleshooting-ssh/using-ssh-over-the-https-port' + expect( + page.permalinks.find((permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion) + .href + ).toBe(expectedPath) expect(page.permalinks.length).toBe(1) }) @@ -287,10 +355,17 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'products/admin/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) - expect(page.permalinks.find(permalink => permalink.pageVersion === `enterprise-server@${enterpriseServerReleases.latest}`).href).toBe(`/en/enterprise-server@${enterpriseServerReleases.latest}/products/admin/some-category/some-article`) - const pageVersions = page.permalinks.map(permalink => permalink.pageVersion) + expect( + page.permalinks.find( + (permalink) => + permalink.pageVersion === `enterprise-server@${enterpriseServerReleases.latest}` + ).href + ).toBe( + `/en/enterprise-server@${enterpriseServerReleases.latest}/products/admin/some-category/some-article` + ) + const pageVersions = page.permalinks.map((permalink) => permalink.pageVersion) expect(pageVersions.length).toBeGreaterThan(1) expect(pageVersions.includes(nonEnterpriseDefaultVersion)).toBe(false) }) @@ -299,21 +374,30 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'products/actions/some-category/some-article.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) - expect(page.permalinks.find(permalink => permalink.pageVersion === nonEnterpriseDefaultVersion).href).toBe('/en/products/actions/some-category/some-article') + expect( + page.permalinks.find((permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion) + .href + ).toBe('/en/products/actions/some-category/some-article') expect(page.permalinks.length).toBe(1) }) test('permalinks for non-GitHub.com products with Enterprise versions', async () => { const page = await Page.init({ - relativePath: '/insights/installing-and-configuring-github-insights/installing-and-updating-github-insights/about-github-insights.md', + relativePath: + '/insights/installing-and-configuring-github-insights/installing-and-updating-github-insights/about-github-insights.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) const expectedPath = `/en/enterprise-server@${enterpriseServerReleases.latest}/insights/installing-and-configuring-github-insights/installing-and-updating-github-insights/about-github-insights` - expect(page.permalinks.find(permalink => permalink.pageVersion === `enterprise-server@${enterpriseServerReleases.latest}`).href).toBe(expectedPath) - const pageVersions = page.permalinks.map(permalink => permalink.pageVersion) + expect( + page.permalinks.find( + (permalink) => + permalink.pageVersion === `enterprise-server@${enterpriseServerReleases.latest}` + ).href + ).toBe(expectedPath) + const pageVersions = page.permalinks.map((permalink) => permalink.pageVersion) expect(pageVersions.length).toBeGreaterThan(1) expect(pageVersions.includes(nonEnterpriseDefaultVersion)).toBe(false) }) @@ -326,7 +410,7 @@ describe('Page class', () => { page = await Page.init({ relativePath: 'article-with-learning-tracks.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) }) @@ -336,7 +420,7 @@ describe('Page class', () => { 'track_2', 'non_existing_track', '{% if currentVersion == "free-pro-team@latest" %}dotcom_only_track{% endif %}', - '{% if currentVersion != "free-pro-team@latest" %}enterprise_only_track{% endif %}' + '{% if currentVersion != "free-pro-team@latest" %}enterprise_only_track{% endif %}', ]) }) @@ -356,28 +440,30 @@ describe('Page class', () => { title: 'title', description: 'description', guides, - featured_track: '{% if currentVersion == "free-pro-team@latest" %}true{% else %}false{% endif %}' + featured_track: + '{% if currentVersion == "free-pro-team@latest" %}true{% else %}false{% endif %}', }, track_2: { title: 'title', description: 'description', guides, - featured_track: '{% if enterpriseServerVersions contains currentVersion %}true{% else %}false{% endif %}' + featured_track: + '{% if enterpriseServerVersions contains currentVersion %}true{% else %}false{% endif %}', }, dotcom_only_track: { title: 'title', description: 'description', - guides + guides, }, enterprise_only_track: { title: 'title', description: 'description', - guides - } - } - } - } - } + guides, + }, + }, + }, + }, + }, } // Test that Liquid versioning is respected during rendering. // Start with Dotcom. @@ -387,7 +473,7 @@ describe('Page class', () => { // expect(getLinkData).toHaveBeenCalledWith(guides, context) // Tracks for dotcom should exclude enterprise_only_track and the featured track_1. expect(page.learningTracks).toHaveLength(2) - const dotcomTrackNames = page.learningTracks.map(t => t.trackName) + const dotcomTrackNames = page.learningTracks.map((t) => t.trackName) expect(dotcomTrackNames.includes('track_2')).toBe(true) expect(dotcomTrackNames.includes('dotcom_only_track')).toBe(true) expect(page.featuredTrack.trackName === 'track_1').toBeTruthy() @@ -398,7 +484,7 @@ describe('Page class', () => { await page.render(context) // Tracks for enterprise should exclude dotcom_only_track and the featured track_2. expect(page.learningTracks).toHaveLength(2) - const ghesTrackNames = page.learningTracks.map(t => t.trackName) + const ghesTrackNames = page.learningTracks.map((t) => t.trackName) expect(ghesTrackNames.includes('track_1')).toBe(true) expect(ghesTrackNames.includes('enterprise_only_track')).toBe(true) expect(page.featuredTrack.trackName === 'track_1').toBeFalsy() @@ -413,7 +499,7 @@ describe('Page class', () => { page = await Page.init({ relativePath: 'article-with-includeGuides.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) }) @@ -434,15 +520,13 @@ describe('Page class', () => { // const guides = ['/path/guide1', '/path/guide2', '/path/guide3'] const context = { currentVersion: nonEnterpriseDefaultVersion, - currentLanguage: 'en' + currentLanguage: 'en', } await page.render(context) // expect(getLinkData).toHaveBeenCalledWith(guides, context) expect(page.includeGuides).toHaveLength(3) expect(page.allTopics).toHaveLength(4) - expect(page.allTopics).toEqual( - expect.arrayContaining(['Spring', 'Summer', 'Fall', 'Winter']) - ) + expect(page.allTopics).toEqual(expect.arrayContaining(['Spring', 'Summer', 'Fall', 'Winter'])) }) }) @@ -458,9 +542,9 @@ describe('Page class', () => { describe('Page.getLanguageVariants()', () => { it('returns an array of language variants of the given URL', () => { const variants = Page.getLanguageVariants('/en') - expect(variants.every(variant => variant.name)).toBe(true) - expect(variants.every(variant => variant.code)).toBe(true) - expect(variants.every(variant => variant.href)).toBe(true) + expect(variants.every((variant) => variant.name)).toBe(true) + expect(variants.every((variant) => variant.code)).toBe(true) + expect(variants.every((variant) => variant.href)).toBe(true) }) it('works for the homepage', () => { @@ -470,8 +554,12 @@ describe('Page class', () => { }) it('works for enterprise URLs', () => { - const variants = Page.getLanguageVariants(`/ja/enterprise/${enterpriseServerReleases.oldestSupported}/user/articles/github-glossary`) - expect(variants.find(({ code }) => code === 'en').href).toBe(`/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/articles/github-glossary`) + const variants = Page.getLanguageVariants( + `/ja/enterprise/${enterpriseServerReleases.oldestSupported}/user/articles/github-glossary` + ) + expect(variants.find(({ code }) => code === 'en').href).toBe( + `/en/enterprise/${enterpriseServerReleases.oldestSupported}/user/articles/github-glossary` + ) // expect(variants.find(({ code }) => code === 'ja').href).toBe('/ja/enterprise/2.14/user/articles/github-glossary') }) }) @@ -480,25 +568,25 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'article-with-mislocalized-frontmatter.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'ja' + languageCode: 'ja', }) expect(page.mapTopic).toBe(true) }) describe('page.versions frontmatter', () => { test.skip('pages that apply to older enterprise versions', async () => { - // There are none of these in the content at this time! + // There are none of these in the content at this time! }) test.skip('pages that apply to newer enterprise versions', async () => { - // There are none of these in the content at this time! + // There are none of these in the content at this time! }) test('pages that use short names in versions frontmatter', async () => { const page = await Page.init({ relativePath: 'short-versions.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) expect(page.versions.fpt).toBe('*') expect(page.versions.ghes).toBe('>3.0') @@ -512,7 +600,7 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'index.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) expect(page.versions).toBe('*') }) @@ -521,7 +609,7 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'admin/index.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) expect(nonEnterpriseDefaultPlan in page.versions).toBe(false) @@ -546,22 +634,22 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'feature-versions-frontmatter.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) - + // Test the raw page data. expect(page.versions.fpt).toBe('*') expect(page.versions.ghes).toBe('>2.21') expect(page.versions.ghae).toBeUndefined() // The `feature` prop gets deleted by lib/get-applicable-versions, so it's undefined. expect(page.versions.feature).toBeUndefined() - - // Test the resolved versioning, where GHES releases specified in frontmatter and in + + // Test the resolved versioning, where GHES releases specified in frontmatter and in // feature versions are combined (i.e., one doesn't overwrite the other). - // We can't test that GHES 2.21 is _not_ included here (which it shouldn't be), + // We can't test that GHES 2.21 is _not_ included here (which it shouldn't be), // because lib/get-applicable-versions only returns currently supported versions, // so as soon as 2.21 is deprecated, a test for that _not_ to exist will not be meaningful. - // But by testing that the _latest_ GHES version is returned, we can ensure that the + // But by testing that the _latest_ GHES version is returned, we can ensure that the // the frontmatter GHES `*` is not being overwritten by the placeholder's GHES `<2.22`. expect(page.applicableVersions.includes('free-pro-team@latest')).toBe(true) expect(page.applicableVersions.includes(`enterprise-server@${latest}`)).toBe(true) @@ -574,9 +662,10 @@ describe('Page class', () => { describe('platform specific content', () => { test('page.defaultPlatform frontmatter', async () => { const page = await Page.init({ - relativePath: 'actions/hosting-your-own-runners/configuring-the-self-hosted-runner-application-as-a-service.md', + relativePath: + 'actions/hosting-your-own-runners/configuring-the-self-hosted-runner-application-as-a-service.md', basePath: path.join(__dirname, '../../content'), - languageCode: 'en' + languageCode: 'en', }) expect(page.defaultPlatform).toBeDefined() expect(page.defaultPlatform).toBe('linux') @@ -588,7 +677,7 @@ describe('Page class', () => { const page = await Page.init({ relativePath: 'default-tool.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) expect(page.defaultTool).toBeDefined() expect(page.defaultTool).toBe('cli') @@ -598,11 +687,11 @@ describe('Page class', () => { describe('catches errors thrown in Page class', () => { test('frontmatter parsing error', async () => { - async function getPage () { + async function getPage() { return await Page.init({ relativePath: 'page-with-frontmatter-error.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) } @@ -610,11 +699,11 @@ describe('catches errors thrown in Page class', () => { }) test('missing versions frontmatter', async () => { - async function getPage () { + async function getPage() { return await Page.init({ relativePath: 'page-with-missing-product-versions.md', basePath: path.join(__dirname, '../fixtures'), - languageCode: 'en' + languageCode: 'en', }) } @@ -622,11 +711,11 @@ describe('catches errors thrown in Page class', () => { }) test('English page with a version in frontmatter that its parent product is not available in', async () => { - async function getPage () { + async function getPage() { return await Page.init({ relativePath: 'admin/some-category/some-article-with-mismatched-versions-frontmatter.md', basePath: path.join(__dirname, '../fixtures/products'), - languageCode: 'en' + languageCode: 'en', }) } @@ -634,14 +723,16 @@ describe('catches errors thrown in Page class', () => { }) test('non-English page with a version in frontmatter that its parent product is not available in', async () => { - async function getPage () { + async function getPage() { return await Page.init({ relativePath: 'admin/some-category/some-article-with-mismatched-versions-frontmatter.md', basePath: path.join(__dirname, '../fixtures/products'), - languageCode: 'es' + languageCode: 'es', }) } - await expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/) + await expect(getPage).rejects.toThrowError( + /`versions` frontmatter.*? product is not available in/ + ) }) }) diff --git a/tests/unit/pages.js b/tests/unit/pages.js index df36514fc786..2319657ab01f 100644 --- a/tests/unit/pages.js +++ b/tests/unit/pages.js @@ -30,17 +30,26 @@ describe('pages module', () => { }) test('every page has a `languageCode`', async () => { - expect(pages.every(page => languageCodes.includes(page.languageCode))).toBe(true) + expect(pages.every((page) => languageCodes.includes(page.languageCode))).toBe(true) }) test('every page has a non-empty `permalinks` array', async () => { const brokenPages = pages - .filter(page => !Array.isArray(page.permalinks) || page.permalinks.length === 0) + .filter((page) => !Array.isArray(page.permalinks) || page.permalinks.length === 0) // Ignore pages that only have "next" versions specified and therefore no permalinks; // These pages are not broken, they just won't render in the currently supported versions. - .filter(page => !Object.values(page.versions).every(pageVersion => checkIfNextVersionOnly(pageVersion))) - - const expectation = JSON.stringify(brokenPages.map(page => page.fullPath), null, 2) + .filter( + (page) => + !Object.values(page.versions).every((pageVersion) => + checkIfNextVersionOnly(pageVersion) + ) + ) + + const expectation = JSON.stringify( + brokenPages.map((page) => page.fullPath), + null, + 2 + ) expect(brokenPages.length, expectation).toBe(0) }) @@ -48,14 +57,14 @@ describe('pages module', () => { const englishPages = chain(pages) .filter(['languageCode', 'en']) .filter('redirect_from') - .map(pages => pick(pages, ['redirect_from', 'applicableVersions'])) + .map((pages) => pick(pages, ['redirect_from', 'applicableVersions'])) .value() const versionedRedirects = [] - englishPages.forEach(page => { - page.redirect_from.forEach(redirect => { - page.applicableVersions.forEach(version => { + englishPages.forEach((page) => { + page.redirect_from.forEach((redirect) => { + page.applicableVersions.forEach((version) => { versionedRedirects.push(removeFPTFromPath(path.posix.join('/', version, redirect))) }) }) @@ -66,31 +75,41 @@ describe('pages module', () => { return acc }, []) - const message = `Found ${duplicates.length} duplicate redirect_from ${duplicates.length === 1 ? 'path' : 'paths'}.\n + const message = `Found ${duplicates.length} duplicate redirect_from ${ + duplicates.length === 1 ? 'path' : 'paths' + }.\n ${duplicates.join('\n')}` expect(duplicates.length, message).toBe(0) }) test('every English page has a filename that matches its slugified title', async () => { const nonMatches = pages - .filter(page => { + .filter((page) => { slugger.reset() - return page.languageCode === 'en' && // only check English - !page.relativePath.includes('index.md') && // ignore TOCs - !page.allowTitleToDifferFromFilename && // ignore docs with override - slugger.slug(entities.decode(page.title)) !== path.basename(page.relativePath, '.md') + return ( + page.languageCode === 'en' && // only check English + !page.relativePath.includes('index.md') && // ignore TOCs + !page.allowTitleToDifferFromFilename && // ignore docs with override + slugger.slug(entities.decode(page.title)) !== path.basename(page.relativePath, '.md') + ) }) // make the output easier to read - .map(page => { - return JSON.stringify({ - file: path.basename(page.relativePath), - title: page.title, - path: page.fullPath - }, null, 2) + .map((page) => { + return JSON.stringify( + { + file: path.basename(page.relativePath), + title: page.title, + path: page.fullPath, + }, + null, + 2 + ) }) const message = ` - Found ${nonMatches.length} ${nonMatches.length === 1 ? 'file' : 'files'} that do not match their slugified titles.\n + Found ${nonMatches.length} ${ + nonMatches.length === 1 ? 'file' : 'files' + } that do not match their slugified titles.\n ${nonMatches.join('\n')}\n To fix, run script/reconcile-filenames-with-ids.js\n\n` @@ -100,11 +119,12 @@ describe('pages module', () => { test('every page has valid frontmatter', async () => { const frontmatterErrors = chain(pages) // .filter(page => page.languageCode === 'en') - .map(page => page.frontmatterErrors) + .map((page) => page.frontmatterErrors) .flatten() .value() - const failureMessage = JSON.stringify(frontmatterErrors, null, 2) + + const failureMessage = + JSON.stringify(frontmatterErrors, null, 2) + '\n\n' + chain(frontmatterErrors).map('filepath').join('\n').value() @@ -122,7 +142,7 @@ describe('pages module', () => { } catch (error) { liquidErrors.push({ filename: page.fullPath, - error: error.message + error: error.message, }) } } @@ -133,12 +153,12 @@ describe('pages module', () => { test.skip('every non-English page has a matching English page', async () => { const englishPaths = chain(pages) - .filter(page => page.languageCode === 'en') - .map(page => page.relativePath) + .filter((page) => page.languageCode === 'en') + .map((page) => page.relativePath) .value() const nonEnglishPaths = chain(pages) - .filter(page => page.languageCode !== 'en') - .map(page => page.relativePath) + .filter((page) => page.languageCode !== 'en') + .map((page) => page.relativePath) .uniq() .value() @@ -166,7 +186,7 @@ describe('pages module', () => { }) test('has an identical key list to the deep permalinks of the array', async () => { - const allPermalinks = pages.flatMap(page => page.permalinks.map(pl => pl.href)).sort() + const allPermalinks = pages.flatMap((page) => page.permalinks.map((pl) => pl.href)).sort() const allPageUrls = Object.keys(pageMap).sort() expect(allPageUrls).toEqual(allPermalinks) diff --git a/tests/unit/permalink.js b/tests/unit/permalink.js index bf95c7d59bf6..1b5356753046 100644 --- a/tests/unit/permalink.js +++ b/tests/unit/permalink.js @@ -10,11 +10,18 @@ describe('Permalink class', () => { test('derives info for unversioned homepage', () => { const versions = { 'free-pro-team': '*', - 'enterprise-server': '*' + 'enterprise-server': '*', } - const permalinks = Permalink.derive('en', 'index.md', 'Hello World', getApplicableVersions(versions)) + const permalinks = Permalink.derive( + 'en', + 'index.md', + 'Hello World', + getApplicableVersions(versions) + ) expect(permalinks.length).toBeGreaterThan(1) - const homepagePermalink = permalinks.find(permalink => permalink.pageVersion === nonEnterpriseDefaultVersion) + const homepagePermalink = permalinks.find( + (permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion + ) expect(homepagePermalink.href).toBe('/en') }) @@ -25,19 +32,34 @@ describe('Permalink class', () => { }) test('derives info for enterprise server versioned homepage', () => { - const permalink = new Permalink('en', `enterprise-server@${enterpriseServerReleases.latest}`, 'index.md', 'Hello World') + const permalink = new Permalink( + 'en', + `enterprise-server@${enterpriseServerReleases.latest}`, + 'index.md', + 'Hello World' + ) expect(permalink.pageVersionTitle).toBe(`Enterprise Server ${enterpriseServerReleases.latest}`) expect(permalink.href).toBe(`/en/enterprise-server@${enterpriseServerReleases.latest}`) }) test('derives info for GitHub.com homepage', () => { - const permalink = new Permalink('en', nonEnterpriseDefaultVersion, 'github/index.md', 'Hello World') + const permalink = new Permalink( + 'en', + nonEnterpriseDefaultVersion, + 'github/index.md', + 'Hello World' + ) expect(permalink.pageVersionTitle).toBe('GitHub.com') expect(permalink.href).toBe('/en/github') }) test('derives info for enterprise version of GitHub.com homepage', () => { - const permalink = new Permalink('en', `enterprise-server@${enterpriseServerReleases.latest}`, 'github/index.md', 'Hello World') + const permalink = new Permalink( + 'en', + `enterprise-server@${enterpriseServerReleases.latest}`, + 'github/index.md', + 'Hello World' + ) expect(permalink.pageVersionTitle).toBe(`Enterprise Server ${enterpriseServerReleases.latest}`) expect(permalink.href).toBe(`/en/enterprise-server@${enterpriseServerReleases.latest}/github`) }) diff --git a/tests/unit/products.js b/tests/unit/products.js index c44897574c48..df8c7a923951 100644 --- a/tests/unit/products.js +++ b/tests/unit/products.js @@ -14,7 +14,7 @@ describe('products module', () => { }) test('every product is valid', () => { - Object.values(productMap).forEach(product => { + Object.values(productMap).forEach((product) => { const { valid, errors } = revalidator.validate(product, schema) const expectation = JSON.stringify({ product, errors }, null, 2) expect(valid, expectation).toBe(true) @@ -28,8 +28,18 @@ describe('mobile-only products nav', () => { expect((await getDOM('/github'))('#current-product').text().trim()).toBe('GitHub.com') // Enterprise server - expect((await getDOM('/en/enterprise/admin'))('#current-product').text().trim()).toBe('Enterprise administrators') - expect((await getDOM('/en/enterprise/user/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address'))('#current-product').text().trim()).toBe('GitHub.com') + expect((await getDOM('/en/enterprise/admin'))('#current-product').text().trim()).toBe( + 'Enterprise administrators' + ) + expect( + ( + await getDOM( + '/en/enterprise/user/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address' + ) + )('#current-product') + .text() + .trim() + ).toBe('GitHub.com') expect((await getDOM('/desktop'))('#current-product').text().trim()).toBe('GitHub Desktop') @@ -54,7 +64,9 @@ describe('products middleware', () => { }) test('adds res.context.currentProduct object', async () => { - const currentProduct = await getJSON(`/en/${nonEnterpriseDefaultVersion}/github?json=currentProduct`) + const currentProduct = await getJSON( + `/en/${nonEnterpriseDefaultVersion}/github?json=currentProduct` + ) expect(currentProduct).toBe('github') }) }) diff --git a/tests/unit/read-frontmatter.js b/tests/unit/read-frontmatter.js index 7177d098779b..8adafbf65486 100644 --- a/tests/unit/read-frontmatter.js +++ b/tests/unit/read-frontmatter.js @@ -36,7 +36,7 @@ I am content. const expectedError = { filepath: 'path/to/file.md', message: 'YML parsing error!', - reason: 'invalid frontmatter entry' + reason: 'invalid frontmatter entry', } expect(errors[0]).toEqual(expectedError) }) @@ -54,7 +54,7 @@ I am content. const expectedError = { filepath: 'path/to/file.md', message: 'YML parsing error!', - reason: 'bad indentation of a mapping entry' + reason: 'bad indentation of a mapping entry', } expect(errors[0]).toEqual(expectedError) }) @@ -65,12 +65,12 @@ I am content. const schema = { properties: { title: { - type: 'string' + type: 'string', }, meaning_of_life: { - type: 'number' - } - } + type: 'number', + }, + }, } const { data, content, errors } = parse(fixture1, { schema }) @@ -85,9 +85,9 @@ I am content. properties: { meaning_of_life: { type: 'number', - minimum: 50 - } - } + minimum: 50, + }, + }, } const { data, content, errors } = parse(fixture1, { schema }) @@ -100,7 +100,7 @@ I am content. property: 'meaning_of_life', expected: 50, actual: 42, - message: 'must be greater than or equal to 50' + message: 'must be greater than or equal to 50', } expect(errors[0]).toEqual(expectedError) }) @@ -110,9 +110,9 @@ I am content. properties: { yet_another_key: { type: 'string', - required: true - } - } + required: true, + }, + }, } const { errors } = parse(fixture1, { schema }) @@ -122,7 +122,7 @@ I am content. property: 'yet_another_key', expected: true, actual: undefined, - message: 'is required' + message: 'is required', } expect(errors[0]).toEqual(expectedError) }) @@ -132,9 +132,9 @@ I am content. const schema = { properties: { age: { - type: 'number' - } - } + type: 'number', + }, + }, } it('creates errors for undocumented keys if `validateKeyNames` is true', () => { @@ -144,13 +144,13 @@ I am content. { property: 'title', message: 'not allowed. Allowed properties are: age', - filepath: 'path/to/file.md' + filepath: 'path/to/file.md', }, { property: 'meaning_of_life', message: 'not allowed. Allowed properties are: age', - filepath: 'path/to/file.md' - } + filepath: 'path/to/file.md', + }, ] expect(errors).toEqual(expectedErrors) }) @@ -166,20 +166,21 @@ I am content. const schema = { properties: { meaning_of_life: { - type: 'number' + type: 'number', }, title: { - type: 'string' - } - } + type: 'string', + }, + }, } const { errors } = parse(fixture1, { schema, validateKeyOrder: true, filepath }) const expectedErrors = [ { property: 'keys', - message: 'keys must be in order. Current: title,meaning_of_life; Expected: meaning_of_life,title', - filepath: 'path/to/file.md' - } + message: + 'keys must be in order. Current: title,meaning_of_life; Expected: meaning_of_life,title', + filepath: 'path/to/file.md', + }, ] expect(errors).toEqual(expectedErrors) }) @@ -188,12 +189,12 @@ I am content. const schema = { properties: { title: { - type: 'string' + type: 'string', }, meaning_of_life: { - type: 'number' - } - } + type: 'number', + }, + }, } const { errors } = parse(fixture1, { schema, validateKeyOrder: true }) expect(errors.length).toBe(0) @@ -204,16 +205,16 @@ I am content. properties: { title: { type: 'string', - required: true + required: true, }, yet_another_key: { - type: 'string' + type: 'string', }, meaning_of_life: { type: 'number', - required: true - } - } + required: true, + }, + }, } const { errors } = parse(fixture1, { schema, validateKeyOrder: true }) expect(errors.length).toBe(0) diff --git a/tests/unit/redis-accessor.js b/tests/unit/redis-accessor.js index 2454ab8d6891..58296f1889b8 100644 --- a/tests/unit/redis-accessor.js +++ b/tests/unit/redis-accessor.js @@ -14,7 +14,12 @@ describe('RedisAccessor', () => { test('has expected instance properties', async () => { const instance = new RedisAccessor() - expect(Object.keys(instance).sort()).toEqual(['_allowGetFailures', '_allowSetFailures', '_client', '_prefix']) + expect(Object.keys(instance).sort()).toEqual([ + '_allowGetFailures', + '_allowSetFailures', + '_client', + '_prefix', + ]) }) test('has expected static methods', async () => { @@ -124,14 +129,14 @@ describe('RedisAccessor', () => { expect( RedisAccessor.translateSetArguments({ newOnly: true, - expireIn: 20 + expireIn: 20, }) ).toEqual(['NX', 'PX', 20]) expect( RedisAccessor.translateSetArguments({ existingOnly: true, - expireIn: 20 + expireIn: 20, }) ).toEqual(['XX', 'PX', 20]) @@ -139,48 +144,46 @@ describe('RedisAccessor', () => { RedisAccessor.translateSetArguments({ existingOnly: true, expireIn: 20, - rollingExpiration: false + rollingExpiration: false, }) ).toEqual(['XX', 'PX', 20, 'KEEPTTL']) expect( RedisAccessor.translateSetArguments({ existingOnly: true, - rollingExpiration: false + rollingExpiration: false, }) ).toEqual(['XX', 'KEEPTTL']) }) test('throws a misconfiguration error if options `newOnly` and `existingOnly` are both set to true', async () => { - expect( - () => RedisAccessor.translateSetArguments({ newOnly: true, existingOnly: true }) - ).toThrowError( - new TypeError('Misconfiguration: entry cannot be both new and existing') - ) + expect(() => + RedisAccessor.translateSetArguments({ newOnly: true, existingOnly: true }) + ).toThrowError(new TypeError('Misconfiguration: entry cannot be both new and existing')) }) test('throws a misconfiguration error if option `expireIn` is set to a finite number that rounds to less than 1', async () => { - const misconfigurationError = new TypeError('Misconfiguration: cannot set a TTL of less than 1 millisecond') + const misconfigurationError = new TypeError( + 'Misconfiguration: cannot set a TTL of less than 1 millisecond' + ) - expect( - () => RedisAccessor.translateSetArguments({ expireIn: 0 }) - ).toThrowError(misconfigurationError) + expect(() => RedisAccessor.translateSetArguments({ expireIn: 0 })).toThrowError( + misconfigurationError + ) - expect( - () => RedisAccessor.translateSetArguments({ expireIn: -1 }) - ).toThrowError(misconfigurationError) + expect(() => RedisAccessor.translateSetArguments({ expireIn: -1 })).toThrowError( + misconfigurationError + ) - expect( - () => RedisAccessor.translateSetArguments({ expireIn: 0.4 }) - ).toThrowError(misconfigurationError) + expect(() => RedisAccessor.translateSetArguments({ expireIn: 0.4 })).toThrowError( + misconfigurationError + ) }) test('throws a misconfiguration error if option `rollingExpiration` is set to false but `newOnly` is set to true', async () => { - expect( - () => RedisAccessor.translateSetArguments({ newOnly: true, rollingExpiration: false }) - ).toThrowError( - new TypeError('Misconfiguration: cannot keep an existing TTL on a new entry') - ) + expect(() => + RedisAccessor.translateSetArguments({ newOnly: true, rollingExpiration: false }) + ).toThrowError(new TypeError('Misconfiguration: cannot keep an existing TTL on a new entry')) }) }) @@ -236,8 +239,7 @@ Error: Redis ReplyError` await expect(instance.set('myKey', 'myValue')).rejects.toThrowError( new Error(`Failed to set value in Redis. Key: myPrefix:myKey -Error: Redis ReplyError` - ) +Error: Redis ReplyError`) ) expect(consoleErrorSpy).not.toBeCalled() @@ -345,8 +347,7 @@ Error: Redis ReplyError` await expect(instance.get('myKey')).rejects.toThrowError( new Error(`Failed to get value from Redis. Key: myPrefix:myKey -Error: Redis ReplyError` - ) +Error: Redis ReplyError`) ) expect(consoleErrorSpy).not.toBeCalled() diff --git a/tests/unit/render-content.js b/tests/unit/render-content.js index 417061d641a5..1f490a7b290c 100644 --- a/tests/unit/render-content.js +++ b/tests/unit/render-content.js @@ -4,44 +4,36 @@ import { EOL } from 'os' // Use platform-specific line endings for realistic tests when templates have // been loaded from disk -const nl = str => str.replace(/\n/g, EOL) +const nl = (str) => str.replace(/\n/g, EOL) describe('renderContent', () => { - test( - 'takes a template and a context and returns a string (async)', - async () => { - const template = 'my favorite color is {{ color }}.' - const context = { color: 'orange' } - const output = await renderContent(template, context) - expect(output, '<p>my favorite color is orange.</p>') - } - ) + test('takes a template and a context and returns a string (async)', async () => { + const template = 'my favorite color is {{ color }}.' + const context = { color: 'orange' } + const output = await renderContent(template, context) + expect(output, '<p>my favorite color is orange.</p>') + }) test('preserves content within {% raw %} tags', async () => { - const template = nl( - 'For example: {% raw %}{% include cool_header.html %}{% endraw %}.' - ) + const template = nl('For example: {% raw %}{% include cool_header.html %}{% endraw %}.') const expected = '<p>For example: {% include cool_header.html %}.</p>' const output = await renderContent(template) expect(output).toBe(expected) }) - test( - 'removes extra newlines to prevent lists from breaking', - async () => { - const template = nl(` + test('removes extra newlines to prevent lists from breaking', async () => { + const template = nl(` 1. item one 1. item two 1. item three`) - const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) - expect($('ol').length).toBe(1) - expect($('ol > li').length).toBe(3) - } - ) + const html = await renderContent(template) + const $ = cheerio.load(html, { xmlMode: true }) + expect($('ol').length).toBe(1) + expect($('ol > li').length).toBe(3) + }) test('removes extra newlines from lists of links', async () => { const template = nl(`- <a>item</a> @@ -75,32 +67,29 @@ describe('renderContent', () => { expect(err).toBeTruthy() }) - test( - 'warns and throws on rendering errors when the file name is passed', - async () => { - const template = 1 - const context = {} - - let err - let warned = false - - const error = console.error - console.error = message => { - expect(message, 'renderContent failed on file: name') - console.error = error - warned = true - } - - try { - await renderContent(template, context, { filename: 'name' }) - } catch (_err) { - err = _err - } - - expect(err).toBeTruthy() - expect(warned).toBeTruthy() + test('warns and throws on rendering errors when the file name is passed', async () => { + const template = 1 + const context = {} + + let err + let warned = false + + const error = console.error + console.error = (message) => { + expect(message, 'renderContent failed on file: name') + console.error = error + warned = true + } + + try { + await renderContent(template, context, { filename: 'name' }) + } catch (_err) { + err = _err } - ) + + expect(err).toBeTruthy() + expect(warned).toBeTruthy() + }) test('renders empty templates', async () => { const template = '' @@ -113,7 +102,7 @@ describe('renderContent', () => { const template = '<beep></beep>' const context = {} const output = await renderContent(template, context, { - encodeEntities: true + encodeEntities: true, }) expect(output).toBe('<p><beep></beep></p>') }) @@ -128,29 +117,22 @@ describe('renderContent', () => { const html = await renderContent(template) const $ = cheerio.load(html, { xmlMode: true }) expect( - $.html().includes( - '"<a href="/articles/about-issues">About issues</a>."' - ) + $.html().includes('"<a href="/articles/about-issues">About issues</a>."') ).toBeTruthy() }) - test( - 'does not render newlines around inline code in tables', - async () => { - const template = nl(` + test('does not render newlines around inline code in tables', async () => { + const template = nl(` | Package manager | formats | | --- | --- | | Python | \`requirements.txt\`, \`pipfile.lock\` `) - const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) - expect( - $.html().includes( - '<code>requirements.txt</code>, <code>pipfile.lock</code>' - ) - ).toBeTruthy() - } - ) + const html = await renderContent(template) + const $ = cheerio.load(html, { xmlMode: true }) + expect( + $.html().includes('<code>requirements.txt</code>, <code>pipfile.lock</code>') + ).toBeTruthy() + }) test('does not render newlines around emphasis in code', async () => { const template = nl(` @@ -253,18 +235,15 @@ some code ) }) - test( - 'renders a copy button for code blocks with {:copy} annotation', - async () => { - const template = nl(` + test('renders a copy button for code blocks with {:copy} annotation', async () => { + const template = nl(` \`\`\`js{:copy} some code \`\`\`\ `) - const html = await renderContent(template) - const $ = cheerio.load(html) - const el = $('button.js-btn-copy') - expect(el.data('clipboard-text')).toBe('some code') - } - ) + const html = await renderContent(template) + const $ = cheerio.load(html) + const el = $('button.js-btn-copy') + expect(el.data('clipboard-text')).toBe('some code') + }) }) diff --git a/tests/unit/search/parse-page-sections-into-records.js b/tests/unit/search/parse-page-sections-into-records.js index 5142c744a1e5..b2dc67f5cd3f 100644 --- a/tests/unit/search/parse-page-sections-into-records.js +++ b/tests/unit/search/parse-page-sections-into-records.js @@ -5,8 +5,14 @@ import cheerio from 'cheerio' import parsePageSectionsIntoRecords from '../../../script/search/parse-page-sections-into-records.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const fixtures = { - pageWithSections: fs.readFileSync(path.join(__dirname, 'fixtures/page-with-sections.html'), 'utf8'), - pageWithoutSections: fs.readFileSync(path.join(__dirname, 'fixtures/page-without-sections.html'), 'utf8') + pageWithSections: fs.readFileSync( + path.join(__dirname, 'fixtures/page-with-sections.html'), + 'utf8' + ), + pageWithoutSections: fs.readFileSync( + path.join(__dirname, 'fixtures/page-without-sections.html'), + 'utf8' + ), } describe('search parsePageSectionsIntoRecords module', () => { @@ -26,7 +32,7 @@ describe('search parsePageSectionsIntoRecords module', () => { heading: 'First heading', title: 'I am the page title', content: "Here's a paragraph. And another.", - topics: ['topic1', 'topic2', 'GitHub Actions', 'Actions'] + topics: ['topic1', 'topic2', 'GitHub Actions', 'Actions'], }, { objectID: '/example/href#second', @@ -36,8 +42,8 @@ describe('search parsePageSectionsIntoRecords module', () => { heading: 'Second heading', title: 'I am the page title', content: "Here's a paragraph in the second section. And another.", - topics: ['topic1', 'topic2', 'GitHub Actions', 'Actions'] - } + topics: ['topic1', 'topic2', 'GitHub Actions', 'Actions'], + }, ] expect(records).toEqual(expected) @@ -57,8 +63,8 @@ describe('search parsePageSectionsIntoRecords module', () => { breadcrumbs: 'Education / map topic', title: 'A page without sections', content: 'First paragraph. Second paragraph.', - topics: ['key1', 'key2', 'key3', 'Education'] - } + topics: ['key1', 'key2', 'key3', 'Education'], + }, ] expect(records).toEqual(expected) }) diff --git a/tests/unit/search/rank.js b/tests/unit/search/rank.js index e4b51cffd41c..72907da83f62 100644 --- a/tests/unit/search/rank.js +++ b/tests/unit/search/rank.js @@ -5,7 +5,7 @@ test('search custom rankings', () => { ['https://docs.github.com/en/github/actions', 3], ['https://docs.github.com/en/rest/reference', 2], ['https://docs.github.com/en/graphql', 1], - ['https://docs.github.com/en/github/site-policy', 0] + ['https://docs.github.com/en/github/site-policy', 0], ] expectedRankings.forEach(([url, expectedRanking]) => { diff --git a/tests/unit/search/topics.js b/tests/unit/search/topics.js index 9a3014a330c9..7250b09647f2 100644 --- a/tests/unit/search/topics.js +++ b/tests/unit/search/topics.js @@ -7,8 +7,8 @@ import allowedTopics from '../../../data/allowed-topics.js' const contentDir = path.join(process.cwd(), 'content') const topics = walk(contentDir, { includeBasePath: true }) - .filter(filename => filename.endsWith('.md') && !filename.includes('README')) - .map(filename => { + .filter((filename) => filename.endsWith('.md') && !filename.includes('README')) + .map((filename) => { const fileContent = fs.readFileSync(filename, 'utf8') const { data } = readFrontmatter(fileContent) return data.topics || [] diff --git a/tests/unit/toc-links.js b/tests/unit/toc-links.js index c5536ffdbda7..9cdddb3877f1 100644 --- a/tests/unit/toc-links.js +++ b/tests/unit/toc-links.js @@ -10,15 +10,16 @@ describe('toc links', () => { test('every toc link works without redirects', async () => { const pages = await loadPages() - const englishIndexPages = pages - .filter(page => page.languageCode === 'en' && page.relativePath.endsWith('index.md')) + const englishIndexPages = pages.filter( + (page) => page.languageCode === 'en' && page.relativePath.endsWith('index.md') + ) const issues = [] for (const pageVersion of allVersions) { for (const page of englishIndexPages) { // skip page if it doesn't have a permalink for the current product version - if (!page.permalinks.some(permalink => permalink.pageVersion === pageVersion)) continue + if (!page.permalinks.some((permalink) => permalink.pageVersion === pageVersion)) continue // build fake context object for rendering the page const context = { @@ -26,7 +27,7 @@ describe('toc links', () => { pages, redirects: {}, currentLanguage: 'en', - currentVersion: pageVersion + currentVersion: pageVersion, } // ensure all toc pages can render @@ -36,7 +37,7 @@ describe('toc links', () => { issues.push({ 'TOC path': page.relativePath, error: err.message, - pageVersion + pageVersion, }) } } diff --git a/tests/unit/versions.js b/tests/unit/versions.js index 45c20691948d..1423c76a8d6a 100644 --- a/tests/unit/versions.js +++ b/tests/unit/versions.js @@ -15,7 +15,7 @@ describe('versions module', () => { }) test('every version is valid', () => { - Object.values(allVersions).forEach(versionObj => { + Object.values(allVersions).forEach((versionObj) => { const { valid, errors } = revalidator.validate(versionObj, schema) const expectation = JSON.stringify({ versionObj, errors }, null, 2) expect(valid, expectation).toBe(true) diff --git a/webpack.config.js b/webpack.config.js index 276eae1cfa01..f5652e573ffb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,22 +10,22 @@ module.exports = { output: { filename: 'index.js', path: path.resolve(__dirname, 'dist'), - publicPath: '/dist' + publicPath: '/dist', }, stats: 'errors-only', resolve: { - extensions: ['.tsx', '.ts', '.js', '.css', '.scss'] + extensions: ['.tsx', '.ts', '.js', '.css', '.scss'], }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', - exclude: /node_modules/ + exclude: /node_modules/, }, { test: /\.css$/i, - use: ['style-loader', 'css-loader'] + use: ['style-loader', 'css-loader'], }, { test: /\.s[ac]ss$/i, @@ -35,13 +35,13 @@ module.exports = { loader: 'css-loader', options: { sourceMap: true, - url: false - } + url: false, + }, }, { // Needed to resolve image url()s within @primer/css loader: 'resolve-url-loader', - options: {} + options: {}, }, { loader: 'sass-loader', @@ -51,30 +51,28 @@ module.exports = { includePaths: ['./stylesheets', './node_modules'], options: { sourceMap: true, - sourceMapContents: false - } - } - } - } - ] - } - ] + sourceMapContents: false, + }, + }, + }, + }, + ], + }, + ], }, plugins: [ new MiniCssExtractPlugin({ - filename: 'index.css' + filename: 'index.css', }), new CopyWebpackPlugin({ - patterns: [ - { from: 'node_modules/@primer/css/fonts', to: 'fonts' } - ] + patterns: [{ from: 'node_modules/@primer/css/fonts', to: 'fonts' }], }), new EnvironmentPlugin({ NODE_ENV: 'development', // use 'development' unless process.env.NODE_ENV is defined - DEBUG: false + DEBUG: false, }), new ProvidePlugin({ - process: 'process/browser' - }) - ] + process: 'process/browser', + }), + ], }