diff --git a/src/automated-pipelines/tests/rendering.ts b/src/automated-pipelines/tests/rendering.ts index e9b23febc4..a58fc7f0ad 100644 --- a/src/automated-pipelines/tests/rendering.ts +++ b/src/automated-pipelines/tests/rendering.ts @@ -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: { diff --git a/src/content-linter/tests/lint-frontmatter-links.ts b/src/content-linter/tests/lint-frontmatter-links.ts index bd0dd47633..c0e5d9fbc4 100644 --- a/src/content-linter/tests/lint-frontmatter-links.ts +++ b/src/content-linter/tests/lint-frontmatter-links.ts @@ -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 diff --git a/src/data-directory/lib/get-data.js b/src/data-directory/lib/get-data.ts similarity index 76% rename from src/data-directory/lib/get-data.js rename to src/data-directory/lib/get-data.ts index 4ea6f90099..8565df9b38 100644 --- a/src/data-directory/lib/get-data.js +++ b/src/data-directory/lib/get-data.ts @@ -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 any>(func: T): T { + const cache = new Map() + 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 } diff --git a/src/data-directory/tests/get-data.js b/src/data-directory/tests/get-data.ts similarity index 99% rename from src/data-directory/tests/get-data.js rename to src/data-directory/tests/get-data.ts index cd7d033eeb..df7f5dfcc4 100644 --- a/src/data-directory/tests/get-data.js +++ b/src/data-directory/tests/get-data.ts @@ -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, {}) diff --git a/src/frame/lib/frontmatter.js b/src/frame/lib/frontmatter.ts similarity index 92% rename from src/frame/lib/frontmatter.js rename to src/frame/lib/frontmatter.ts index 2e27e8e465..6d26a951ef 100644 --- a/src/frame/lib/frontmatter.js +++ b/src/frame/lib/frontmatter.ts @@ -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 + required?: string[] + additionalProperties?: boolean + format?: string + description?: string + minItems?: number + maxItems?: number +} + +interface Schema { + type: string + required: string[] + additionalProperties: boolean + properties: Record +} + 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)[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).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, } diff --git a/src/frame/lib/page-data.js b/src/frame/lib/page-data.ts similarity index 77% rename from src/frame/lib/page-data.js rename to src/frame/lib/page-data.ts index 9566bfd179..6f0d204705 100644 --- a/src/frame/lib/page-data.js +++ b/src/frame/lib/page-data.ts @@ -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 { 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, setB: Set): 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 { + const item: Partial = {} 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 { 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 { // 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 { 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 { 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 { + const pageMap = pageList.reduce( + (pageMap: Record, page: Page) => { + for (const permalink of page.permalinks) { + pageMap[permalink.href] = page + } + return pageMap + }, + {} as Record, + ) return pageMap } -export async function loadPageMap(pageList, languagesOnly = []) { - const pages = pageList || (await loadPageList(languagesOnly)) +export async function loadPageMap( + pageList?: Page[], + languagesOnly: string[] = [], +): Promise> { + const pages = pageList || (await loadPageList(undefined, languagesOnly)) const pageMap = createMapFromArray(pages) return pageMap } diff --git a/src/frame/lib/page.ts b/src/frame/lib/page.ts index 3ce5de6edc..5ef97334ba 100644 --- a/src/frame/lib/page.ts +++ b/src/frame/lib/page.ts @@ -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 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) { diff --git a/src/frame/middleware/context/context.ts b/src/frame/middleware/context/context.ts index 80fcf8990c..7a9ddcb826 100644 --- a/src/frame/middleware/context/context.ts +++ b/src/frame/middleware/context/context.ts @@ -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 diff --git a/src/frame/middleware/context/glossaries.ts b/src/frame/middleware/context/glossaries.ts index d8eb5669db..047bf246e6 100644 --- a/src/frame/middleware/context/glossaries.ts +++ b/src/frame/middleware/context/glossaries.ts @@ -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( diff --git a/src/frame/tests/page.js b/src/frame/tests/page.ts similarity index 76% rename from src/frame/tests/page.js rename to src/frame/tests/page.ts index 8c93b709d6..9057315ef1 100644 --- a/src/frame/tests/page.js +++ b/src/frame/tests/page.ts @@ -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!') }) }) }) diff --git a/src/frame/tests/server.js b/src/frame/tests/server.ts similarity index 94% rename from src/frame/tests/server.js rename to src/frame/tests/server.ts index 4ae8e418ce..916871d50f 100644 --- a/src/frame/tests/server.js +++ b/src/frame/tests/server.ts @@ -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) }) diff --git a/src/ghes-releases/scripts/deprecate/archive-version.ts b/src/ghes-releases/scripts/deprecate/archive-version.ts index de266713c5..01509d0777 100755 --- a/src/ghes-releases/scripts/deprecate/archive-version.ts +++ b/src/ghes-releases/scripts/deprecate/archive-version.ts @@ -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 } diff --git a/src/graphql/tests/server-rendering.ts b/src/graphql/tests/server-rendering.ts index 9219b28ce9..cc3d823859 100644 --- a/src/graphql/tests/server-rendering.ts +++ b/src/graphql/tests/server-rendering.ts @@ -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)( diff --git a/src/learning-track/lib/process-learning-tracks.ts b/src/learning-track/lib/process-learning-tracks.ts index 24a961f4a4..e4871081b3 100644 --- a/src/learning-track/lib/process-learning-tracks.ts +++ b/src/learning-track/lib/process-learning-tracks.ts @@ -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}'.`) diff --git a/src/learning-track/middleware/learning-track.ts b/src/learning-track/middleware/learning-track.ts index 827f796567..b9c1cab791 100644 --- a/src/learning-track/middleware/learning-track.ts +++ b/src/learning-track/middleware/learning-track.ts @@ -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