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