The Node.js® Website
at main 8.3 kB view raw
1'use strict'; 2 3import { existsSync } from 'node:fs'; 4import { readFile } from 'node:fs/promises'; 5import { join, normalize, sep } from 'node:path'; 6 7import matter from 'gray-matter'; 8import { cache } from 'react'; 9import { VFile } from 'vfile'; 10 11import { BASE_URL, BASE_PATH, IS_DEVELOPMENT } from './next.constants.mjs'; 12import { 13 IGNORED_ROUTES, 14 DYNAMIC_ROUTES, 15 PAGE_METADATA, 16} from './next.dynamic.constants.mjs'; 17import { getMarkdownFiles } from './next.helpers.mjs'; 18import { siteConfig } from './next.json.mjs'; 19import { availableLocaleCodes, defaultLocale } from './next.locales.mjs'; 20import { compileMDX } from './next.mdx.compiler.mjs'; 21 22// This is the combination of the Application Base URL and Base PATH 23const baseUrlAndPath = `${BASE_URL}${BASE_PATH}`; 24 25// This is a small utility that allows us to quickly separate locale from the remaining pathname 26const getPathname = (path = []) => path.join('/'); 27 28// This maps a pathname into an actual route object that can be used 29// we use a platform-specific separator to split the pathname 30// since we're using filepaths here and not URL paths 31const mapPathToRoute = (locale = defaultLocale.code, path = '') => ({ 32 locale, 33 path: path.split(sep), 34}); 35 36// Provides an in-memory Map that lasts the whole build process 37// and disabled when on development mode (stubbed) 38const createCachedMarkdownCache = () => { 39 if (IS_DEVELOPMENT) { 40 return { 41 has: () => false, 42 set: () => {}, 43 get: () => null, 44 }; 45 } 46 47 return new Map(); 48}; 49 50const getDynamicRouter = async () => { 51 // Creates a Cache System that is disabled during development mode 52 const cachedMarkdownFiles = createCachedMarkdownCache(); 53 54 // Keeps the map of pathnames to filenames 55 const pathnameToFilename = new Map(); 56 57 const websitePages = await getMarkdownFiles( 58 process.cwd(), 59 `pages/${defaultLocale.code}` 60 ); 61 62 websitePages.forEach(filename => { 63 // This Regular Expression is used to remove the `index.md(x)` suffix 64 // of a name and to remove the `.md(x)` extensions of a filename. 65 let pathname = filename.replace(/((\/)?(index))?\.mdx?$/i, ''); 66 67 if (pathname.length > 1 && pathname.endsWith(sep)) { 68 pathname = pathname.substring(0, pathname.length - 1); 69 } 70 71 pathname = normalize(pathname).replace('.', ''); 72 73 // We map the pathname to the filename to be able to quickly 74 // resolve the filename for a given pathname 75 pathnameToFilename.set(pathname, filename); 76 }); 77 78 /** 79 * This method returns a list of all routes that exist for a given locale 80 * 81 * @param {string} locale 82 * @returns {Array<string>} 83 */ 84 const getRoutesByLanguage = async (locale = defaultLocale.code) => { 85 const shouldIgnoreStaticRoute = pathname => 86 IGNORED_ROUTES.every(e => !e({ pathname, locale })); 87 88 return [...pathnameToFilename.keys()] 89 .filter(shouldIgnoreStaticRoute) 90 .concat([...DYNAMIC_ROUTES.keys()]); 91 }; 92 93 /** 94 * This method attempts to retrieve either a localized Markdown file 95 * or the English version of the Markdown file if no localized version exists 96 * and then returns the contents of the file and the name of the file (not the path) 97 * 98 * @param {string} locale 99 * @param {string} pathname 100 * @returns {Promise<{ source: string; filename: string }>} 101 */ 102 const _getMarkdownFile = async (locale = '', pathname = '') => { 103 const normalizedPathname = normalize(pathname).replace('.', ''); 104 105 // This verifies if the given pathname actually exists on our Map 106 // meaning that the route exists on the website and can be rendered 107 if (pathnameToFilename.has(normalizedPathname)) { 108 const filename = pathnameToFilename.get(normalizedPathname); 109 110 let filePath = join(process.cwd(), 'pages'); 111 112 // We verify if our Markdown cache already has a cache entry for a localized 113 // version of this file, because if not, it means that either 114 // we did not cache this file yet or there is no localized version of this file 115 if (cachedMarkdownFiles.has(`${locale}${normalizedPathname}`)) { 116 const fileContent = cachedMarkdownFiles.get( 117 `${locale}${normalizedPathname}` 118 ); 119 120 return { source: fileContent, filename }; 121 } 122 123 // No cache hit exists, so we check if the localized file actually 124 // exists within our file system and if it does we set it on the cache 125 // and return the current fetched result; If the file does not exist 126 // we fallback to the English source 127 if (existsSync(join(filePath, locale, filename))) { 128 filePath = join(filePath, locale, filename); 129 130 const fileContent = await readFile(filePath, 'utf8'); 131 132 cachedMarkdownFiles.set(`${locale}${normalizedPathname}`, fileContent); 133 134 return { source: fileContent, filename }; 135 } 136 137 // We then attempt to retrieve the source version of the file as there is no localised version 138 // of the file and we set it on the cache to prevent future checks of the same locale for this file 139 const { source: fileContent } = await _getMarkdownFile( 140 defaultLocale.code, 141 pathname 142 ); 143 144 // We set the source file on the localized cache to prevent future checks 145 // of the same locale for this file and improve read performance 146 cachedMarkdownFiles.set(`${locale}${normalizedPathname}`, fileContent); 147 148 return { source: fileContent, filename }; 149 } 150 151 return { filename: '', source: '' }; 152 }; 153 154 // Creates a Cached Version of the Markdown File Resolver 155 const getMarkdownFile = cache(async (locale, pathname) => { 156 return await _getMarkdownFile(locale, pathname); 157 }); 158 159 /** 160 * This method runs the MDX compiler on the server-side and returns the 161 * parsed JSX ready to be rendered on a page as a React Component 162 * 163 * @param {string} source 164 * @param {string} filename 165 */ 166 const _getMDXContent = async (source = '', filename = '') => { 167 // We create a VFile (Virtual File) to be able to access some contextual 168 // data post serialization (compilation) of the source Markdown into MDX 169 const sourceAsVirtualFile = new VFile(source); 170 171 // Gets the file extension of the file, to determine which parser and plugins to use 172 const fileExtension = filename.endsWith('.mdx') ? 'mdx' : 'md'; 173 174 // This compiles our MDX source (VFile) into a final MDX-parsed VFile 175 // that then is passed as a string to the MDXProvider which will run the MDX Code 176 return compileMDX(sourceAsVirtualFile, fileExtension); 177 }; 178 179 // Creates a Cached Version of the MDX Compiler 180 const getMDXContent = cache(async (source, filename) => { 181 return await _getMDXContent(source, filename); 182 }); 183 184 /** 185 * This method generates the Next.js App Router Metadata 186 * that can be used for each page to provide metadata 187 * 188 * @param {string} locale 189 * @param {string} path 190 * @returns {import('next').Metadata} 191 */ 192 const _getPageMetadata = async (locale = defaultLocale.code, path = '') => { 193 const pageMetadata = { ...PAGE_METADATA }; 194 195 const { source = '' } = await getMarkdownFile(locale, path); 196 197 const { data } = matter(source); 198 199 pageMetadata.title = data.title 200 ? `${siteConfig.title}${data.title}` 201 : siteConfig.title; 202 203 pageMetadata.twitter.title = pageMetadata.title; 204 205 const getUrlForPathname = (l, p) => 206 `${baseUrlAndPath}/${l}${p ? `/${p}` : ''}`; 207 208 pageMetadata.alternates.canonical = getUrlForPathname(locale, path); 209 210 pageMetadata.alternates.languages['x-default'] = getUrlForPathname( 211 defaultLocale.code, 212 path 213 ); 214 215 availableLocaleCodes.forEach(currentLocale => { 216 pageMetadata.alternates.languages[currentLocale] = getUrlForPathname( 217 currentLocale, 218 path 219 ); 220 pageMetadata.openGraph.images = [ 221 `${currentLocale}/next-data/og?title=${data.title}&type=${data.category ?? 'announcement'}`, 222 ]; 223 }); 224 225 return pageMetadata; 226 }; 227 228 // Creates a Cached Version of the Page Metadata Context 229 const getPageMetadata = cache(async (locale, path) => { 230 return await _getPageMetadata(locale, path); 231 }); 232 233 return { 234 mapPathToRoute, 235 getPathname, 236 getRoutesByLanguage, 237 getMDXContent, 238 getMarkdownFile, 239 getPageMetadata, 240 }; 241}; 242 243export const dynamicRouter = await getDynamicRouter();