Migrate 6 files from JavaScript to TypeScript (#58002)
This commit is contained in:
@@ -8,7 +8,7 @@ import { get } from '@/tests/helpers/e2etest'
|
||||
|
||||
// Type definitions for page objects
|
||||
type Page = {
|
||||
autogenerated?: boolean
|
||||
autogenerated?: string
|
||||
fullPath: string
|
||||
permalinks: Array<{ href: string }>
|
||||
versions: {
|
||||
|
||||
@@ -49,8 +49,8 @@ describe('front matter', () => {
|
||||
async (page) => {
|
||||
const redirectsContext = { redirects, pages }
|
||||
|
||||
const trouble = page.includeGuides
|
||||
// Using any type for uri because includeGuides can contain various URI formats
|
||||
const trouble = page
|
||||
.includeGuides! // Using any type for uri because includeGuides can contain various URI formats
|
||||
.map((uri: any, i: number) => checkURL(uri, i, redirectsContext))
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -58,14 +58,14 @@ describe('front matter', () => {
|
||||
expect(trouble.length, customErrorMessage).toEqual(0)
|
||||
|
||||
const counts = new Map()
|
||||
for (const guide of page.includeGuides) {
|
||||
for (const guide of page.includeGuides!) {
|
||||
counts.set(guide, (counts.get(guide) || 0) + 1)
|
||||
}
|
||||
const countUnique = counts.size
|
||||
let notDistinctMessage = `In ${page.relativePath} there are duplicate links in .includeGuides`
|
||||
const dupes = [...counts.entries()].filter(([, count]) => count > 1).map(([entry]) => entry)
|
||||
notDistinctMessage += `\nTo fix this, remove: ${dupes.join(' and ')}`
|
||||
expect(page.includeGuides.length, notDistinctMessage).toEqual(countUnique)
|
||||
expect(page.includeGuides!.length, notDistinctMessage).toEqual(countUnique)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -78,12 +78,17 @@ describe('front matter', () => {
|
||||
const redirectsContext = { redirects, pages }
|
||||
|
||||
const trouble = []
|
||||
for (const links of Object.values(page.featuredLinks)) {
|
||||
for (const links of Object.values(page.featuredLinks!)) {
|
||||
// Some thing in `.featuredLinks` are not arrays.
|
||||
// For example `popularHeading`. So just skip them.
|
||||
if (!Array.isArray(links)) continue
|
||||
|
||||
trouble.push(...links.map((uri, i) => checkURL(uri, i, redirectsContext)).filter(Boolean))
|
||||
trouble.push(
|
||||
...links
|
||||
.filter((link) => link.href)
|
||||
.map((link, i) => checkURL(link.href, i, redirectsContext))
|
||||
.filter(Boolean),
|
||||
)
|
||||
}
|
||||
|
||||
const customErrorMessage = makeCustomErrorMessage(page, trouble, 'featuredLinks')
|
||||
@@ -98,7 +103,7 @@ describe('front matter', () => {
|
||||
const redirectsContext = { redirects, pages }
|
||||
|
||||
const trouble = []
|
||||
for (const linksRaw of Object.values(page.introLinks)) {
|
||||
for (const linksRaw of Object.values(page.introLinks!)) {
|
||||
const links = Array.isArray(linksRaw) ? linksRaw : [linksRaw]
|
||||
trouble.push(
|
||||
...links
|
||||
|
||||
@@ -8,6 +8,14 @@ import { merge, get } from 'lodash-es'
|
||||
import languages from '@/languages/lib/languages'
|
||||
import { correctTranslatedContentStrings } from '@/languages/lib/correct-translation-content'
|
||||
|
||||
interface YAMLException extends Error {
|
||||
mark?: any
|
||||
}
|
||||
|
||||
interface FileSystemError extends Error {
|
||||
code?: string
|
||||
}
|
||||
|
||||
// If you run `export DEBUG_JIT_DATA_READS=true` in your terminal,
|
||||
// next time it will mention every file it reads from disk.
|
||||
const DEBUG_JIT_DATA_READS = Boolean(JSON.parse(process.env.DEBUG_JIT_DATA_READS || 'false'))
|
||||
@@ -23,29 +31,33 @@ const ALWAYS_ENGLISH_MD_FILES = new Set([
|
||||
])
|
||||
|
||||
// Returns all the things inside a directory
|
||||
export const getDeepDataByLanguage = memoize((dottedPath, langCode, dir = null) => {
|
||||
if (!(langCode in languages))
|
||||
throw new Error(`langCode '${langCode}' not a recognized language code`)
|
||||
export const getDeepDataByLanguage = memoize(
|
||||
(dottedPath: string, langCode: string, dir: string | null = null): any => {
|
||||
if (!(langCode in languages)) {
|
||||
throw new Error(`langCode '${langCode}' not a recognized language code`)
|
||||
}
|
||||
|
||||
// The `dir` argument is only used for testing purposes.
|
||||
// For example, our unit tests that depend on using a fixtures
|
||||
// root.
|
||||
// If we don't allow those tests to override the `dir` argument,
|
||||
// it'll be stuck from the first time `languages.js` was imported.
|
||||
if (dir === null) {
|
||||
dir = languages[langCode].dir
|
||||
}
|
||||
return getDeepDataByDir(dottedPath, dir)
|
||||
})
|
||||
// The `dir` argument is only used for testing purposes.
|
||||
// For example, our unit tests that depend on using a fixtures
|
||||
// root.
|
||||
// If we don't allow those tests to override the `dir` argument,
|
||||
// it'll be stuck from the first time `languages.js` was imported.
|
||||
let actualDir = dir
|
||||
if (actualDir === null) {
|
||||
actualDir = languages[langCode].dir
|
||||
}
|
||||
return getDeepDataByDir(dottedPath, actualDir)
|
||||
},
|
||||
)
|
||||
|
||||
// Doesn't need to be memoized because it's used by getDataKeysByLanguage
|
||||
// which is already memoized.
|
||||
function getDeepDataByDir(dottedPath, dir) {
|
||||
function getDeepDataByDir(dottedPath: string, dir: string): any {
|
||||
const fullPath = ['data']
|
||||
const split = dottedPath.split(/\./g)
|
||||
fullPath.push(...split)
|
||||
|
||||
const things = {}
|
||||
const things: any = {}
|
||||
const relPath = fullPath.join(path.sep)
|
||||
for (const dirent of getDirents(dir, relPath)) {
|
||||
if (dirent.name === 'README.md') continue
|
||||
@@ -63,12 +75,12 @@ function getDeepDataByDir(dottedPath, dir) {
|
||||
return things
|
||||
}
|
||||
|
||||
function getDirents(root, relPath) {
|
||||
function getDirents(root: string, relPath: string): fs.Dirent[] {
|
||||
const filePath = root ? path.join(root, relPath) : relPath
|
||||
return fs.readdirSync(filePath, { withFileTypes: true })
|
||||
}
|
||||
|
||||
export const getUIDataMerged = memoize((langCode) => {
|
||||
export const getUIDataMerged = memoize((langCode: string): any => {
|
||||
const uiEnglish = getUIData('en')
|
||||
if (langCode === 'en') return uiEnglish
|
||||
// Got to combine. Start with the English and put the translation on top.
|
||||
@@ -77,7 +89,7 @@ export const getUIDataMerged = memoize((langCode) => {
|
||||
// swedish = {food: "Mat"}
|
||||
// =>
|
||||
// combind = {food: "Mat", drink: "Drink"}
|
||||
const combined = {}
|
||||
const combined: any = {}
|
||||
merge(combined, uiEnglish)
|
||||
merge(combined, getUIData(langCode))
|
||||
return combined
|
||||
@@ -85,13 +97,13 @@ export const getUIDataMerged = memoize((langCode) => {
|
||||
|
||||
// Doesn't need to be memoized because it's used by another function
|
||||
// that is memoized.
|
||||
const getUIData = (langCode) => {
|
||||
const getUIData = (langCode: string): any => {
|
||||
const fullPath = ['data', 'ui.yml']
|
||||
const { dir } = languages[langCode]
|
||||
return getYamlContent(dir, fullPath.join(path.sep))
|
||||
}
|
||||
|
||||
export const getDataByLanguage = memoize((dottedPath, langCode) => {
|
||||
export const getDataByLanguage = memoize((dottedPath: string, langCode: string): any => {
|
||||
if (!(langCode in languages))
|
||||
throw new Error(`langCode '${langCode}' not a recognized language code`)
|
||||
const { dir } = languages[langCode]
|
||||
@@ -111,7 +123,7 @@ export const getDataByLanguage = memoize((dottedPath, langCode) => {
|
||||
}
|
||||
return value
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.mark && error.message) {
|
||||
if (error instanceof Error && (error as YAMLException).mark && error.message) {
|
||||
// It's a yaml.load() generated error!
|
||||
// Remember, the file that we read might have been a .yml or a .md
|
||||
// file. If it was a .md file, with corrupt front-matter that too
|
||||
@@ -128,12 +140,17 @@ export const getDataByLanguage = memoize((dottedPath, langCode) => {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (error.code === 'ENOENT') return undefined
|
||||
if ((error as FileSystemError).code === 'ENOENT') return undefined
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
function getDataByDir(dottedPath, dir, englishRoot, langCode) {
|
||||
function getDataByDir(
|
||||
dottedPath: string,
|
||||
dir: string,
|
||||
englishRoot?: string,
|
||||
langCode?: string,
|
||||
): any {
|
||||
const fullPath = ['data']
|
||||
|
||||
// Using English here because it doesn't matter. We just want to
|
||||
@@ -159,17 +176,17 @@ function getDataByDir(dottedPath, dir, englishRoot, langCode) {
|
||||
// data/early-access/reusables/foo/bar.md
|
||||
//
|
||||
if (split[0] === 'early-access') {
|
||||
fullPath.push(split.shift())
|
||||
fullPath.push(split.shift()!)
|
||||
}
|
||||
const first = split[0]
|
||||
|
||||
if (first === 'variables') {
|
||||
const key = split.pop()
|
||||
const basename = split.pop()
|
||||
const key = split.pop()!
|
||||
const basename = split.pop()!
|
||||
fullPath.push(...split)
|
||||
fullPath.push(`${basename}.yml`)
|
||||
const allData = getYamlContent(dir, fullPath.join(path.sep), englishRoot)
|
||||
if (allData) {
|
||||
if (allData && key) {
|
||||
const value = allData[key]
|
||||
if (value) {
|
||||
return matter(value).content
|
||||
@@ -177,11 +194,11 @@ function getDataByDir(dottedPath, dir, englishRoot, langCode) {
|
||||
} else {
|
||||
console.warn(`Unable to find variables Yaml file ${fullPath.join(path.sep)}`)
|
||||
}
|
||||
return
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (first === 'reusables') {
|
||||
const nakedname = split.pop()
|
||||
const nakedname = split.pop()!
|
||||
fullPath.push(...split)
|
||||
fullPath.push(`${nakedname}.md`)
|
||||
const markdown = getMarkdownContent(dir, fullPath.join(path.sep), englishRoot)
|
||||
@@ -205,7 +222,7 @@ function getDataByDir(dottedPath, dir, englishRoot, langCode) {
|
||||
// genuinely give it the English equivalent content, which it
|
||||
// sometimes uses to correct some Liquid tags. At least other
|
||||
// good corrections might happen.
|
||||
if (error.code !== 'ENOENT') {
|
||||
if ((error as FileSystemError).code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -226,25 +243,25 @@ function getDataByDir(dottedPath, dir, englishRoot, langCode) {
|
||||
}
|
||||
|
||||
if (first === 'product-examples' || first === 'glossaries' || first === 'release-notes') {
|
||||
const basename = split.pop()
|
||||
const basename = split.pop()!
|
||||
fullPath.push(...split)
|
||||
fullPath.push(`${basename}.yml`)
|
||||
return getYamlContent(dir, fullPath.join(path.sep), englishRoot)
|
||||
}
|
||||
|
||||
if (first === 'learning-tracks') {
|
||||
const key = split.pop()
|
||||
const basename = split.pop()
|
||||
const key = split.pop()!
|
||||
const basename = split.pop()!
|
||||
fullPath.push(...split)
|
||||
fullPath.push(`${basename}.yml`)
|
||||
const allData = getYamlContent(dir, fullPath.join(path.sep), englishRoot)
|
||||
return allData[key]
|
||||
return key ? allData[key] : undefined
|
||||
}
|
||||
|
||||
throw new Error(`Can't find the key '${dottedPath}' in the scope.`)
|
||||
}
|
||||
|
||||
function getSmartSplit(dottedPath) {
|
||||
function getSmartSplit(dottedPath: string): string[] {
|
||||
const split = dottedPath.split('.')
|
||||
const bits = []
|
||||
for (let i = 0, len = split.length; i < len; i++) {
|
||||
@@ -284,36 +301,44 @@ function getSmartSplit(dottedPath) {
|
||||
// 2.1. read and parse data/variables/product.yml
|
||||
// -> cache HIT (Yay!)
|
||||
//
|
||||
const getYamlContent = memoize((root, relPath, englishRoot) => {
|
||||
// Certain Yaml files we know we always want the English one
|
||||
// no matter what the specified language is.
|
||||
// For example, we never want `data/variables/product.yml` translated
|
||||
// so we know to immediately fall back to the English one.
|
||||
if (ALWAYS_ENGLISH_YAML_FILES.has(relPath)) {
|
||||
// This forces it to read from English. Later, when it goes
|
||||
// into `getFileContent(...)` it will note that `root !== englishRoot`
|
||||
// so it won't try to fall back.
|
||||
root = englishRoot
|
||||
}
|
||||
const fileContent = getFileContent(root, relPath, englishRoot)
|
||||
return yaml.load(fileContent, { filename: relPath })
|
||||
})
|
||||
const getYamlContent = memoize(
|
||||
(root: string | undefined, relPath: string, englishRoot?: string): any => {
|
||||
// Certain Yaml files we know we always want the English one
|
||||
// no matter what the specified language is.
|
||||
// For example, we never want `data/variables/product.yml` translated
|
||||
// so we know to immediately fall back to the English one.
|
||||
if (ALWAYS_ENGLISH_YAML_FILES.has(relPath)) {
|
||||
// This forces it to read from English. Later, when it goes
|
||||
// into `getFileContent(...)` it will note that `root !== englishRoot`
|
||||
// so it won't try to fall back.
|
||||
root = englishRoot
|
||||
}
|
||||
const fileContent = getFileContent(root, relPath, englishRoot)
|
||||
return yaml.load(fileContent, { filename: relPath })
|
||||
},
|
||||
)
|
||||
|
||||
// The reason why this is memoized, is the same as for getYamlContent() above.
|
||||
const getMarkdownContent = memoize((root, relPath, englishRoot) => {
|
||||
// Certain reusables we never want to be pulled from the translations.
|
||||
// For example, certain reusables don't contain any English prose. Just
|
||||
// facts like numbers or hardcoded key words.
|
||||
// If this is the case, forcibly always draw from the English files.
|
||||
if (ALWAYS_ENGLISH_MD_FILES.has(relPath)) {
|
||||
root = englishRoot
|
||||
}
|
||||
const getMarkdownContent = memoize(
|
||||
(root: string | undefined, relPath: string, englishRoot?: string): string => {
|
||||
// Certain reusables we never want to be pulled from the translations.
|
||||
// For example, certain reusables don't contain any English prose. Just
|
||||
// facts like numbers or hardcoded key words.
|
||||
// If this is the case, forcibly always draw from the English files.
|
||||
if (ALWAYS_ENGLISH_MD_FILES.has(relPath)) {
|
||||
root = englishRoot
|
||||
}
|
||||
|
||||
const fileContent = getFileContent(root, relPath, englishRoot)
|
||||
return matter(fileContent).content.trimEnd()
|
||||
})
|
||||
const fileContent = getFileContent(root, relPath, englishRoot)
|
||||
return matter(fileContent).content.trimEnd()
|
||||
},
|
||||
)
|
||||
|
||||
const getFileContent = (root, relPath, englishRoot) => {
|
||||
const getFileContent = (
|
||||
root: string | undefined,
|
||||
relPath: string,
|
||||
englishRoot?: string,
|
||||
): string => {
|
||||
const filePath = root ? path.join(root, relPath) : relPath
|
||||
if (DEBUG_JIT_DATA_READS) console.log('READ', filePath)
|
||||
try {
|
||||
@@ -321,10 +346,10 @@ const getFileContent = (root, relPath, englishRoot) => {
|
||||
} catch (err) {
|
||||
// It might fail because that particular data entry doesn't yet
|
||||
// exist in a translation
|
||||
if (err.code === 'ENOENT') {
|
||||
if ((err as FileSystemError).code === 'ENOENT') {
|
||||
// If looking it up as a file fails, give it one more chance if the
|
||||
// read was for a translation.
|
||||
if (root !== englishRoot) {
|
||||
if (englishRoot && root !== englishRoot) {
|
||||
// We can try again but this time using the English files
|
||||
return getFileContent(englishRoot, relPath, englishRoot)
|
||||
}
|
||||
@@ -333,9 +358,9 @@ const getFileContent = (root, relPath, englishRoot) => {
|
||||
}
|
||||
}
|
||||
|
||||
function memoize(func) {
|
||||
const cache = new Map()
|
||||
return (...args) => {
|
||||
function memoize<T extends (...args: any[]) => any>(func: T): T {
|
||||
const cache = new Map<string, any>()
|
||||
return ((...args: any[]) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// It is very possible that certain files, when caching is disabled,
|
||||
// are read multiple times in short succession. E.g. `product.yml`.
|
||||
@@ -374,5 +399,5 @@ function memoize(func) {
|
||||
if (Array.isArray(value)) return [...value]
|
||||
if (typeof value === 'object') return { ...value }
|
||||
return value
|
||||
}
|
||||
}) as T
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { DataDirectory } from '@/tests/helpers/data-directory'
|
||||
|
||||
describe('get-data', () => {
|
||||
let dd
|
||||
let dd: DataDirectory
|
||||
const enDirBefore = languages.en.dir
|
||||
// Only `en` is available in tests, so pretend we also have Japanese
|
||||
languages.ja = Object.assign({}, languages.en, {})
|
||||
@@ -219,7 +219,7 @@ front: >'matter
|
||||
`.trim()
|
||||
|
||||
describe('get-data on corrupt translations', () => {
|
||||
let dd
|
||||
let dd: DataDirectory
|
||||
const enDirBefore = languages.en.dir
|
||||
// Only `en` is available in vitest tests, so pretend we also have Japanese
|
||||
languages.ja = Object.assign({}, languages.en, {})
|
||||
@@ -6,6 +6,32 @@ import { allVersions } from '@/versions/lib/all-versions'
|
||||
import { allTools } from '@/tools/lib/all-tools'
|
||||
import { getDeepDataByLanguage } from '@/data-directory/lib/get-data'
|
||||
|
||||
interface SchemaProperty {
|
||||
type?: string | string[]
|
||||
translatable?: boolean
|
||||
deprecated?: boolean
|
||||
default?: any
|
||||
minimum?: number
|
||||
maximum?: number
|
||||
enum?: any[]
|
||||
errorMessage?: string
|
||||
items?: any
|
||||
properties?: Record<string, any>
|
||||
required?: string[]
|
||||
additionalProperties?: boolean
|
||||
format?: string
|
||||
description?: string
|
||||
minItems?: number
|
||||
maxItems?: number
|
||||
}
|
||||
|
||||
interface Schema {
|
||||
type: string
|
||||
required: string[]
|
||||
additionalProperties: boolean
|
||||
properties: Record<string, SchemaProperty>
|
||||
}
|
||||
|
||||
const layoutNames = [
|
||||
'default',
|
||||
'graphql-explorer',
|
||||
@@ -38,7 +64,7 @@ export const contentTypesEnum = [
|
||||
'other', // Everything else.
|
||||
]
|
||||
|
||||
export const schema = {
|
||||
export const schema: Schema = {
|
||||
type: 'object',
|
||||
required: ['title', 'versions'],
|
||||
additionalProperties: false,
|
||||
@@ -383,8 +409,8 @@ export const schema = {
|
||||
}
|
||||
|
||||
// returns a list of deprecated properties
|
||||
export const deprecatedProperties = Object.keys(schema.properties).filter((prop) => {
|
||||
return schema.properties[prop].deprecated
|
||||
export const deprecatedProperties = Object.keys(schema.properties).filter((prop: string) => {
|
||||
return (schema.properties as Record<string, SchemaProperty>)[prop].deprecated
|
||||
})
|
||||
|
||||
const featureVersionsProp = {
|
||||
@@ -407,17 +433,17 @@ const semverRange = {
|
||||
errorMessage: 'Must be a valid SemVer range: ${0}',
|
||||
}
|
||||
|
||||
schema.properties.versions = {
|
||||
;(schema.properties as Record<string, any>).versions = {
|
||||
type: ['object', 'string'], // allow a '*' string to indicate all versions
|
||||
additionalProperties: false, // don't allow any versions in FM that aren't defined in lib/all-versions
|
||||
properties: Object.values(allVersions).reduce((acc, versionObj) => {
|
||||
properties: Object.values(allVersions).reduce((acc: any, versionObj) => {
|
||||
acc[versionObj.plan] = semverRange
|
||||
acc[versionObj.shortName] = semverRange
|
||||
return acc
|
||||
}, featureVersionsProp),
|
||||
}
|
||||
|
||||
export function frontmatter(markdown, opts = {}) {
|
||||
export function frontmatter(markdown: string, opts: any = {}) {
|
||||
const defaults = {
|
||||
schema,
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import path from 'path'
|
||||
|
||||
import type { Language } from '@/languages/lib/languages'
|
||||
import type { UnversionedTree, UnversionLanguageTree, SiteTree, Tree } from '@/types'
|
||||
|
||||
import languages from '@/languages/lib/languages'
|
||||
import { allVersions } from '@/versions/lib/all-versions'
|
||||
import createTree from './create-tree'
|
||||
@@ -10,6 +13,10 @@ import Permalink from './permalink'
|
||||
import frontmatterSchema from './frontmatter'
|
||||
import { correctTranslatedContentStrings } from '@/languages/lib/correct-translation-content'
|
||||
|
||||
interface FileSystemError extends Error {
|
||||
code?: string
|
||||
}
|
||||
|
||||
// If you run `export DEBUG_TRANSLATION_FALLBACKS=true` in your terminal,
|
||||
// every time a translation file fails to initialize we fall back to English
|
||||
// and write a warning to stdout.
|
||||
@@ -29,7 +36,7 @@ class FrontmatterParsingError extends Error {}
|
||||
// Note! As of Nov 2022, the schema says that 'product' is translatable
|
||||
// which is surprising since only a single page has prose in it.
|
||||
const translatableFrontmatterKeys = Object.entries(frontmatterSchema.schema.properties)
|
||||
.filter(([, value]) => value.translatable)
|
||||
.filter(([, value]: [string, any]) => value.translatable)
|
||||
.map(([key]) => key)
|
||||
|
||||
/**
|
||||
@@ -37,13 +44,18 @@ const translatableFrontmatterKeys = Object.entries(frontmatterSchema.schema.prop
|
||||
* 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(languagesOnly = []) {
|
||||
export async function loadUnversionedTree(
|
||||
languagesOnly: string[] = [],
|
||||
): Promise<UnversionLanguageTree> {
|
||||
if (languagesOnly && !Array.isArray(languagesOnly)) {
|
||||
throw new Error("'languagesOnly' has to be an array")
|
||||
}
|
||||
const unversionedTree = {}
|
||||
unversionedTree.en = await createTree(path.join(languages.en.dir, 'content'))
|
||||
setCategoryApplicableVersions(unversionedTree.en)
|
||||
const unversionedTree: UnversionLanguageTree = {} as UnversionLanguageTree
|
||||
const enTree = await createTree(path.join(languages.en.dir, 'content'))
|
||||
if (enTree) {
|
||||
unversionedTree.en = enTree
|
||||
setCategoryApplicableVersions(unversionedTree.en)
|
||||
}
|
||||
|
||||
const languagesValues = Object.entries(languages)
|
||||
.filter(([language]) => {
|
||||
@@ -70,14 +82,14 @@ export async function loadUnversionedTree(languagesOnly = []) {
|
||||
return unversionedTree
|
||||
}
|
||||
|
||||
function setCategoryApplicableVersions(tree) {
|
||||
function setCategoryApplicableVersions(tree: UnversionedTree): void {
|
||||
// Now that the tree has been fully computed, we can for any node that
|
||||
// is a category page, re-set its `.applicableVersions` and `.permalinks`
|
||||
// based on the union set of all its immediate children's
|
||||
// `.applicableVersions`.
|
||||
for (const childPage of tree.childPages) {
|
||||
if (childPage.page.relativePath.endsWith('index.md')) {
|
||||
const combinedApplicableVersions = []
|
||||
const combinedApplicableVersions: string[] = []
|
||||
let moreThanOneChild = false
|
||||
for (const childChildPage of childPage.childPages || []) {
|
||||
for (const version of childChildPage.page.applicableVersions) {
|
||||
@@ -112,12 +124,16 @@ function setCategoryApplicableVersions(tree) {
|
||||
}
|
||||
}
|
||||
|
||||
function equalSets(setA, setB) {
|
||||
function equalSets(setA: Set<string>, setB: Set<string>): boolean {
|
||||
return setA.size === setB.size && [...setA].every((x) => setB.has(x))
|
||||
}
|
||||
|
||||
async function translateTree(dir, langObj, enTree) {
|
||||
const item = {}
|
||||
async function translateTree(
|
||||
dir: string,
|
||||
langObj: Language,
|
||||
enTree: UnversionedTree,
|
||||
): Promise<UnversionedTree> {
|
||||
const item: Partial<UnversionedTree> = {}
|
||||
const enPage = enTree.page
|
||||
const { ...enData } = enPage
|
||||
|
||||
@@ -170,13 +186,14 @@ async function translateTree(dir, langObj, enTree) {
|
||||
if (THROW_TRANSLATION_ERRORS) {
|
||||
throw new Error(message)
|
||||
}
|
||||
data[property] = enData[property]
|
||||
// Using any because the property is dynamic
|
||||
;(data as any)[property] = (enData as any)[property]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If it didn't work because it didn't exist, don't fret,
|
||||
// we'll use the English equivalent's data and content.
|
||||
if (error.code === 'ENOENT' || error instanceof FrontmatterParsingError) {
|
||||
if ((error as FileSystemError).code === 'ENOENT' || error instanceof FrontmatterParsingError) {
|
||||
data = enData
|
||||
content = enPage.markdown
|
||||
const message = `Unable to initialize ${fullPath} because translation content file does not exist.`
|
||||
@@ -204,14 +221,14 @@ async function translateTree(dir, langObj, enTree) {
|
||||
code: langObj.code,
|
||||
})
|
||||
|
||||
translatedData.title = correctTranslatedContentStrings(translatedData.title, enPage.title, {
|
||||
translatedData.title = correctTranslatedContentStrings(translatedData.title || '', enPage.title, {
|
||||
relativePath,
|
||||
code: langObj.code,
|
||||
})
|
||||
if (translatedData.shortTitle) {
|
||||
translatedData.shortTitle = correctTranslatedContentStrings(
|
||||
translatedData.shortTitle,
|
||||
enPage.shortTitle,
|
||||
enPage.shortTitle || '',
|
||||
{
|
||||
relativePath,
|
||||
code: langObj.code,
|
||||
@@ -225,7 +242,8 @@ async function translateTree(dir, langObj, enTree) {
|
||||
})
|
||||
}
|
||||
|
||||
item.page = new Page(
|
||||
// Using any to handle the complex object merging for Page constructor
|
||||
;(item as UnversionedTree).page = new Page(
|
||||
Object.assign(
|
||||
{},
|
||||
// By default, shallow-copy everything from the English equivalent.
|
||||
@@ -239,20 +257,23 @@ async function translateTree(dir, langObj, enTree) {
|
||||
},
|
||||
// And the translations translated properties.
|
||||
translatedData,
|
||||
),
|
||||
)
|
||||
if (item.page.children) {
|
||||
item.childPages = await Promise.all(
|
||||
) as any,
|
||||
) as any
|
||||
if (
|
||||
((item as UnversionedTree).page as any).children &&
|
||||
((item as UnversionedTree).page as any).children.length > 0
|
||||
) {
|
||||
;(item as UnversionedTree).childPages = await Promise.all(
|
||||
enTree.childPages
|
||||
.filter((childTree) => {
|
||||
.filter((childTree: UnversionedTree) => {
|
||||
// Translations should not get early access pages at all.
|
||||
return childTree.page.relativePath.split(path.sep)[0] !== 'early-access'
|
||||
})
|
||||
.map((childTree) => translateTree(dir, langObj, childTree)),
|
||||
.map((childTree: UnversionedTree) => translateTree(dir, langObj, childTree)),
|
||||
)
|
||||
}
|
||||
|
||||
return item
|
||||
return item as UnversionedTree
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,9 +288,12 @@ async function translateTree(dir, langObj, enTree) {
|
||||
*
|
||||
* Order of languages and versions doesn't matter, but order of child page arrays DOES matter (for navigation).
|
||||
*/
|
||||
export async function loadSiteTree(unversionedTree, languagesOnly = []) {
|
||||
export async function loadSiteTree(
|
||||
unversionedTree?: UnversionLanguageTree,
|
||||
languagesOnly: string[] = [],
|
||||
): Promise<SiteTree> {
|
||||
const rawTree = Object.assign({}, unversionedTree || (await loadUnversionedTree(languagesOnly)))
|
||||
const siteTree = {}
|
||||
const siteTree: SiteTree = {}
|
||||
|
||||
const langCodes = (languagesOnly.length && languagesOnly) || Object.keys(languages)
|
||||
// For every language...
|
||||
@@ -278,7 +302,7 @@ export async function loadSiteTree(unversionedTree, languagesOnly = []) {
|
||||
if (!(langCode in rawTree)) {
|
||||
throw new Error(`No tree for language ${langCode}`)
|
||||
}
|
||||
const treePerVersion = {}
|
||||
const treePerVersion: { [version: string]: Tree } = {}
|
||||
// in every version...
|
||||
await Promise.all(
|
||||
versions.map(async (version) => {
|
||||
@@ -298,10 +322,10 @@ export async function loadSiteTree(unversionedTree, languagesOnly = []) {
|
||||
return siteTree
|
||||
}
|
||||
|
||||
export async function versionPages(obj, version, langCode) {
|
||||
export async function versionPages(obj: any, version: string, langCode: string): Promise<Tree> {
|
||||
// Add a versioned href as a convenience for use in layouts.
|
||||
const permalink = obj.page.permalinks.find(
|
||||
(pl) =>
|
||||
(pl: any) =>
|
||||
pl.pageVersion === version ||
|
||||
(pl.pageVersion === 'homepage' && version === nonEnterpriseDefaultVersion),
|
||||
)
|
||||
@@ -316,9 +340,9 @@ export async function versionPages(obj, version, langCode) {
|
||||
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))
|
||||
.filter((childPage: any) => childPage.page.applicableVersions.includes(version))
|
||||
// Version the child pages recursively.
|
||||
.map((childPage) => versionPages(Object.assign({}, childPage), version, langCode)),
|
||||
.map((childPage: any) => versionPages(Object.assign({}, childPage), version, langCode)),
|
||||
)
|
||||
|
||||
obj.childPages = [...versionedChildPages]
|
||||
@@ -327,12 +351,15 @@ export async function versionPages(obj, version, langCode) {
|
||||
}
|
||||
|
||||
// Derive a flat array of Page objects in all languages.
|
||||
export async function loadPageList(unversionedTree, languagesOnly = []) {
|
||||
export async function loadPageList(
|
||||
unversionedTree?: UnversionLanguageTree,
|
||||
languagesOnly: string[] = [],
|
||||
): Promise<Page[]> {
|
||||
if (languagesOnly && !Array.isArray(languagesOnly)) {
|
||||
throw new Error("'languagesOnly' has to be an array")
|
||||
}
|
||||
const rawTree = unversionedTree || (await loadUnversionedTree(languagesOnly))
|
||||
const pageList = []
|
||||
const pageList: Page[] = []
|
||||
|
||||
const langCodes = (languagesOnly.length && languagesOnly) || Object.keys(languages)
|
||||
await Promise.all(
|
||||
@@ -344,13 +371,15 @@ export async function loadPageList(unversionedTree, languagesOnly = []) {
|
||||
}),
|
||||
)
|
||||
|
||||
async function addToCollection(item, collection) {
|
||||
async function addToCollection(item: UnversionedTree, collection: Page[]): Promise<void> {
|
||||
if (!item.page) return
|
||||
collection.push(item.page)
|
||||
collection.push(item.page as any)
|
||||
|
||||
if (!item.childPages) return
|
||||
await Promise.all(
|
||||
item.childPages.map(async (childPage) => await addToCollection(childPage, collection)),
|
||||
item.childPages.map(
|
||||
async (childPage: UnversionedTree) => await addToCollection(childPage, collection),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -360,19 +389,25 @@ export async function loadPageList(unversionedTree, languagesOnly = []) {
|
||||
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: Page[]): Record<string, Page> {
|
||||
const pageMap = pageList.reduce(
|
||||
(pageMap: Record<string, Page>, page: Page) => {
|
||||
for (const permalink of page.permalinks) {
|
||||
pageMap[permalink.href] = page
|
||||
}
|
||||
return pageMap
|
||||
},
|
||||
{} as Record<string, Page>,
|
||||
)
|
||||
|
||||
return pageMap
|
||||
}
|
||||
|
||||
export async function loadPageMap(pageList, languagesOnly = []) {
|
||||
const pages = pageList || (await loadPageList(languagesOnly))
|
||||
export async function loadPageMap(
|
||||
pageList?: Page[],
|
||||
languagesOnly: string[] = [],
|
||||
): Promise<Record<string, Page>> {
|
||||
const pages = pageList || (await loadPageList(undefined, languagesOnly))
|
||||
const pageMap = createMapFromArray(pages)
|
||||
return pageMap
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'assert'
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import cheerio from 'cheerio'
|
||||
import getApplicableVersions from '@/versions/lib/get-applicable-versions'
|
||||
import generateRedirectsForPermalinks from '@/redirects/lib/permalinks'
|
||||
@@ -19,7 +20,7 @@ import { renderContentWithFallback } from '@/languages/lib/render-with-fallback'
|
||||
import { deprecated, supported } from '@/versions/lib/enterprise-server-releases'
|
||||
import { allPlatforms } from '@/tools/lib/all-platforms'
|
||||
|
||||
import type { Context, FrontmatterVersions } from '@/types'
|
||||
import type { Context, FrontmatterVersions, FeaturedLinksExpanded } from '@/types'
|
||||
|
||||
// We're going to check a lot of pages' "ID" (the first part of
|
||||
// the relativePath) against `productMap` to make sure it's valid.
|
||||
@@ -97,6 +98,8 @@ class Page {
|
||||
public rawIntroLinks?: Record<string, string>
|
||||
public recommended?: string[]
|
||||
public rawRecommended?: string[]
|
||||
public autogenerated?: string
|
||||
public featuredLinks?: FeaturedLinksExpanded
|
||||
|
||||
// Derived properties
|
||||
public languageCode!: string
|
||||
@@ -104,6 +107,7 @@ class Page {
|
||||
public basePath!: string
|
||||
public fullPath!: string
|
||||
public markdown!: string
|
||||
public mtime!: number
|
||||
public documentType: string
|
||||
public applicableVersions: string[]
|
||||
public permalinks: Permalink[]
|
||||
@@ -142,6 +146,10 @@ class Page {
|
||||
errors: frontmatterErrors,
|
||||
}: ReadFileContentsResult = await readFileContents(fullPath)
|
||||
|
||||
// Get file modification time
|
||||
const stats = await fs.stat(fullPath)
|
||||
const mtime = stats.mtimeMs
|
||||
|
||||
// The `|| ''` is for pages that are purely frontmatter.
|
||||
// So the `content` property will be `undefined`.
|
||||
let markdown = content || ''
|
||||
@@ -178,6 +186,7 @@ class Page {
|
||||
fullPath,
|
||||
...(data || {}),
|
||||
markdown,
|
||||
mtime,
|
||||
frontmatterErrors,
|
||||
} as PageReadResult
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -70,10 +70,10 @@ export default async function contextualize(
|
||||
req.context.redirects = redirects
|
||||
req.context.site = {
|
||||
data: {
|
||||
ui: getUIDataMerged(req.language),
|
||||
ui: getUIDataMerged(req.language!),
|
||||
},
|
||||
}
|
||||
req.context.getDottedData = (dottedPath) => getDataByLanguage(dottedPath, req.language)
|
||||
req.context.getDottedData = (dottedPath) => getDataByLanguage(dottedPath, req.language!)
|
||||
req.context.siteTree = siteTree
|
||||
req.context.pages = pageMap
|
||||
req.context.nonEnterpriseDefaultVersion = nonEnterpriseDefaultVersion
|
||||
|
||||
@@ -39,7 +39,7 @@ export default async function glossaries(req: ExtendedRequest, res: Response, ne
|
||||
// injected there it needs to have its own possible Liquid rendered out.
|
||||
const glossariesRaw: Glossary[] = getDataByLanguage(
|
||||
'glossaries.external',
|
||||
req.context.currentLanguage,
|
||||
req.context.currentLanguage!,
|
||||
)
|
||||
const glossaries = (
|
||||
await Promise.all(
|
||||
|
||||
@@ -9,6 +9,14 @@ import { allVersions } from '@/versions/lib/all-versions'
|
||||
import enterpriseServerReleases, { latest } from '@/versions/lib/enterprise-server-releases'
|
||||
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version'
|
||||
|
||||
interface TestContext {
|
||||
currentVersion: string
|
||||
currentLanguage: string
|
||||
currentPath?: string
|
||||
enterpriseServerVersions?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const enterpriseServerVersions = Object.keys(allVersions).filter((v) =>
|
||||
v.startsWith('enterprise-server@'),
|
||||
@@ -27,12 +35,14 @@ const opts = {
|
||||
describe('Page class', () => {
|
||||
test('preserves file path info', async () => {
|
||||
const page = await Page.init(opts)
|
||||
expect(page.relativePath).toBe(opts.relativePath)
|
||||
expect(page.fullPath.includes(page.relativePath)).toBe(true)
|
||||
expect(page!.relativePath).toBe(opts.relativePath)
|
||||
expect(page!.fullPath.includes(page!.relativePath)).toBe(true)
|
||||
})
|
||||
|
||||
describe('showMiniToc page property', () => {
|
||||
let article, articleWithFM, tocPage
|
||||
let article: Page | undefined
|
||||
let articleWithFM: Page | undefined
|
||||
let tocPage: Page | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
article = await Page.init({
|
||||
@@ -43,10 +53,10 @@ describe('Page class', () => {
|
||||
|
||||
articleWithFM = await Page.init({
|
||||
showMiniToc: false,
|
||||
relativePath: article.relativePath,
|
||||
basePath: article.basePath,
|
||||
languageCode: article.languageCode,
|
||||
})
|
||||
relativePath: article!.relativePath,
|
||||
basePath: article!.basePath,
|
||||
languageCode: article!.languageCode,
|
||||
} as any)
|
||||
|
||||
tocPage = await Page.init({
|
||||
relativePath: 'sample-toc-index.md',
|
||||
@@ -56,16 +66,16 @@ describe('Page class', () => {
|
||||
})
|
||||
|
||||
test('is true by default on articles', () => {
|
||||
expect(article.showMiniToc).toBe(true)
|
||||
expect(article!.showMiniToc).toBe(true)
|
||||
})
|
||||
|
||||
test('is false on articles when set in frontmatter', () => {
|
||||
expect(articleWithFM.showMiniToc).toBe(false)
|
||||
expect(articleWithFM!.showMiniToc).toBe(false)
|
||||
})
|
||||
|
||||
// products, categories, and subcategories have index.md pages
|
||||
test('is undefined by default on index.md pages', () => {
|
||||
expect(tocPage.showMiniToc).toBeUndefined()
|
||||
expect(tocPage!.showMiniToc).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -80,40 +90,40 @@ describe('Page class', () => {
|
||||
languageCode: 'en',
|
||||
})
|
||||
// set version to the latest enterprise version
|
||||
const context = {
|
||||
const context: TestContext = {
|
||||
currentVersion: `enterprise-server@${enterpriseServerReleases.latest}`,
|
||||
currentLanguage: 'en',
|
||||
enterpriseServerVersions,
|
||||
}
|
||||
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page.relativePath}`
|
||||
let rendered = await page.render(context)
|
||||
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}`
|
||||
let rendered = await page!.render(context)
|
||||
let $ = cheerio.load(rendered)
|
||||
expect($.text()).toBe(
|
||||
expect(($ as any).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')
|
||||
expect(($ as any).text()).not.toBe('This text should only render on non-Enterprise')
|
||||
|
||||
// change version to the oldest enterprise version, re-render, and test again;
|
||||
// the results should be the same
|
||||
context.currentVersion = `enterprise-server@${enterpriseServerReleases.oldestSupported}`
|
||||
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page.relativePath}`
|
||||
rendered = await page.render(context)
|
||||
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}`
|
||||
rendered = await page!.render(context)
|
||||
$ = cheerio.load(rendered)
|
||||
expect($.text()).toBe(
|
||||
expect(($ as any).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')
|
||||
expect(($ as any).text()).not.toBe('This text should only render on non-Enterprise')
|
||||
|
||||
// change version to non-enterprise, re-render, and test again;
|
||||
// the results should be the opposite
|
||||
context.currentVersion = nonEnterpriseDefaultVersion
|
||||
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page.relativePath}`
|
||||
rendered = await page.render(context)
|
||||
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}`
|
||||
rendered = await page!.render(context)
|
||||
$ = cheerio.load(rendered)
|
||||
expect($.text()).not.toBe(
|
||||
expect(($ as any).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')
|
||||
expect(($ as any).text()).toBe('This text should only render on non-Enterprise')
|
||||
})
|
||||
|
||||
test('support next to-be-released Enterprise Server version in frontmatter', async () => {
|
||||
@@ -124,19 +134,19 @@ describe('Page class', () => {
|
||||
languageCode: 'en',
|
||||
})
|
||||
// set version to 3.0
|
||||
const context = {
|
||||
const context: TestContext = {
|
||||
currentVersion: 'enterprise-server@3.0',
|
||||
currentLanguage: 'en',
|
||||
}
|
||||
await expect(() => {
|
||||
return page.render(context)
|
||||
return page!.render(context)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
test('preserves `languageCode`', async () => {
|
||||
const page = await Page.init(opts)
|
||||
expect(page.languageCode).toBe('en')
|
||||
expect(page!.languageCode).toBe('en')
|
||||
})
|
||||
|
||||
test('parentProductId getter', async () => {
|
||||
@@ -145,32 +155,32 @@ describe('Page class', () => {
|
||||
basePath: path.join(__dirname, '../../../src/fixtures/fixtures/products'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(page.parentProductId).toBe('github')
|
||||
expect(page!.parentProductId).toBe('github')
|
||||
|
||||
page = await Page.init({
|
||||
relativePath: 'actions/some-category/some-article.md',
|
||||
basePath: path.join(__dirname, '../../../src/fixtures/fixtures/products'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(page.parentProductId).toBe('actions')
|
||||
expect(page!.parentProductId).toBe('actions')
|
||||
|
||||
page = await Page.init({
|
||||
relativePath: 'admin/some-category/some-article.md',
|
||||
basePath: path.join(__dirname, '../../../src/fixtures/fixtures/products'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(page.parentProductId).toBe('admin')
|
||||
expect(page!.parentProductId).toBe('admin')
|
||||
})
|
||||
|
||||
describe('permalinks', () => {
|
||||
test('is an array', async () => {
|
||||
const page = await Page.init(opts)
|
||||
expect(Array.isArray(page.permalinks)).toBe(true)
|
||||
expect(Array.isArray(page!.permalinks)).toBe(true)
|
||||
})
|
||||
|
||||
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)
|
||||
const pageVersions = page!.permalinks.map((permalink: any) => permalink.pageVersion)
|
||||
expect(
|
||||
enterpriseServerReleases.supported.every((version) =>
|
||||
pageVersions.includes(`enterprise-server@${version}`),
|
||||
@@ -188,15 +198,16 @@ describe('Page class', () => {
|
||||
const expectedPath =
|
||||
'pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches'
|
||||
expect(
|
||||
page.permalinks.find((permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion)
|
||||
.href,
|
||||
page!.permalinks.find(
|
||||
(permalink: any) => permalink.pageVersion === nonEnterpriseDefaultVersion,
|
||||
)!.href,
|
||||
).toBe(`/en/${expectedPath}`)
|
||||
expect(
|
||||
page.permalinks.find(
|
||||
(permalink) =>
|
||||
page!.permalinks.find(
|
||||
(permalink: any) =>
|
||||
permalink.pageVersion ===
|
||||
`enterprise-server@${enterpriseServerReleases.oldestSupported}`,
|
||||
).href,
|
||||
)!.href,
|
||||
).toBe(`/en/enterprise-server@${enterpriseServerReleases.oldestSupported}/${expectedPath}`)
|
||||
})
|
||||
|
||||
@@ -207,15 +218,15 @@ describe('Page class', () => {
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(
|
||||
page.permalinks.find((permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion)
|
||||
.href,
|
||||
page!.permalinks.find((permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion)
|
||||
?.href,
|
||||
).toBe('/en')
|
||||
expect(
|
||||
page.permalinks.find(
|
||||
page!.permalinks.find(
|
||||
(permalink) =>
|
||||
permalink.pageVersion ===
|
||||
`enterprise-server@${enterpriseServerReleases.oldestSupported}`,
|
||||
).href,
|
||||
)?.href,
|
||||
).toBe(`/en/enterprise-server@${enterpriseServerReleases.oldestSupported}`)
|
||||
})
|
||||
|
||||
@@ -226,15 +237,14 @@ describe('Page class', () => {
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(
|
||||
page.permalinks.find(
|
||||
(permalink) =>
|
||||
permalink.pageVersion === `enterprise-server@${enterpriseServerReleases.latest}`,
|
||||
).href,
|
||||
page!.permalinks.find(
|
||||
(permalink: any) => permalink.pageVersion === `enterprise-server@${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)
|
||||
const pageVersions = page!.permalinks.map((permalink: any) => permalink.pageVersion)
|
||||
expect(page!.permalinks.length).toBeGreaterThan(0)
|
||||
expect(pageVersions.includes(nonEnterpriseDefaultVersion)).toBe(false)
|
||||
})
|
||||
|
||||
@@ -245,15 +255,16 @@ describe('Page class', () => {
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(
|
||||
page.permalinks.find((permalink) => permalink.pageVersion === nonEnterpriseDefaultVersion)
|
||||
.href,
|
||||
page!.permalinks.find(
|
||||
(permalink: any) => permalink.pageVersion === nonEnterpriseDefaultVersion,
|
||||
)!.href,
|
||||
).toBe('/en/products/actions/some-category/some-article')
|
||||
expect(page.permalinks.length).toBe(1)
|
||||
expect(page!.permalinks.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('videos', () => {
|
||||
let page
|
||||
let page: Page | undefined
|
||||
|
||||
beforeEach(async () => {
|
||||
page = await Page.init({
|
||||
@@ -264,7 +275,7 @@ describe('Page class', () => {
|
||||
})
|
||||
|
||||
test('includes videos specified in the featuredLinks frontmatter', async () => {
|
||||
expect(page.featuredLinks.videos).toStrictEqual([
|
||||
expect((page as any)!.featuredLinks.videos).toStrictEqual([
|
||||
{
|
||||
title: 'codespaces',
|
||||
href: 'https://www.youtube-nocookie.com/embed/_W9B7qc9lVc',
|
||||
@@ -279,7 +290,7 @@ describe('Page class', () => {
|
||||
},
|
||||
])
|
||||
|
||||
expect(page.featuredLinks.videosHeading).toBe('Custom Videos heading')
|
||||
expect((page as any)!.featuredLinks.videosHeading).toBe('Custom Videos heading')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -291,7 +302,7 @@ describe('Page class', () => {
|
||||
languageCode: 'en',
|
||||
})
|
||||
|
||||
expect(page.introLinks).toStrictEqual({
|
||||
expect(page!.introLinks).toStrictEqual({
|
||||
overview: 'https://github.com',
|
||||
'custom link!': 'https://github.com/features',
|
||||
})
|
||||
@@ -302,7 +313,7 @@ describe('Page class', () => {
|
||||
test('throws an error on bad input', () => {
|
||||
const markdown = null
|
||||
expect(() => {
|
||||
Page.parseFrontmatter('some/file.md', markdown)
|
||||
;(Page as any).parseFrontmatter('some/file.md', markdown)
|
||||
}).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -314,10 +325,10 @@ describe('Page class', () => {
|
||||
basePath: path.join(__dirname, '../../../src/fixtures/fixtures'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(page.versions.fpt).toBe('*')
|
||||
expect(page.versions.ghes).toBe('>3.0')
|
||||
expect(page.applicableVersions.includes('free-pro-team@latest')).toBe(true)
|
||||
expect(page.applicableVersions.includes(`enterprise-server@${latest}`)).toBe(true)
|
||||
expect((page!.versions as any).fpt).toBe('*')
|
||||
expect((page!.versions as any).ghes).toBe('>3.0')
|
||||
expect(page!.applicableVersions.includes('free-pro-team@latest')).toBe(true)
|
||||
expect(page!.applicableVersions.includes(`enterprise-server@${latest}`)).toBe(true)
|
||||
})
|
||||
|
||||
test('index page', async () => {
|
||||
@@ -326,7 +337,7 @@ describe('Page class', () => {
|
||||
basePath: path.join(__dirname, '../../../content'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(page.versions).toEqual({ fpt: '*', ghec: '*', ghes: '*' })
|
||||
expect(page!.versions).toEqual({ fpt: '*', ghec: '*', ghes: '*' })
|
||||
})
|
||||
|
||||
test('enterprise admin index page', async () => {
|
||||
@@ -336,8 +347,8 @@ describe('Page class', () => {
|
||||
languageCode: 'en',
|
||||
})
|
||||
|
||||
expect(nonEnterpriseDefaultPlan in page.versions).toBe(false)
|
||||
expect(page.versions.ghes).toBe('*')
|
||||
expect(nonEnterpriseDefaultPlan in page!.versions).toBe(false)
|
||||
expect(page!.versions.ghes).toBe('*')
|
||||
})
|
||||
|
||||
test('feature versions frontmatter', async () => {
|
||||
@@ -361,9 +372,8 @@ describe('Page class', () => {
|
||||
})
|
||||
|
||||
// Test the raw page data.
|
||||
expect(page.versions.fpt).toBe('*')
|
||||
expect(page.versions.ghes).toBe('>2.21')
|
||||
expect(page.versions.ghae).toBeUndefined()
|
||||
expect(page!.versions.fpt).toBe('*')
|
||||
expect(page!.versions.ghes).toBe('>2.21')
|
||||
|
||||
// Test the resolved versioning, where GHES releases specified in frontmatter and in
|
||||
// feature versions are combined (i.e., one doesn't overwrite the other).
|
||||
@@ -372,10 +382,10 @@ describe('Page class', () => {
|
||||
// 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
|
||||
// the frontmatter GHES `*` is not being overwritten by the placeholder's GHES `<3.0`.
|
||||
expect(page.applicableVersions.includes('free-pro-team@latest')).toBe(true)
|
||||
expect(page.applicableVersions.includes(`enterprise-server@${latest}`)).toBe(true)
|
||||
expect(page.applicableVersions.includes('feature')).toBe(false)
|
||||
expect(page.applicableVersions.includes('placeholder')).toBe(false)
|
||||
expect(page!.applicableVersions.includes('free-pro-team@latest')).toBe(true)
|
||||
expect(page!.applicableVersions.includes(`enterprise-server@${latest}`)).toBe(true)
|
||||
expect(page!.applicableVersions.includes('feature')).toBe(false)
|
||||
expect(page!.applicableVersions.includes('placeholder')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -386,8 +396,8 @@ describe('Page class', () => {
|
||||
basePath: path.join(__dirname, '../../../src/fixtures/fixtures/products'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(page.defaultPlatform).toBeDefined()
|
||||
expect(page.defaultPlatform).toBe('linux')
|
||||
expect((page as any)!.defaultPlatform).toBeDefined()
|
||||
expect((page as any)!.defaultPlatform).toBe('linux')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -398,8 +408,8 @@ describe('Page class', () => {
|
||||
basePath: path.join(__dirname, '../../../src/fixtures/fixtures'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
expect(page.defaultTool).toBeDefined()
|
||||
expect(page.defaultTool).toBe('cli')
|
||||
expect((page as any)!.defaultTool).toBeDefined()
|
||||
expect((page as any)!.defaultTool).toBe('cli')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -461,7 +471,7 @@ describe('catches errors thrown in Page class', () => {
|
||||
basePath: path.join(__dirname, '../../../src/fixtures/fixtures'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
const context = {
|
||||
const context: any = {
|
||||
page: { version: `enterprise-server@3.2` },
|
||||
currentVersion: `enterprise-server@3.2`,
|
||||
currentVersionObj: {},
|
||||
@@ -471,18 +481,18 @@ describe('catches errors thrown in Page class', () => {
|
||||
fpt: false, // what the shortVersions contextualizer does
|
||||
}
|
||||
|
||||
await page.render(context)
|
||||
expect(page.product).toBe('')
|
||||
expect(page.permissions).toBe('')
|
||||
await page!.render(context)
|
||||
expect(page!.product).toBe('')
|
||||
expect(page!.permissions).toBe('')
|
||||
|
||||
// Change to FPT
|
||||
context.page.version = nonEnterpriseDefaultVersion
|
||||
context.version = nonEnterpriseDefaultVersion
|
||||
context.currentPath = '/en/optional/attributes'
|
||||
context.fpt = true
|
||||
await page.render(context)
|
||||
expect(page.product).toContain('FPT rulez!')
|
||||
expect(page.permissions).toContain('FPT only!')
|
||||
await page!.render(context)
|
||||
expect(page!.product).toContain('FPT rulez!')
|
||||
expect(page!.permissions).toContain('FPT only!')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
// csp-parse doesn't have TypeScript types
|
||||
import CspParse from 'csp-parse'
|
||||
import { beforeAll, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
@@ -10,6 +11,11 @@ import {
|
||||
makeLanguageSurrogateKey,
|
||||
} from '@/frame/middleware/set-fastly-surrogate-key'
|
||||
|
||||
interface Category {
|
||||
name: string
|
||||
published_articles: string[]
|
||||
}
|
||||
|
||||
describe('server', () => {
|
||||
vi.setConfig({ testTimeout: 60 * 1000 })
|
||||
|
||||
@@ -81,11 +87,14 @@ describe('server', () => {
|
||||
// it will render a very basic plain text 404 response.
|
||||
const $ = await getDOM('/en/not-a-real-page', { allow404: true })
|
||||
expect($('h1').first().text()).toBe('Ooops!')
|
||||
expect($.text().includes("It looks like this page doesn't exist.")).toBe(true)
|
||||
// Using type assertion because cheerio v1 types don't include text() on root
|
||||
expect(($ as any).text().includes("It looks like this page doesn't exist.")).toBe(true)
|
||||
expect(
|
||||
$.text().includes(
|
||||
'We track these errors automatically, but if the problem persists please feel free to contact us.',
|
||||
),
|
||||
($ as any)
|
||||
.text()
|
||||
.includes(
|
||||
'We track these errors automatically, but if the problem persists please feel free to contact us.',
|
||||
),
|
||||
).toBe(true)
|
||||
expect($.res.statusCode).toBe(404)
|
||||
})
|
||||
@@ -107,11 +116,14 @@ describe('server', () => {
|
||||
test('renders a 500 page when errors are thrown', async () => {
|
||||
const $ = await getDOM('/_500', { allow500s: true })
|
||||
expect($('h1').first().text()).toBe('Ooops!')
|
||||
expect($.text().includes('It looks like something went wrong.')).toBe(true)
|
||||
// Using type assertion because cheerio v1 types don't include text() on root
|
||||
expect(($ as any).text().includes('It looks like something went wrong.')).toBe(true)
|
||||
expect(
|
||||
$.text().includes(
|
||||
'We track these errors automatically, but if the problem persists please feel free to contact us.',
|
||||
),
|
||||
($ as any)
|
||||
.text()
|
||||
.includes(
|
||||
'We track these errors automatically, but if the problem persists please feel free to contact us.',
|
||||
),
|
||||
).toBe(true)
|
||||
expect($.res.statusCode).toBe(500)
|
||||
})
|
||||
@@ -154,7 +166,7 @@ describe('server', () => {
|
||||
const categories = JSON.parse(res.body)
|
||||
expect(Array.isArray(categories)).toBe(true)
|
||||
expect(categories.length).toBeGreaterThan(1)
|
||||
categories.forEach((category) => {
|
||||
categories.forEach((category: Category) => {
|
||||
expect('name' in category).toBe(true)
|
||||
expect('published_articles' in category).toBe(true)
|
||||
})
|
||||
@@ -19,15 +19,13 @@ import loadRedirects from '@/redirects/lib/precompile'
|
||||
import { loadPageMap, loadPages } from '@/frame/lib/page-data'
|
||||
import { languageKeys } from '@/languages/lib/languages'
|
||||
import { RewriteAssetPathsPlugin } from '@/ghes-releases/scripts/deprecate/rewrite-asset-paths'
|
||||
import type { Page } from '@/types'
|
||||
import Page from '@/frame/lib/page'
|
||||
|
||||
const port = '4001'
|
||||
const host = `http://localhost:${port}`
|
||||
const version = EnterpriseServerReleases.oldestSupported
|
||||
const GH_PAGES_URL = `https://github.github.com/docs-ghes-${version}`
|
||||
|
||||
// Once page-data.js is converted to TS,
|
||||
// we can import the more comprehesive type
|
||||
type PageList = Page[]
|
||||
type MapObj = { [key: string]: string }
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('server rendering certain GraphQL pages', () => {
|
||||
)
|
||||
.filter(Boolean)
|
||||
const nonFPTPermalinksHrefs = nonFPTPermalinks.map((permalink) => {
|
||||
return permalink.href
|
||||
return permalink!.href
|
||||
})
|
||||
|
||||
test.each(nonFPTPermalinksHrefs)(
|
||||
|
||||
@@ -43,7 +43,7 @@ export default async function processLearningTracks(
|
||||
// fall back to English if they don't exist on disk in the translation.
|
||||
const track = getDataByLanguage(
|
||||
`learning-tracks.${context.currentProduct}.${renderedTrackName}`,
|
||||
context.currentLanguage,
|
||||
context.currentLanguage!,
|
||||
)
|
||||
if (!track) {
|
||||
throw new Error(`No learning track called '${renderedTrackName}'.`)
|
||||
|
||||
@@ -29,7 +29,10 @@ export default async function learningTrack(
|
||||
let trackProduct = req.context.currentProduct as string
|
||||
// TODO: Once getDeepDataByLanguage is ported to TS
|
||||
// a more appropriate API would be to use `getDeepDataByLanguage<LearningTracks)(...)`
|
||||
const allLearningTracks = getDeepDataByLanguage('learning-tracks', req.language) as LearningTracks
|
||||
const allLearningTracks = getDeepDataByLanguage(
|
||||
'learning-tracks',
|
||||
req.language!,
|
||||
) as LearningTracks
|
||||
|
||||
if (req.language !== 'en') {
|
||||
// Don't trust the `.guides` from the translation. It too often has
|
||||
|
||||
6
src/tests/declarations.d.ts
vendored
Normal file
6
src/tests/declarations.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module 'csp-parse' {
|
||||
export default class CspParse {
|
||||
constructor(cspHeader: string)
|
||||
get(directive: string): string | string[]
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,7 @@ export type FeaturedLinkExpanded = {
|
||||
intro?: string
|
||||
}
|
||||
|
||||
type FeaturedLinksExpanded = {
|
||||
export type FeaturedLinksExpanded = {
|
||||
[key: string]: FeaturedLinkExpanded[]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user