diff --git a/build/blog.ts b/build/blog.ts index a79c6af15d28..b6c22ef8b9cf 100644 --- a/build/blog.ts +++ b/build/blog.ts @@ -281,7 +281,7 @@ export async function buildBlogPosts(options: { const url = `/${locale}/blog/${blogMeta.slug}/`; const renderUrl = `/${locale}/blog/${blogMeta.slug}`; - const renderDoc = { + const renderDoc: BlogPostDoc = { url: renderUrl, rawBody: body, metadata: { locale, ...blogMeta }, @@ -344,11 +344,11 @@ export async function buildBlogPosts(options: { } } -interface BlogPostDoc { +export interface BlogPostDoc { url: string; rawBody: string; metadata: BlogPostMetadata & { locale: string }; - isMarkdown: boolean; + isMarkdown: true; fileInfo: { path: string; }; @@ -368,7 +368,7 @@ export async function buildPost( let $ = null; const liveSamples: LiveSample[] = []; - [$] = await kumascript.render(document.url, {}, document as any); + [$] = await kumascript.render(document.url, {}, document); const liveSamplePages = await kumascript.buildLiveSamplePages( document.url, @@ -449,8 +449,12 @@ export async function buildBlogFeed(options: { verbose?: boolean }) { description: post.description, author: [ { - name: post.author?.name || "The MDN Team", - link: post.author?.link, + name: + (typeof post.author === "string" + ? post.author + : post.author?.name) || "The MDN Team", + link: + typeof post.author === "string" ? post.author : post.author?.link, }, ], date: new Date(post.date), diff --git a/build/curriculum.ts b/build/curriculum.ts index 1a2ed232e58f..1a7cc7c77e65 100644 --- a/build/curriculum.ts +++ b/build/curriculum.ts @@ -40,7 +40,7 @@ import { memoize, slugToFolder } from "../content/utils.js"; import { renderHTML } from "../ssr/dist/main.js"; import { CheerioAPI } from "cheerio"; -export const allFiles: () => string[] = memoize(async () => { +export const allFiles = memoize(async () => { const api = new fdir() .withFullPaths() .withErrors() @@ -49,7 +49,7 @@ export const allFiles: () => string[] = memoize(async () => { return (await api.withPromise()).sort(); }); -export const buildIndex: () => CurriculumMetaData[] = memoize(async () => { +export const buildIndex = memoize(async () => { const files = await allFiles(); const modules = await Promise.all( files.map( diff --git a/content/document.ts b/content/document.ts index dc4060c53865..6a7c4d126fa8 100644 --- a/content/document.ts +++ b/content/document.ts @@ -34,7 +34,7 @@ import { MEMOIZE_INVALIDATE, } from "./utils.js"; import * as Redirect from "./redirect.js"; -import { DocFrontmatter } from "../libs/types/document.js"; +import { DocFrontmatter, UnbuiltDocument } from "../libs/types/document.js"; export { urlToFolderPath, MEMOIZE_INVALIDATE } from "./utils.js"; @@ -185,173 +185,179 @@ export function getFolderPath(metadata, root: string | null = null) { ); } -export const read = memoize((folderOrFilePath: string, ...roots: string[]) => { - if (roots.length === 0) { - roots = ROOTS; - } - let filePath: string = null; - let folder: string = null; - let root: string = null; - let locale: string = null; - - if (fs.existsSync(folderOrFilePath)) { - filePath = folderOrFilePath; - - // It exists, but it is sane? - if ( - !( - filePath.endsWith(HTML_FILENAME) || filePath.endsWith(MARKDOWN_FILENAME) - ) - ) { - throw new Error(`'${filePath}' is not a HTML or Markdown file.`); +export const read = memoize( + (folderOrFilePath: string, ...roots: string[]): UnbuiltDocument => { + if (roots.length === 0) { + roots = ROOTS; } + let filePath: string = null; + let folder: string = null; + let root: string = null; + let locale: string = null; + + if (fs.existsSync(folderOrFilePath)) { + filePath = folderOrFilePath; + + // It exists, but it is sane? + if ( + !( + filePath.endsWith(HTML_FILENAME) || + filePath.endsWith(MARKDOWN_FILENAME) + ) + ) { + throw new Error(`'${filePath}' is not a HTML or Markdown file.`); + } - root = roots.find((possibleRoot) => filePath.startsWith(possibleRoot)); - if (root) { - folder = filePath - .replace(root + path.sep, "") - .replace(path.sep + HTML_FILENAME, "") - .replace(path.sep + MARKDOWN_FILENAME, ""); - locale = extractLocale(filePath.replace(root + path.sep, "")); + root = roots.find((possibleRoot) => filePath.startsWith(possibleRoot)); + if (root) { + folder = filePath + .replace(root + path.sep, "") + .replace(path.sep + HTML_FILENAME, "") + .replace(path.sep + MARKDOWN_FILENAME, ""); + locale = extractLocale(filePath.replace(root + path.sep, "")); + } else { + // The file exists but it doesn't appear to belong to any of our roots. + // That could happen if you pass in a file that is something completely + // different not a valid file anyway. + throw new Error( + `'${filePath}' does not appear to exist in any known content roots.` + ); + } } else { - // The file exists but it doesn't appear to belong to any of our roots. - // That could happen if you pass in a file that is something completely - // different not a valid file anyway. + folder = folderOrFilePath; + for (const possibleRoot of roots) { + const possibleMarkdownFilePath = path.join( + possibleRoot, + getMarkdownPath(folder) + ); + if (fs.existsSync(possibleMarkdownFilePath)) { + root = possibleRoot; + filePath = possibleMarkdownFilePath; + break; + } + const possibleHTMLFilePath = path.join( + possibleRoot, + getHTMLPath(folder) + ); + if (fs.existsSync(possibleHTMLFilePath)) { + root = possibleRoot; + filePath = possibleHTMLFilePath; + break; + } + } + if (!filePath) { + return; + } + locale = extractLocale(folder); + } + + if (folder.includes(" ")) { throw new Error( - `'${filePath}' does not appear to exist in any known content roots.` + `Folder contains whitespace which is not allowed (${util.inspect( + filePath + )})` ); } - } else { - folder = folderOrFilePath; - for (const possibleRoot of roots) { - const possibleMarkdownFilePath = path.join( - possibleRoot, - getMarkdownPath(folder) + if (folder.includes("\u200b")) { + throw new Error( + `Folder contains zero width whitespace which is not allowed (${filePath})` ); - if (fs.existsSync(possibleMarkdownFilePath)) { - root = possibleRoot; - filePath = possibleMarkdownFilePath; - break; - } - const possibleHTMLFilePath = path.join(possibleRoot, getHTMLPath(folder)); - if (fs.existsSync(possibleHTMLFilePath)) { - root = possibleRoot; - filePath = possibleHTMLFilePath; - break; - } } - if (!filePath) { - return; - } - locale = extractLocale(folder); - } - - if (folder.includes(" ")) { - throw new Error( - `Folder contains whitespace which is not allowed (${util.inspect( - filePath - )})` - ); - } - if (folder.includes("\u200b")) { - throw new Error( - `Folder contains zero width whitespace which is not allowed (${filePath})` + // Use Boolean() because otherwise, `isTranslated` might become `undefined` + // rather than an actual boolean value. + const isTranslated = Boolean( + CONTENT_TRANSLATED_ROOT && filePath.startsWith(CONTENT_TRANSLATED_ROOT) ); - } - // Use Boolean() because otherwise, `isTranslated` might become `undefined` - // rather than an actual boolean value. - const isTranslated = Boolean( - CONTENT_TRANSLATED_ROOT && filePath.startsWith(CONTENT_TRANSLATED_ROOT) - ); - const rawContent = fs.readFileSync(filePath, "utf-8"); - if (!rawContent) { - throw new Error(`${filePath} is an empty file`); - } + const rawContent = fs.readFileSync(filePath, "utf-8"); + if (!rawContent) { + throw new Error(`${filePath} is an empty file`); + } - // This is very useful in CI where every page gets built. If there's an - // accidentally unresolved git conflict, that's stuck in the content, - // bail extra early. - if ( - // If the document itself, is a page that explains and talks about git merge - // conflicts, i.e. a false positive, those angled brackets should be escaped - /^<<<<<<< HEAD\n/m.test(rawContent) && - /^=======\n/m.test(rawContent) && - /^>>>>>>>/m.test(rawContent) - ) { - throw new Error(`${filePath} contains git merge conflict markers`); - } + // This is very useful in CI where every page gets built. If there's an + // accidentally unresolved git conflict, that's stuck in the content, + // bail extra early. + if ( + // If the document itself, is a page that explains and talks about git merge + // conflicts, i.e. a false positive, those angled brackets should be escaped + /^<<<<<<< HEAD\n/m.test(rawContent) && + /^=======\n/m.test(rawContent) && + /^>>>>>>>/m.test(rawContent) + ) { + throw new Error(`${filePath} contains git merge conflict markers`); + } - const { - attributes: metadata, - body: rawBody, - bodyBegin: frontMatterOffset, - } = fm(rawContent); + const { + attributes: metadata, + body: rawBody, + bodyBegin: frontMatterOffset, + } = fm(rawContent); - const url = `/${locale}/docs/${metadata.slug}`; + const url = `/${locale}/docs/${metadata.slug}`; - const isActive = ACTIVE_LOCALES.has(locale.toLowerCase()); + const isActive = ACTIVE_LOCALES.has(locale.toLowerCase()); - // The last-modified is always coming from the git logs. Independent of - // which root it is. - const gitHistory = getGitHistories(root, locale).get( - path.relative(root, filePath) - ); - let modified = null; - let hash = null; - if (gitHistory) { - if ( - gitHistory.merged && - gitHistory.merged.modified && - gitHistory.merged.hash - ) { - modified = gitHistory.merged.modified; - hash = gitHistory.merged.hash; - } else { - modified = gitHistory.modified; - hash = gitHistory.hash; + // The last-modified is always coming from the git logs. Independent of + // which root it is. + const gitHistory = getGitHistories(root, locale).get( + path.relative(root, filePath) + ); + let modified = null; + let hash = null; + if (gitHistory) { + if ( + gitHistory.merged && + gitHistory.merged.modified && + gitHistory.merged.hash + ) { + modified = gitHistory.merged.modified; + hash = gitHistory.merged.hash; + } else { + modified = gitHistory.modified; + hash = gitHistory.hash; + } } + // Use the wiki histories for a list of legacy contributors. + const wikiHistory = getWikiHistories(root, locale).get(url); + if (!modified && wikiHistory && wikiHistory.modified) { + modified = wikiHistory.modified; + } + const fullMetadata = { + metadata: { + ...metadata, + // This is our chance to record and remember which keys were actually + // dug up from the front-matter. + // It matters because the keys in front-matter are arbitrary. + // Meaning, if a document contains `foo: bar` as a front-matter key/value + // we need to take note of that and make sure we preserve that if we + // save the metadata back (e.g. fixable flaws). + frontMatterKeys: Object.keys(metadata), + locale, + popularity: getPopularities().get(url) || 0.0, + modified, + hash, + contributors: wikiHistory ? wikiHistory.contributors : [], + }, + url, + }; + + return { + ...fullMetadata, + // ...{ rawContent }, + rawContent, // HTML or Markdown whole string with all the front-matter + rawBody, // HTML or Markdown string without the front-matter + isMarkdown: filePath.endsWith(MARKDOWN_FILENAME), + isTranslated, + isActive, + fileInfo: { + folder, + path: filePath, + frontMatterOffset, + root, + }, + }; } - // Use the wiki histories for a list of legacy contributors. - const wikiHistory = getWikiHistories(root, locale).get(url); - if (!modified && wikiHistory && wikiHistory.modified) { - modified = wikiHistory.modified; - } - const fullMetadata = { - metadata: { - ...metadata, - // This is our chance to record and remember which keys were actually - // dug up from the front-matter. - // It matters because the keys in front-matter are arbitrary. - // Meaning, if a document contains `foo: bar` as a front-matter key/value - // we need to take note of that and make sure we preserve that if we - // save the metadata back (e.g. fixable flaws). - frontMatterKeys: Object.keys(metadata), - locale, - popularity: getPopularities().get(url) || 0.0, - modified, - hash, - contributors: wikiHistory ? wikiHistory.contributors : [], - }, - url, - }; - - return { - ...fullMetadata, - // ...{ rawContent }, - rawContent, // HTML or Markdown whole string with all the front-matter - rawBody, // HTML or Markdown string without the front-matter - isMarkdown: filePath.endsWith(MARKDOWN_FILENAME), - isTranslated, - isActive, - fileInfo: { - folder, - path: filePath, - frontMatterOffset, - root, - }, - }; -}); +); export async function update(url: string, rawBody: string, metadata) { const folder = urlToFolderPath(url); diff --git a/content/translations.ts b/content/translations.ts index 3b06a4c59f23..d1fe335e6f71 100644 --- a/content/translations.ts +++ b/content/translations.ts @@ -1,6 +1,7 @@ import * as Document from "./document.js"; import { VALID_LOCALES } from "../libs/constants/index.js"; import LANGUAGES_RAW from "../libs/languages/index.js"; +import { Translation } from "../libs/types/document.js"; const LANGUAGES = new Map( Object.entries(LANGUAGES_RAW).map(([locale, data]) => { @@ -8,12 +9,6 @@ const LANGUAGES = new Map( }) ); -type Translation = { - locale: string; - title: string; - native: string; -}; - const TRANSLATIONS_OF = new Map>(); // gather and cache all translations of a document, diff --git a/content/utils.ts b/content/utils.ts index fc359f1c20a8..7e750e864f23 100644 --- a/content/utils.ts +++ b/content/utils.ts @@ -38,15 +38,15 @@ export function buildURL(locale: string, slug: string) { * Note: The parameter are turned into a cache key quite naively, so * different object key order would lead to new cache entries. */ -export function memoize( - fn: (...args: Args[]) => any -): (...args: (Args | typeof MEMOIZE_INVALIDATE)[]) => any { +export function memoize any>( + fn: F +): (...args: [...Parameters, typeof MEMOIZE_INVALIDATE?]) => ReturnType { if (process.env.NODE_ENV !== "production") { - return fn as (...args: (Args | typeof MEMOIZE_INVALIDATE)[]) => any; + return fn; } const cache = new LRUCache({ max: 2000 }); - return (...args: (Args | typeof MEMOIZE_INVALIDATE)[]) => { + return (...args) => { let invalidate = false; if (args.includes(MEMOIZE_INVALIDATE)) { args.splice(args.indexOf(MEMOIZE_INVALIDATE), 1); @@ -62,7 +62,7 @@ export function memoize( } } - const value = fn(...(args as Args[])); + const value = fn(...args); cache.set(key, value); if (value instanceof Promise) { value.catch(() => { diff --git a/kumascript/index.ts b/kumascript/index.ts index 78a59928820a..123bc77c6e06 100644 --- a/kumascript/index.ts +++ b/kumascript/index.ts @@ -93,7 +93,7 @@ export async function render( { ...metadata, url, - tags: metadata.tags || [], + tags: "tags" in metadata ? metadata.tags || [] : [], selective_mode, interactive_examples: { base_url: INTERACTIVE_EXAMPLES_BASE_URL, diff --git a/libs/types/document.ts b/libs/types/document.ts index f870d4ee032b..5500bf85cd08 100644 --- a/libs/types/document.ts +++ b/libs/types/document.ts @@ -228,3 +228,27 @@ export interface BuildData { path: string; }; } + +export interface UnbuiltDocument { + metadata: DocFrontmatter & { + frontMatterKeys: string[]; + locale: string; + popularity: number; + modified: any; + hash: any; + contributors: any; + }; + url: string; + rawContent: string; + rawBody: string; + isMarkdown: boolean; + isTranslated: boolean; + isActive: boolean; + fileInfo: { + folder: string; + path: string; + frontMatterOffset: number; + root: string; + }; + translations?: Translation[]; +}