Port whats-new-changelog.js to TypeScript (#51199)
This commit is contained in:
@@ -4,11 +4,13 @@ import path from 'path'
|
||||
|
||||
import Parser from 'rss-parser'
|
||||
|
||||
import type { ChangelogItem } from '@/types'
|
||||
|
||||
const CHANGELOG_CACHE_FILE_PATH = process.env.CHANGELOG_CACHE_FILE_PATH
|
||||
// This is useful to set when doing things like sync search.
|
||||
const CHANGELOG_DISABLED = Boolean(JSON.parse(process.env.CHANGELOG_DISABLED || 'false'))
|
||||
|
||||
async function getRssFeed(url) {
|
||||
async function getRssFeed(url: string) {
|
||||
const parser = new Parser({ timeout: 5000 })
|
||||
const feedUrl = `${url}/feed`
|
||||
let feed
|
||||
@@ -16,14 +18,18 @@ async function getRssFeed(url) {
|
||||
try {
|
||||
feed = await parser.parseURL(feedUrl)
|
||||
} catch (err) {
|
||||
console.error(`cannot get ${feedUrl}: ${err.message}`)
|
||||
console.error(`cannot get ${feedUrl}: ${err instanceof Error ? err.message : err}`)
|
||||
return
|
||||
}
|
||||
|
||||
return feed
|
||||
}
|
||||
|
||||
export async function getChangelogItems(prefix, feedUrl, ignoreCache = false) {
|
||||
export async function getChangelogItems(
|
||||
prefix: string | undefined,
|
||||
feedUrl: string,
|
||||
ignoreCache = false,
|
||||
): Promise<ChangelogItem[] | undefined> {
|
||||
if (CHANGELOG_DISABLED) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`Downloading changelog (${feedUrl}) items is disabled.`)
|
||||
@@ -44,14 +50,15 @@ export async function getChangelogItems(prefix, feedUrl, ignoreCache = false) {
|
||||
}
|
||||
|
||||
// only show the first 3 posts
|
||||
const changelog = feed.items.slice(0, 3).map((item) => {
|
||||
const changelog: ChangelogItem[] = feed.items.slice(0, 3).map((item) => {
|
||||
const rawTitle = item.title as string
|
||||
// remove the prefix if it exists (Ex: 'GitHub Actions: '), where the colon and expected whitespace should be hardcoded.
|
||||
const title = prefix ? item.title.replace(new RegExp(`^${prefix}`), '') : item.title
|
||||
const title = prefix ? rawTitle.replace(new RegExp(`^${prefix}`), '') : rawTitle
|
||||
return {
|
||||
// capitalize the first letter of the title
|
||||
title: title.trim().charAt(0).toUpperCase() + title.slice(1),
|
||||
date: item.isoDate,
|
||||
href: item.link,
|
||||
date: item.isoDate as string,
|
||||
href: item.link as string,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -65,13 +72,13 @@ export async function getChangelogItems(prefix, feedUrl, ignoreCache = false) {
|
||||
|
||||
const globalCache = new Map()
|
||||
|
||||
function getChangelogCacheKey(prefix, feedUrl) {
|
||||
function getChangelogCacheKey(prefix: string | undefined, feedUrl: string) {
|
||||
// Return a string that is only letters so it's safe to use this
|
||||
// for the filename when caching to disk.
|
||||
return `${prefix || ''}${feedUrl}`.replace(/[^a-z]+/gi, '')
|
||||
}
|
||||
|
||||
function getDiskCachePath(prefix, feedUrl) {
|
||||
function getDiskCachePath(prefix: string | undefined, feedUrl: string) {
|
||||
// When in local development or in tests, use disk caching
|
||||
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') {
|
||||
if (CHANGELOG_CACHE_FILE_PATH) {
|
||||
@@ -84,7 +91,7 @@ function getDiskCachePath(prefix, feedUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
function getChangelogItemsFromCache(prefix, feedUrl) {
|
||||
function getChangelogItemsFromCache(prefix: string | undefined, feedUrl: string) {
|
||||
const cacheKey = getChangelogCacheKey(prefix, feedUrl)
|
||||
|
||||
if (globalCache.get(cacheKey)) {
|
||||
@@ -103,7 +110,7 @@ function getChangelogItemsFromCache(prefix, feedUrl) {
|
||||
return payload
|
||||
} catch (err) {
|
||||
// If it wasn't on disk, that's fine.
|
||||
if (err.code === 'ENOENT') return
|
||||
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return
|
||||
// The JSON.parse() most likely failed. Ignore the error
|
||||
// but delete the file so it won't be attempted again.
|
||||
if (err instanceof SyntaxError) {
|
||||
@@ -115,7 +122,11 @@ function getChangelogItemsFromCache(prefix, feedUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
function setChangelogItemsCache(prefix, feedUrl, payload) {
|
||||
function setChangelogItemsCache(
|
||||
prefix: string | undefined,
|
||||
feedUrl: string,
|
||||
payload: ChangelogItem[],
|
||||
) {
|
||||
const cacheKey = getChangelogCacheKey(prefix, feedUrl)
|
||||
globalCache.set(cacheKey, payload)
|
||||
|
||||
@@ -4,10 +4,11 @@ import path from 'path'
|
||||
import nock from 'nock'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import { getChangelogItems } from '#src/changelogs/lib/changelog.js'
|
||||
import { getChangelogItems } from '@/changelogs/lib/changelog'
|
||||
import type { ChangelogItem } from '@/types'
|
||||
|
||||
describe('getChangelogItems module', () => {
|
||||
let changelog
|
||||
let changelog: ChangelogItem[] | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
const rssFeedContent = await fs.readFile(
|
||||
@@ -35,7 +36,7 @@ describe('getChangelogItems module', () => {
|
||||
afterAll(() => nock.cleanAll())
|
||||
|
||||
test('changelog contains 3 items', async () => {
|
||||
expect(changelog.length).toEqual(3)
|
||||
expect(changelog && changelog.length).toEqual(3)
|
||||
})
|
||||
|
||||
test('each changelog item has expected title, date, and href', async () => {
|
||||
@@ -57,6 +58,7 @@ describe('getChangelogItems module', () => {
|
||||
},
|
||||
]
|
||||
|
||||
if (!changelog) throw new Error('changelog is undefined')
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const changeLogEntry = changelog[i]
|
||||
const expectedEntry = expectedChangelogValues[i]
|
||||
@@ -1,7 +1,15 @@
|
||||
import { getChangelogItems } from '#src/changelogs/lib/changelog.js'
|
||||
import getApplicableVersions from '#src/versions/lib/get-applicable-versions.js'
|
||||
import type { Response, NextFunction } from 'express'
|
||||
|
||||
export default async function whatsNewChangelog(req, res, next) {
|
||||
import { getChangelogItems } from '@/changelogs/lib/changelog.js'
|
||||
import getApplicableVersions from '@/versions/lib/get-applicable-versions.js'
|
||||
import type { ExtendedRequest } from '@/types'
|
||||
|
||||
export default async function whatsNewChangelog(
|
||||
req: ExtendedRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
if (!req.context) throw new Error('request not contextualized')
|
||||
if (!req.context.page) return next()
|
||||
if (!req.context.page.changelog) return next()
|
||||
const label = req.context.page.changelog.label.split(/\s+/g).join('')
|
||||
@@ -16,7 +24,7 @@ export default async function whatsNewChangelog(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
const labelUrls = {
|
||||
const labelUrls: Record<string, string> = {
|
||||
education: 'https://github.blog/category/community/education',
|
||||
enterprise: 'https://github.blog/category/enterprise/',
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import categoriesForSupport from './categories-for-support'
|
||||
import triggerError from '@/observability/middleware/trigger-error'
|
||||
import secretScanning from '@/secret-scanning/middleware/secret-scanning'
|
||||
import ghesReleaseNotes from '@/release-notes/middleware/ghes-release-notes'
|
||||
import whatsNewChangelog from './context/whats-new-changelog.js'
|
||||
import whatsNewChangelog from './context/whats-new-changelog'
|
||||
import layout from './context/layout.js'
|
||||
import currentProductTree from './context/current-product-tree.js'
|
||||
import genericToc from './context/generic-toc.js'
|
||||
|
||||
15
src/types.ts
15
src/types.ts
@@ -74,6 +74,8 @@ export type Context = {
|
||||
languages?: Languages
|
||||
redirectNotFound?: string
|
||||
earlyAccessPageLinks?: string
|
||||
changelogUrl?: string
|
||||
whatsNewChangelog?: ChangelogItem[]
|
||||
secretScanningData?: SecretScanningData[]
|
||||
ghesReleases?: GHESRelease[]
|
||||
ghesReleaseNotes?: GHESReleasePatch[]
|
||||
@@ -123,6 +125,12 @@ export type ReleaseNotes = {
|
||||
}
|
||||
}
|
||||
|
||||
export type ChangelogItem = {
|
||||
title: string
|
||||
date: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export type SecretScanningData = {
|
||||
provider: string
|
||||
supportedSecret: string
|
||||
@@ -176,6 +184,13 @@ export type Page = {
|
||||
markdown: string
|
||||
versions: FrontmatterVersions
|
||||
applicableVersions: string[]
|
||||
changelog?: ChangeLog
|
||||
}
|
||||
|
||||
type ChangeLog = {
|
||||
label: string
|
||||
prefix?: string
|
||||
versions?: FrontmatterVersions
|
||||
}
|
||||
|
||||
export type Tree = {
|
||||
|
||||
Reference in New Issue
Block a user