Skip to content

Commit b22b462

Browse files
authored
repo sync
2 parents 8feea0b + f4134a9 commit b22b462

1,145 files changed

Lines changed: 18289 additions & 2654 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

javascripts/explorer.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1+
const explorerUrl = location.hostname === 'localhost'
2+
? 'http://localhost:3000'
3+
: 'https://graphql.github.com/explorer'
14

5+
// Pass non-search query params to Explorer app via the iFrame
26
export default function () {
3-
// TODO support "Run in Explorer" links in GraphQL guides
4-
// will need to handle query params separately from search queries
7+
const graphiqlExplorer = document.getElementById('graphiql')
8+
const queryString = window.location.search
9+
10+
if (!(queryString && graphiqlExplorer)) return
11+
12+
window.onload = () => {
13+
graphiqlExplorer.contentWindow.postMessage(queryString, explorerUrl)
14+
}
515
}

javascripts/search.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@ let $searchResultsContainer
77
let $searchOverlay
88
let $searchInput
99

10+
let isExplorerPage
11+
1012
// This is our default placeholder, but it can be localized with a <meta> tag
1113
let placeholder = 'Search topics, products...'
1214
let version
1315
let language
1416

1517
export default function search () {
18+
// We don't want to mess with query params intended for the GraphQL Explorer
19+
isExplorerPage = Boolean(document.getElementById('graphiql'))
20+
1621
// First, only initialize search if the elements are on the page
1722
$searchInputContainer = document.getElementById('search-input-container')
1823
$searchResultsContainer = document.getElementById('search-results-container')
@@ -51,7 +56,9 @@ export default function search () {
5156
searchWithYourKeyboard('#search-input-container input', '.ais-Hits-item')
5257

5358
// If the user already has a query in the URL, parse it and search away
54-
parseExistingSearch()
59+
if (!isExplorerPage) {
60+
parseExistingSearch()
61+
}
5562

5663
// If not on home page, decide if search panel should be open
5764
toggleSearchDisplay() // must come after parseExistingSearch
@@ -133,8 +140,9 @@ async function onSearch () {
133140
const query = $searchInput.value
134141

135142
// Update the URL with the search parameters in the query string
143+
// UNLESS this is the GraphQL Explorer page, where a query in the URL is a GraphQL query
136144
const pushUrl = new URL(location)
137-
pushUrl.search = query ? new URLSearchParams({ query }) : ''
145+
pushUrl.search = query && !isExplorerPage ? new URLSearchParams({ query }) : ''
138146
history.pushState({}, '', pushUrl)
139147

140148
// If there's a query, call the endpoint

lib/get-document-type.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = function getDocumentType (relativePath) {
2+
if (!relativePath.endsWith('index.md')) {
3+
return 'article'
4+
}
5+
6+
// Derive the document type from the path segment length
7+
switch (relativePath.split('/').length) {
8+
case 1:
9+
return 'homepage'
10+
case 2:
11+
return 'product'
12+
case 3:
13+
return 'category'
14+
case 4:
15+
return 'mapTopic'
16+
}
17+
}

script/content-migrations/remove-map-topics.js

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,31 @@
33
const fs = require('fs')
44
const path = require('path')
55
const walk = require('walk-sync')
6+
const languages = require('../../lib/languages')
7+
const frontmatter = require('../../lib/read-frontmatter')
8+
const addRedirectToFrontmatter = require('../../lib/redirects/add-redirect-to-frontmatter')
69

710
const relativeRefRegex = /\/[a-zA-Z0-9-]+/g
811
const linkString = /{% [^}]*?link.*? \/(.*?) ?%}/m
912
const linksArray = new RegExp(linkString.source, 'gm')
1013

11-
const fullDirectoryPath = path.join(process.cwd(), '/content')
12-
const files = walk(fullDirectoryPath, {
14+
const walkOpts = {
1315
includeBasePath: true,
1416
directories: false
15-
})
17+
}
18+
19+
// We only want category TOC files, not product TOCs.
20+
const categoryFileRegex = /content\/[^/]+?\/[^/]+?\/index.md/
1621

17-
files.forEach(file => {
18-
if (path.basename(file) !== 'index.md') return
22+
const fullDirectoryPaths = Object.values(languages).map(langObj => path.join(process.cwd(), langObj.dir, 'content'))
23+
const categoryIndexFiles = fullDirectoryPaths.map(fullDirectoryPath => walk(fullDirectoryPath, walkOpts)).flat()
24+
.filter(file => categoryFileRegex.test(file))
25+
26+
categoryIndexFiles.forEach(categoryIndexFile => {
27+
let categoryIndexContent = fs.readFileSync(categoryIndexFile, 'utf8')
1928

20-
let fileContent = fs.readFileSync(file, 'utf-8')
2129
// find array of TOC link strings
22-
const rawItems = fileContent.match(linksArray)
30+
const rawItems = categoryIndexContent.match(linksArray)
2331
if (!rawItems || !rawItems[0].includes('topic_link_in_list')) return
2432

2533
const pageToc = {}
@@ -37,24 +45,53 @@ files.forEach(file => {
3745
pageToc[currentTopic] = tmpArray
3846
}
3947
})
48+
4049
for (const topic in pageToc) {
41-
const oldTopicDirectory = path.dirname(file)
50+
const oldTopicDirectory = path.dirname(categoryIndexFile)
4251
const newTopicDirectory = path.join(oldTopicDirectory, topic)
43-
const topicFile = path.join(oldTopicDirectory, `${topic}.md`)
52+
const oldTopicFile = path.join(oldTopicDirectory, `${topic}.md`)
53+
54+
// Some translated category TOCs may be outdated and contain incorrect links
55+
if (!fs.existsSync(oldTopicFile)) continue
4456

4557
if (!fs.existsSync(newTopicDirectory)) fs.mkdirSync(newTopicDirectory)
4658

47-
let topicContent = fs.readFileSync(topicFile, 'utf-8')
48-
topicContent = topicContent.replace('mapTopic: true\n', '')
59+
const { data, content } = frontmatter(fs.readFileSync(oldTopicFile, 'utf8'))
60+
delete data.mapTopic
61+
62+
let topicContent = content
4963

5064
const articles = pageToc[topic]
5165

5266
articles.forEach(article => {
53-
fs.renameSync(`${oldTopicDirectory}/${article}.md`, `${newTopicDirectory}/${article}.md`)
67+
// Update the new map topic index file content
5468
topicContent = topicContent + `{% link_with_intro /${article} %}\n`
55-
fileContent = fileContent.replace(`/{% link_in_list /${article}`, `/{% link_in_list /${newTopicDirectory}/${article}`)
69+
70+
// Update the category index file content
71+
categoryIndexContent = categoryIndexContent.replace(`{% link_in_list /${article}`, `{% link_in_list /${topic}/${article}`)
72+
73+
// Early return if the article doesn't exist (some translated category TOCs may be outdated and contain incorrect links)
74+
if (!fs.existsSync(`${oldTopicDirectory}/${article}.md`)) return
75+
76+
// Move the file under the new map topic directory
77+
const newArticlePath = `${newTopicDirectory}/${article}.md`
78+
fs.renameSync(`${oldTopicDirectory}/${article}.md`, newArticlePath)
79+
80+
// Read the article file so we can add a redirect from its old path
81+
const articleContents = frontmatter(fs.readFileSync(newArticlePath, 'utf8'))
82+
addRedirectToFrontmatter(articleContents.data.redirect_from, `${oldTopicDirectory}/${article}`)
83+
84+
// Write the article with updated frontmatter
85+
fs.writeFileSync(newArticlePath, frontmatter.stringify(articleContents.content.trim(), articleContents.data, { lineWidth: 10000 }))
5686
})
57-
fs.writeFileSync(`${newTopicDirectory}/index.md`, topicContent)
58-
fs.unlinkSync(topicFile)
87+
88+
// Write the map topic index file
89+
fs.writeFileSync(`${newTopicDirectory}/index.md`, frontmatter.stringify(topicContent.trim(), data, { lineWidth: 10000 }))
90+
91+
// Write the category index file
92+
fs.writeFileSync(categoryIndexFile, categoryIndexContent)
93+
94+
// Delete the old map topic
95+
fs.unlinkSync(oldTopicFile)
5996
}
6097
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs')
4+
const path = require('path')
5+
const walk = require('walk-sync')
6+
const yaml = require('js-yaml')
7+
const frontmatter = require('../../lib/read-frontmatter')
8+
const getDocumentType = require('../../lib/get-document-type')
9+
const languages = require('../../lib/languages')
10+
11+
const linkString = /{% [^}]*?link.*? (\/.*?) ?%}/m
12+
const linksArray = new RegExp(linkString.source, 'gm')
13+
14+
// The product order is determined by data/products.yml
15+
const productsFile = path.join(process.cwd(), 'data/products.yml')
16+
const productsYml = yaml.load(fs.readFileSync(productsFile, 'utf8'))
17+
const sortedProductIds = productsYml.productsInOrder.concat('early-access')
18+
19+
// This script turns `{% link /<link> %} style content into children: [ -/<link> ] frontmatter arrays.
20+
//
21+
// It MUST be run after script/content-migrations/remove-map-topics.js.
22+
//
23+
// NOTE: The results won't work with the TOC handling currently in production, so the results must NOT
24+
// be committed until the updated handling is in place.
25+
26+
const walkOpts = {
27+
includeBasePath: true,
28+
directories: false
29+
}
30+
31+
const fullDirectoryPaths = Object.values(languages).map(langObj => path.join(process.cwd(), langObj.dir, 'content'))
32+
const indexFiles = fullDirectoryPaths.map(fullDirectoryPath => walk(fullDirectoryPath, walkOpts)).flat()
33+
.filter(file => file.endsWith('index.md'))
34+
35+
indexFiles
36+
.forEach(indexFile => {
37+
const relativePath = indexFile.replace(/^.+\/content\//, '')
38+
const documentType = getDocumentType(relativePath)
39+
40+
const { data, content } = frontmatter(fs.readFileSync(indexFile, 'utf8'))
41+
42+
if (documentType === 'homepage') {
43+
data.children = sortedProductIds
44+
}
45+
46+
const linkItems = content.match(linksArray) || []
47+
48+
// Turn the `{% link /<link> %}` list into an array of /<link> items
49+
if (documentType === 'product' || documentType === 'mapTopic') {
50+
data.children = getLinks(linkItems)
51+
}
52+
53+
if (documentType === 'category') {
54+
const childMapTopics = linkItems.filter(item => item.includes('topic_'))
55+
56+
data.children = childMapTopics.length ? getLinks(childMapTopics) : getLinks(linkItems)
57+
}
58+
59+
// Fix this one weird file
60+
if (relativePath === 'discussions/guides/index.md') {
61+
data.children = [
62+
'/best-practices-for-community-conversations-on-github',
63+
'/finding-discussions-across-multiple-repositories',
64+
'/granting-higher-permissions-to-top-contributors'
65+
]
66+
}
67+
68+
// Index files should no longer have body content, so we write an empty string
69+
fs.writeFileSync(indexFile, frontmatter.stringify('', data, { lineWidth: 10000 }))
70+
})
71+
72+
function getLinks (linkItemArray) {
73+
// do a oneoff replacement while mapping
74+
return linkItemArray.map(item => item.match(linkString)[1].replace('/discussions-guides', '/guides'))
75+
}

script/enterprise-server-releases/create-graphql-files.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,15 @@ const objectsFile = path.join(graphqlStaticDir, 'prerendered-objects.json')
5555
const previews = require(previewsFile)
5656
const changes = require(changesFile)
5757
const objects = require(objectsFile)
58+
// The prerendered objects file for the "old version" contains hardcoded links with the old version number.
59+
// We need to update those links to include the new version to prevent a test from failing.
60+
const regexOldVersion = new RegExp(oldVersion, 'gi')
61+
const stringifiedObject = JSON.stringify(objects[oldVersionId])
62+
.replace(regexOldVersion, newVersion)
5863

5964
previews[newVersionId] = previews[oldVersionId]
6065
changes[newVersionId] = changes[oldVersionId]
61-
objects[newVersionId] = objects[oldVersionId]
66+
objects[newVersionId] = JSON.parse(stringifiedObject)
6267

6368
// check that it worked
6469
if (!Object.keys(previews).includes(newVersionId)) {

script/enterprise-server-releases/create-rest-files.js

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ const fs = require('fs')
44
const path = require('path')
55
const program = require('commander')
66
const allVersions = require('../../lib/all-versions')
7+
const getOperations = require('../rest/utils/get-operations')
78
const dereferencedDir = 'lib/rest/static/dereferenced'
89
const decoratedDir = 'lib/rest/static/decorated'
910

1011
// [start-readme]
1112
//
12-
// This script creates new static openAPI files for a new version and modifies the info.version.
13+
// This script first copies the dereferenced schema from the previous GHES version for the new one.
14+
// It then replaces references to the previous version's docs URL (e.g., enterprise-server@3.0) with the new version (e.g., enterprise-server@3.1).
15+
// Finally, it generates a new decorated file from the new dereferenced file to ensure that the dereferenced and decorated files match.
1316
//
1417
// [end-readme]
1518

@@ -32,48 +35,43 @@ if (!(Object.keys(allVersions).includes(newVersion) && Object.keys(allVersions).
3235
process.exit(1)
3336
}
3437

35-
const oldDereferencedFilename = `${allVersions[oldVersion].openApiVersionName}.deref.json`
36-
const newDereferencedFilename = `${allVersions[newVersion].openApiVersionName}.deref.json`
37-
const oldDecoratedFilename = `${allVersions[oldVersion].openApiVersionName}.json`
38-
const newDecoratedFilename = `${allVersions[newVersion].openApiVersionName}.json`
38+
main()
3939

40-
const oldDereferencedFile = path.join(dereferencedDir, oldDereferencedFilename)
41-
const newDereferencedFile = path.join(dereferencedDir, newDereferencedFilename)
42-
const oldDecoratedFile = path.join(decoratedDir, oldDecoratedFilename)
43-
const newDecoratedFile = path.join(decoratedDir, newDecoratedFilename)
40+
async function main () {
41+
const oldDereferencedFilename = `${allVersions[oldVersion].openApiVersionName}.deref.json`
42+
const newDereferencedFilename = `${allVersions[newVersion].openApiVersionName}.deref.json`
43+
const newDecoratedFilename = `${allVersions[newVersion].openApiVersionName}.json`
4444

45-
// check that the old files exist
46-
if (!fs.existsSync(oldDereferencedFile)) {
47-
console.log(`Error! Can't find ${oldDereferencedFile} for ${oldVersion}.`)
48-
process.exit(1)
49-
}
45+
const oldDereferencedFile = path.join(dereferencedDir, oldDereferencedFilename)
46+
const newDereferencedFile = path.join(dereferencedDir, newDereferencedFilename)
47+
const newDecoratedFile = path.join(decoratedDir, newDecoratedFilename)
5048

51-
if (!fs.existsSync(oldDecoratedFile)) {
52-
console.log(`Error! Can't find ${oldDecoratedFile} for ${oldVersion}.`)
53-
process.exit(1)
54-
}
49+
// check that the old files exist
50+
if (!fs.existsSync(oldDereferencedFile)) {
51+
console.log(`Error! Can't find ${oldDereferencedFile} for ${oldVersion}.`)
52+
process.exit(1)
53+
}
5554

56-
// copy the files
57-
fs.copyFileSync(oldDereferencedFile, newDereferencedFile)
58-
fs.copyFileSync(oldDecoratedFile, newDecoratedFile)
55+
const oldDereferencedContent = fs.readFileSync(oldDereferencedFile, 'utf8')
5956

60-
// check that it worked
61-
if (!fs.existsSync(newDereferencedFile)) {
62-
console.log(`Error! Can't find ${newDereferencedFile} for ${oldVersion}.`)
63-
process.exit(1)
64-
}
57+
// Replace old version with new version
58+
// (ex: enterprise-server@3.0 -> enterprise-server@3.1)
59+
const regexOldVersion = new RegExp(oldVersion, 'gi')
60+
const newDereferenceContent = oldDereferencedContent.replace(regexOldVersion, newVersion)
6561

66-
if (!fs.existsSync(newDecoratedFile)) {
67-
console.log(`Error! Can't find ${newDecoratedFile} for ${oldVersion}.`)
68-
process.exit(1)
69-
}
62+
// Write processed dereferenced schema to disk
63+
fs.writeFileSync(newDereferencedFile, newDereferenceContent)
64+
console.log(`Created ${newDereferencedFile}.`)
65+
66+
const dereferencedSchema = require(path.join(process.cwd(), newDereferencedFile))
7067

71-
// set the info.version to development mode
72-
const derefFilepath = path.join(process.cwd(), newDereferencedFile)
73-
const derefSchema = require(derefFilepath)
74-
console.log(derefSchema.info.version)
75-
derefSchema.info.version = `Copy of ${oldVersion} !!DEVELOPMENT MODE - DO NOT MERGE!!`
76-
fs.writeFileSync(derefFilepath, JSON.stringify(derefSchema, null, 2))
68+
// Store all operations in an array of operation objects
69+
const operations = await getOperations(dereferencedSchema)
7770

78-
// print success message
79-
console.log(`Done! Created ${newDereferencedFile} and ${newDecoratedFile}.`)
71+
// Process each operation asynchronously
72+
await Promise.all(operations.map(operation => operation.process()))
73+
74+
// Write processed operations to disk
75+
fs.writeFileSync(newDecoratedFile, JSON.stringify(operations, null, 2))
76+
console.log(`Done! Created ${newDecoratedFile}.`)
77+
}

tests/browser/browser.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,30 @@ describe('language banner', () => {
281281
}
282282
})
283283
})
284+
285+
// The Explorer in the iFrame will not be accessible on localhost, but we can still
286+
// test the query param handling
287+
describe('GraphQL Explorer', () => {
288+
it('preserves query strings on the Explorer page without opening search', async () => {
289+
const queryString = `query {
290+
viewer {
291+
foo
292+
}
293+
}`
294+
// Encoded as: query%20%7B%0A%20%20viewer%20%7B%0A%20%20%20%20foo%0A%20%20%7D%0A%7D
295+
const encodedString = encodeURIComponent(queryString)
296+
const explorerUrl = 'http://localhost:4001/en/graphql/overview/explorer'
297+
298+
await page.goto(`${explorerUrl}?query=${encodedString}`)
299+
300+
// On non-Explorer pages, query params handled by search JS get form-encoded using `+` instead of `%20`.
301+
// So on these pages, the following test will be false; but on the Explorer page, it should be true.
302+
expect(page.url().endsWith(encodedString)).toBe(true)
303+
304+
// On non-Explorer pages, query params handled by search JS will populate in the search box and the `js-open`
305+
// class is added. On these pages, the following test will NOT be null; but on the Explorer page, it should be null.
306+
await page.waitForSelector('#search-results-container')
307+
const searchResult = await page.$('#search-results-container.js-open')
308+
expect(searchResult).toBeNull()
309+
})
310+
})

0 commit comments

Comments
 (0)