diff --git a/Dockerfile b/Dockerfile index 505f3dd3a4..5e9ae0c3b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------------- # To update the sha, run `docker pull node:$VERSION-alpine` # look for something like: `Digest: sha256:0123456789abcdef` -FROM node:20-alpine@sha256:66c7d989b6dabba6b4305b88f40912679aebd9f387a5b16ffa76dfb9ae90b060 as base +FROM node:20-alpine@sha256:66f7f89199daea88a6b5d5aadaa6d20f7a16a90fc35274deda8e901e267d4bd7 as base # This directory is owned by the node user ARG APP_HOME=/home/node/app diff --git a/src/frame/middleware/context/context.ts b/src/frame/middleware/context/context.ts index dbf3c8a6cd..439b3f3c54 100644 --- a/src/frame/middleware/context/context.ts +++ b/src/frame/middleware/context/context.ts @@ -35,6 +35,8 @@ export default async function contextualize( const context: Context = {} req.context = context + console.log('CREATING CONTEXT') + req.context.process = { env: {} } // define each context property explicitly for code-search friendliness diff --git a/src/frame/middleware/fast-head.js b/src/frame/middleware/fast-head.ts similarity index 55% rename from src/frame/middleware/fast-head.js rename to src/frame/middleware/fast-head.ts index 9887773331..3685f92b0b 100644 --- a/src/frame/middleware/fast-head.js +++ b/src/frame/middleware/fast-head.ts @@ -1,6 +1,10 @@ +import type { Response, NextFunction } from 'express' + +import type { ExtendedRequest } from '@/types' import { defaultCacheControl } from './cache-control.js' -export default function fastHead(req, res, next) { +export default function fastHead(req: ExtendedRequest, res: Response, next: NextFunction) { + if (!req.context) throw new Error('request is not contextualized') const { context } = req const { page } = context if (page) { diff --git a/src/frame/middleware/favicons.js b/src/frame/middleware/favicons.ts similarity index 86% rename from src/frame/middleware/favicons.js rename to src/frame/middleware/favicons.ts index 1155f089c7..c3fafea883 100644 --- a/src/frame/middleware/favicons.js +++ b/src/frame/middleware/favicons.ts @@ -5,10 +5,19 @@ // solution to serve this directly. import fs from 'fs' +import type { Response, NextFunction } from 'express' + +import type { ExtendedRequest } from '@/types' import { SURROGATE_ENUMS, setFastlySurrogateKey } from './set-fastly-surrogate-key.js' import { assetCacheControl } from './cache-control.js' -const MAP = { +type IconConfig = { + contentType: string + buffer: () => Buffer +} +const MAP: { + [uri: string]: IconConfig +} = { '/favicon.ico': { contentType: 'image/x-icon', buffer: getBuffer('assets/images/site/favicon.ico'), @@ -35,8 +44,8 @@ MAP['/apple-touch-icon-precomposed.png'] = MAP['/apple-touch-icon.png'] MAP['/apple-touch-icon-120x120-precomposed.png'] = MAP['/apple-touch-icon-120x120.png'] MAP['/apple-touch-icon-152x152-precomposed.png'] = MAP['/apple-touch-icon-152x152.png'] -function getBuffer(filePath) { - let buffer +function getBuffer(filePath: string) { + let buffer: Buffer if (!fs.existsSync(filePath)) { throw new Error(`${filePath} not found on disk`) } @@ -51,7 +60,7 @@ function getBuffer(filePath) { } } -export default function favicons(req, res, next) { +export default function favicons(req: ExtendedRequest, res: Response, next: NextFunction) { if (!MAP[req.path]) return next() // This makes sure the CDN caching survives each production deployment. diff --git a/src/frame/middleware/index.ts b/src/frame/middleware/index.ts index e4c8f431e0..3b010aaf92 100644 --- a/src/frame/middleware/index.ts +++ b/src/frame/middleware/index.ts @@ -49,14 +49,14 @@ import features from '@/versions/middleware/features.js' import productExamples from './context/product-examples' import productGroups from './context/product-groups' import featuredLinks from '@/landings/middleware/featured-links' -import learningTrack from '@/learning-track/middleware/learning-track.js' +import learningTrack from '@/learning-track/middleware/learning-track' import next from './next.js' import renderPage from './render-page.js' import assetPreprocessing from '@/assets/middleware/asset-preprocessing' import archivedAssetRedirects from '@/archives/middleware/archived-asset-redirects' -import favicons from './favicons.js' +import favicons from './favicons' import setStaticAssetCaching from '@/assets/middleware/static-asset-caching' -import fastHead from './fast-head.js' +import fastHead from './fast-head' import fastlyCacheTest from './fastly-cache-test.js' import trailingSlashes from './trailing-slashes.js' import fastlyBehavior from './fastly-behavior.js' diff --git a/src/frame/tests/favicons.js b/src/frame/tests/favicons.ts similarity index 94% rename from src/frame/tests/favicons.js rename to src/frame/tests/favicons.ts index 5af409a5a8..030b35fdd1 100644 --- a/src/frame/tests/favicons.js +++ b/src/frame/tests/favicons.ts @@ -1,7 +1,7 @@ import { describe, expect, test, vi } from 'vitest' -import { SURROGATE_ENUMS } from '#src/frame/middleware/set-fastly-surrogate-key.js' -import { get } from '#src/tests/helpers/e2etest.js' +import { SURROGATE_ENUMS } from '@/frame/middleware/set-fastly-surrogate-key.js' +import { get } from '@/tests/helpers/e2etest.js' describe('favicon assets', () => { vi.setConfig({ testTimeout: 60 * 1000 }) diff --git a/src/learning-track/middleware/learning-track.js b/src/learning-track/middleware/learning-track.ts similarity index 64% rename from src/learning-track/middleware/learning-track.js rename to src/learning-track/middleware/learning-track.ts index 2f5a5b6e7d..823f4e8c8d 100644 --- a/src/learning-track/middleware/learning-track.js +++ b/src/learning-track/middleware/learning-track.ts @@ -1,25 +1,48 @@ -import { getPathWithoutLanguage, getPathWithoutVersion } from '#src/frame/lib/path-utils.js' -import getLinkData from '../lib/get-link-data.js' -import { renderContent } from '#src/content-render/index.js' -import { getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js' +import type { Response, NextFunction } from 'express' + +import type { + Context, + ExtendedRequest, + LearningTrack, + LearningTracks, + TrackGuide, + Page, +} from '@/types' +import { getPathWithoutLanguage, getPathWithoutVersion } from '@/frame/lib/path-utils.js' +import getLinkData from '../lib/get-link-data.js' +import { renderContent } from '@/content-render/index.js' +import { getDeepDataByLanguage } from '@/data-directory/lib/get-data.js' + +export default async function learningTrack( + req: ExtendedRequest, + res: Response, + next: NextFunction, +) { + if (!req.context) throw new Error('request is not contextualized') -export default async function learningTrack(req, res, next) { const noTrack = () => { - req.context.currentLearningTrack = {} + req.context!.currentLearningTrack = null return next() } if (!req.context.page) return next() - const trackName = req.query.learn - if (!trackName) return noTrack() + if (!req.query.learn) return noTrack() + if (Array.isArray(req.query.learn)) return noTrack() + const trackName = req.query.learn as string - let trackProduct = req.context.currentProduct - const allLearningTracks = getDeepDataByLanguage('learning-tracks', req.language) - if (req.langauge !== 'en') { + let trackProduct = req.context.currentProduct as string + // TODO: Once getDeepDataByLanguage is ported to TS + // a more appropriate API would be to use `getDeepDataByLanguage { return getPathWithoutLanguage(getPathWithoutVersion(guide.href)) @@ -97,8 +125,13 @@ export default async function learningTrack(req, res, next) { if (guideIndex > 0) { const prevGuidePath = trackGuidePaths[guideIndex - 1] - const result = await getLinkData(prevGuidePath, req.context, { title: true, intro: false }) - if (!result) return noTrack() + const resultData = await getLinkData(prevGuidePath, req.context, { + title: true, + intro: false, + fullTitle: false, + }) + if (!resultData) return noTrack() + const result = resultData as { href: string; page: Page; title: string } const href = result.href const title = result.title @@ -107,8 +140,13 @@ export default async function learningTrack(req, res, next) { if (guideIndex < trackGuidePaths.length - 1) { const nextGuidePath = trackGuidePaths[guideIndex + 1] - const result = await getLinkData(nextGuidePath, req.context, { title: true, intro: false }) - if (!result) return noTrack() + const resultData = await getLinkData(nextGuidePath, req.context, { + title: true, + intro: false, + fullTitle: false, + }) + if (!resultData) return noTrack() + const result = resultData as { href: string; page: Page; title: string } const href = result.href const title = result.title @@ -123,7 +161,11 @@ export default async function learningTrack(req, res, next) { // Find the index of a learning track guide path in an array of guide paths, // return -1 if not found. -async function indexOfLearningTrackGuide(trackGuidePaths, guidePath, context) { +async function indexOfLearningTrackGuide( + trackGuidePaths: string[], + guidePath: string, + context: Context, +) { let guideIndex = -1 for (let i = 0; i < trackGuidePaths.length; i++) { diff --git a/src/learning-track/tests/lint-data.js b/src/learning-track/tests/lint-data.ts similarity index 77% rename from src/learning-track/tests/lint-data.js rename to src/learning-track/tests/lint-data.ts index 6451a33946..3e1e3cafb1 100644 --- a/src/learning-track/tests/lint-data.js +++ b/src/learning-track/tests/lint-data.ts @@ -1,16 +1,19 @@ import { describe, expect, test } from 'vitest' -import { loadPages, loadPageMap } from '#src/frame/lib/page-data.js' -import loadRedirects from '#src/redirects/lib/precompile.js' -import { getDeepDataByLanguage } from '#src/data-directory/lib/get-data.js' -import { checkURL } from '#src/tests/helpers/check-url.js' +import type { LearningTracks } from '@/types' +import { loadPages, loadPageMap } from '@/frame/lib/page-data.js' +import loadRedirects from '@/redirects/lib/precompile.js' +import { getDeepDataByLanguage } from '@/data-directory/lib/get-data.js' +import { checkURL } from '@/tests/helpers/check-url.js' const pageList = await loadPages(undefined, ['en']) const pages = await loadPageMap(pageList) const redirects = await loadRedirects(pageList) describe('learning tracks', () => { - const allLearningTracks = getDeepDataByLanguage('learning-tracks', 'en') + // TODO: Once getDeepDataByLanguage is ported to TS + // a more appropriate API would be to use `getDeepDataByLanguage { @@ -33,6 +36,12 @@ describe('learning tracks', () => { expect(length, errorMessage).toEqual(size) } + type Trouble = { + uri: string + index: number + redirects: string | undefined + } + type TroubleTuple = [string, Trouble[]] const troubles = Object.entries(learningTracks) .map(([learningTrackKey, learningTrack]) => { return [ @@ -42,7 +51,7 @@ describe('learning tracks', () => { .filter(Boolean), ] }) - .filter(([, trouble]) => trouble.length > 0) + .filter(([, trouble]) => trouble.length > 0) as TroubleTuple[] let errorMessage = `In data/learning-tracks/${topLevel}.yml there are ${troubles.length} guides that are not correct.\n` let fixables = 0 diff --git a/src/types.ts b/src/types.ts index f0459077bf..684a5e717c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -159,6 +159,39 @@ export type Context = { productUserExamples?: ProductExample[] productGroups?: ProductGroup[] featuredLinks?: FeaturedLinksExpanded + currentLearningTrack?: LearningTrack | null +} +export type LearningTracks = { + [group: string]: { + [track: string]: { + title: string + description: string + versions?: FrontmatterVersions + guides: string[] + } + } +} +export type LearningTrack = { + trackName: string + trackProduct: string + trackTitle: string + numberOfGuides?: number + currentGuideIndex?: number + nextGuide?: { + href: string + title: string + } + prevGuide?: { + href: string + title: string + } +} + +export type TrackGuide = { + href: string + page: Page + title: string + intro: string } export type FeaturedLinkExpanded = { @@ -311,6 +344,7 @@ export type Page = { earlyAccessToc?: boolean autogenerated?: string featuredLinks?: FeaturedLinksExpanded + redirect_from?: string[] } type ChangeLog = {