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