next
Version:
The React Framework
472 lines (471 loc) • 23 kB
JavaScript
import path from 'path';
import fs from 'fs/promises';
import * as Log from '../../../build/output/log';
import setupDebug from 'next/dist/compiled/debug';
import { LRUCache } from '../lru-cache';
import loadCustomRoutes from '../../../lib/load-custom-routes';
import { modifyRouteRegex } from '../../../lib/redirect-status';
import { FileType, fileExists } from '../../../lib/file-exists';
import { recursiveReadDir } from '../../../lib/recursive-readdir';
import { isDynamicRoute } from '../../../shared/lib/router/utils';
import { escapeStringRegexp } from '../../../shared/lib/escape-regexp';
import { getPathMatch } from '../../../shared/lib/router/utils/path-match';
import { getRouteRegex } from '../../../shared/lib/router/utils/route-regex';
import { getRouteMatcher } from '../../../shared/lib/router/utils/route-matcher';
import { pathHasPrefix } from '../../../shared/lib/router/utils/path-has-prefix';
import { normalizeLocalePath } from '../../../shared/lib/i18n/normalize-locale-path';
import { removePathPrefix } from '../../../shared/lib/router/utils/remove-path-prefix';
import { getMiddlewareRouteMatcher } from '../../../shared/lib/router/utils/middleware-route-matcher';
import { APP_PATH_ROUTES_MANIFEST, BUILD_ID_FILE, FUNCTIONS_CONFIG_MANIFEST, MIDDLEWARE_MANIFEST, PAGES_MANIFEST, PRERENDER_MANIFEST, ROUTES_MANIFEST } from '../../../shared/lib/constants';
import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep';
import { normalizeMetadataRoute } from '../../../lib/metadata/get-metadata-route';
import { RSCPathnameNormalizer } from '../../normalizers/request/rsc';
import { PrefetchRSCPathnameNormalizer } from '../../normalizers/request/prefetch-rsc';
import { encodeURIPath } from '../../../shared/lib/encode-uri-path';
const debug = setupDebug('next:router-server:filesystem');
export const buildCustomRoute = (type, item, basePath, caseSensitive)=>{
const restrictedRedirectPaths = [
'/_next'
].map((p)=>basePath ? `${basePath}${p}` : p);
const match = getPathMatch(item.source, {
strict: true,
removeUnnamedParams: true,
regexModifier: !item.internal ? (regex)=>modifyRouteRegex(regex, type === 'redirect' ? restrictedRedirectPaths : undefined) : undefined,
sensitive: caseSensitive
});
return {
...item,
...type === 'rewrite' ? {
check: true
} : {},
match
};
};
export async function setupFsCheck(opts) {
const getItemsLru = !opts.dev ? new LRUCache(1024 * 1024, function length(value) {
if (!value) return 0;
return (value.fsPath || '').length + value.itemPath.length + value.type.length;
}) : undefined;
// routes that have _next/data endpoints (SSG/SSP)
const nextDataRoutes = new Set();
const publicFolderItems = new Set();
const nextStaticFolderItems = new Set();
const legacyStaticFolderItems = new Set();
const appFiles = new Set();
const pageFiles = new Set();
let dynamicRoutes = [];
let middlewareMatcher = ()=>false;
const distDir = path.join(opts.dir, opts.config.distDir);
const publicFolderPath = path.join(opts.dir, 'public');
const nextStaticFolderPath = path.join(distDir, 'static');
const legacyStaticFolderPath = path.join(opts.dir, 'static');
let customRoutes = {
redirects: [],
rewrites: {
beforeFiles: [],
afterFiles: [],
fallback: []
},
headers: []
};
let buildId = 'development';
let prerenderManifest;
if (!opts.dev) {
var _middlewareManifest_middleware_, _middlewareManifest_middleware;
const buildIdPath = path.join(opts.dir, opts.config.distDir, BUILD_ID_FILE);
try {
buildId = await fs.readFile(buildIdPath, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT') throw err;
throw Object.defineProperty(new Error(`Could not find a production build in the '${opts.config.distDir}' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id`), "__NEXT_ERROR_CODE", {
value: "E427",
enumerable: false,
configurable: true
});
}
try {
for (const file of (await recursiveReadDir(publicFolderPath))){
// Ensure filename is encoded and normalized.
publicFolderItems.add(encodeURIPath(normalizePathSep(file)));
}
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
try {
for (const file of (await recursiveReadDir(legacyStaticFolderPath))){
// Ensure filename is encoded and normalized.
legacyStaticFolderItems.add(encodeURIPath(normalizePathSep(file)));
}
Log.warn(`The static directory has been deprecated in favor of the public directory. https://nextjs.org/docs/messages/static-dir-deprecated`);
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
try {
for (const file of (await recursiveReadDir(nextStaticFolderPath))){
// Ensure filename is encoded and normalized.
nextStaticFolderItems.add(path.posix.join('/_next/static', encodeURIPath(normalizePathSep(file))));
}
} catch (err) {
if (opts.config.output !== 'standalone') throw err;
}
const routesManifestPath = path.join(distDir, ROUTES_MANIFEST);
const prerenderManifestPath = path.join(distDir, PRERENDER_MANIFEST);
const middlewareManifestPath = path.join(distDir, 'server', MIDDLEWARE_MANIFEST);
const functionsConfigManifestPath = path.join(distDir, 'server', FUNCTIONS_CONFIG_MANIFEST);
const pagesManifestPath = path.join(distDir, 'server', PAGES_MANIFEST);
const appRoutesManifestPath = path.join(distDir, APP_PATH_ROUTES_MANIFEST);
const routesManifest = JSON.parse(await fs.readFile(routesManifestPath, 'utf8'));
prerenderManifest = JSON.parse(await fs.readFile(prerenderManifestPath, 'utf8'));
const middlewareManifest = JSON.parse(await fs.readFile(middlewareManifestPath, 'utf8').catch(()=>'{}'));
const functionsConfigManifest = JSON.parse(await fs.readFile(functionsConfigManifestPath, 'utf8').catch(()=>'{}'));
const pagesManifest = JSON.parse(await fs.readFile(pagesManifestPath, 'utf8'));
const appRoutesManifest = JSON.parse(await fs.readFile(appRoutesManifestPath, 'utf8').catch(()=>'{}'));
for (const key of Object.keys(pagesManifest)){
// ensure the non-locale version is in the set
if (opts.config.i18n) {
pageFiles.add(normalizeLocalePath(key, opts.config.i18n.locales).pathname);
} else {
pageFiles.add(key);
}
}
for (const key of Object.keys(appRoutesManifest)){
appFiles.add(appRoutesManifest[key]);
}
const escapedBuildId = escapeStringRegexp(buildId);
for (const route of routesManifest.dataRoutes){
if (isDynamicRoute(route.page)) {
const routeRegex = getRouteRegex(route.page);
dynamicRoutes.push({
...route,
regex: routeRegex.re.toString(),
match: getRouteMatcher({
// TODO: fix this in the manifest itself, must also be fixed in
// upstream builder that relies on this
re: opts.config.i18n ? new RegExp(route.dataRouteRegex.replace(`/${escapedBuildId}/`, `/${escapedBuildId}/(?<nextLocale>[^/]+?)/`)) : new RegExp(route.dataRouteRegex),
groups: routeRegex.groups
})
});
}
nextDataRoutes.add(route.page);
}
for (const route of routesManifest.dynamicRoutes){
dynamicRoutes.push({
...route,
match: getRouteMatcher(getRouteRegex(route.page))
});
}
if ((_middlewareManifest_middleware = middlewareManifest.middleware) == null ? void 0 : (_middlewareManifest_middleware_ = _middlewareManifest_middleware['/']) == null ? void 0 : _middlewareManifest_middleware_.matchers) {
var _middlewareManifest_middleware_1, _middlewareManifest_middleware1;
middlewareMatcher = getMiddlewareRouteMatcher((_middlewareManifest_middleware1 = middlewareManifest.middleware) == null ? void 0 : (_middlewareManifest_middleware_1 = _middlewareManifest_middleware1['/']) == null ? void 0 : _middlewareManifest_middleware_1.matchers);
} else if (functionsConfigManifest == null ? void 0 : functionsConfigManifest.functions['/_middleware']) {
middlewareMatcher = getMiddlewareRouteMatcher(functionsConfigManifest.functions['/_middleware'].matchers ?? [
{
regexp: '.*',
originalSource: '/:path*'
}
]);
}
customRoutes = {
redirects: routesManifest.redirects,
rewrites: routesManifest.rewrites ? Array.isArray(routesManifest.rewrites) ? {
beforeFiles: [],
afterFiles: routesManifest.rewrites,
fallback: []
} : routesManifest.rewrites : {
beforeFiles: [],
afterFiles: [],
fallback: []
},
headers: routesManifest.headers
};
} else {
// dev handling
customRoutes = await loadCustomRoutes(opts.config);
prerenderManifest = {
version: 4,
routes: {},
dynamicRoutes: {},
notFoundRoutes: [],
preview: {
previewModeId: require('crypto').randomBytes(16).toString('hex'),
previewModeSigningKey: require('crypto').randomBytes(32).toString('hex'),
previewModeEncryptionKey: require('crypto').randomBytes(32).toString('hex')
}
};
}
const headers = customRoutes.headers.map((item)=>buildCustomRoute('header', item, opts.config.basePath, opts.config.experimental.caseSensitiveRoutes));
const redirects = customRoutes.redirects.map((item)=>buildCustomRoute('redirect', item, opts.config.basePath, opts.config.experimental.caseSensitiveRoutes));
const rewrites = {
beforeFiles: customRoutes.rewrites.beforeFiles.map((item)=>buildCustomRoute('before_files_rewrite', item)),
afterFiles: customRoutes.rewrites.afterFiles.map((item)=>buildCustomRoute('rewrite', item, opts.config.basePath, opts.config.experimental.caseSensitiveRoutes)),
fallback: customRoutes.rewrites.fallback.map((item)=>buildCustomRoute('rewrite', item, opts.config.basePath, opts.config.experimental.caseSensitiveRoutes))
};
const { i18n } = opts.config;
const handleLocale = (pathname, locales)=>{
let locale;
if (i18n) {
const i18nResult = normalizeLocalePath(pathname, locales || i18n.locales);
pathname = i18nResult.pathname;
locale = i18nResult.detectedLocale;
}
return {
locale,
pathname
};
};
debug('nextDataRoutes', nextDataRoutes);
debug('dynamicRoutes', dynamicRoutes);
debug('pageFiles', pageFiles);
debug('appFiles', appFiles);
let ensureFn;
const normalizers = {
// Because we can't know if the app directory is enabled or not at this
// stage, we assume that it is.
rsc: new RSCPathnameNormalizer(),
prefetchRSC: opts.config.experimental.ppr ? new PrefetchRSCPathnameNormalizer() : undefined
};
return {
headers,
rewrites,
redirects,
buildId,
handleLocale,
appFiles,
pageFiles,
dynamicRoutes,
nextDataRoutes,
exportPathMapRoutes: undefined,
devVirtualFsItems: new Set(),
prerenderManifest,
middlewareMatcher: middlewareMatcher,
ensureCallback (fn) {
ensureFn = fn;
},
async getItem (itemPath) {
const originalItemPath = itemPath;
const itemKey = originalItemPath;
const lruResult = getItemsLru == null ? void 0 : getItemsLru.get(itemKey);
if (lruResult) {
return lruResult;
}
const { basePath } = opts.config;
const hasBasePath = pathHasPrefix(itemPath, basePath);
// Return null if path doesn't start with basePath
if (basePath && !hasBasePath) {
return null;
}
// Remove basePath if it exists.
if (basePath && hasBasePath) {
itemPath = removePathPrefix(itemPath, basePath) || '/';
}
// Simulate minimal mode requests by normalizing RSC and postponed
// requests.
if (opts.minimalMode) {
var _normalizers_prefetchRSC;
if ((_normalizers_prefetchRSC = normalizers.prefetchRSC) == null ? void 0 : _normalizers_prefetchRSC.match(itemPath)) {
itemPath = normalizers.prefetchRSC.normalize(itemPath, true);
} else if (normalizers.rsc.match(itemPath)) {
itemPath = normalizers.rsc.normalize(itemPath, true);
}
}
if (itemPath !== '/' && itemPath.endsWith('/')) {
itemPath = itemPath.substring(0, itemPath.length - 1);
}
let decodedItemPath = itemPath;
try {
decodedItemPath = decodeURIComponent(itemPath);
} catch {}
if (itemPath === '/_next/image') {
return {
itemPath,
type: 'nextImage'
};
}
const itemsToCheck = [
[
this.devVirtualFsItems,
'devVirtualFsItem'
],
[
nextStaticFolderItems,
'nextStaticFolder'
],
[
legacyStaticFolderItems,
'legacyStaticFolder'
],
[
publicFolderItems,
'publicFolder'
],
[
appFiles,
'appFile'
],
[
pageFiles,
'pageFile'
]
];
for (let [items, type] of itemsToCheck){
let locale;
let curItemPath = itemPath;
let curDecodedItemPath = decodedItemPath;
const isDynamicOutput = type === 'pageFile' || type === 'appFile';
if (i18n) {
var _i18n_domains;
const localeResult = handleLocale(itemPath, // legacy behavior allows visiting static assets under
// default locale but no other locale
isDynamicOutput ? undefined : [
i18n == null ? void 0 : i18n.defaultLocale,
// default locales from domains need to be matched too
...((_i18n_domains = i18n.domains) == null ? void 0 : _i18n_domains.map((item)=>item.defaultLocale)) || []
]);
if (localeResult.pathname !== curItemPath) {
curItemPath = localeResult.pathname;
locale = localeResult.locale;
try {
curDecodedItemPath = decodeURIComponent(curItemPath);
} catch {}
}
}
if (type === 'legacyStaticFolder') {
if (!pathHasPrefix(curItemPath, '/static')) {
continue;
}
curItemPath = curItemPath.substring('/static'.length);
try {
curDecodedItemPath = decodeURIComponent(curItemPath);
} catch {}
}
if (type === 'nextStaticFolder' && !pathHasPrefix(curItemPath, '/_next/static')) {
continue;
}
const nextDataPrefix = `/_next/data/${buildId}/`;
if (type === 'pageFile' && curItemPath.startsWith(nextDataPrefix) && curItemPath.endsWith('.json')) {
items = nextDataRoutes;
// remove _next/data/<build-id> prefix
curItemPath = curItemPath.substring(nextDataPrefix.length - 1);
// remove .json postfix
curItemPath = curItemPath.substring(0, curItemPath.length - '.json'.length);
const curLocaleResult = handleLocale(curItemPath);
curItemPath = curLocaleResult.pathname === '/index' ? '/' : curLocaleResult.pathname;
locale = curLocaleResult.locale;
try {
curDecodedItemPath = decodeURIComponent(curItemPath);
} catch {}
}
let matchedItem = items.has(curItemPath);
// check decoded variant as well
if (!matchedItem && !opts.dev) {
matchedItem = items.has(curDecodedItemPath);
if (matchedItem) curItemPath = curDecodedItemPath;
else {
// x-ref: https://github.com/vercel/next.js/issues/54008
// There're cases that urls get decoded before requests, we should support both encoded and decoded ones.
// e.g. nginx could decode the proxy urls, the below ones should be treated as the same:
// decoded version: `/_next/static/chunks/pages/blog/[slug]-d4858831b91b69f6.js`
// encoded version: `/_next/static/chunks/pages/blog/%5Bslug%5D-d4858831b91b69f6.js`
try {
// encode the special characters in the path and retrieve again to determine if path exists.
const encodedCurItemPath = encodeURIPath(curItemPath);
matchedItem = items.has(encodedCurItemPath);
} catch {}
}
}
if (matchedItem || opts.dev) {
let fsPath;
let itemsRoot;
switch(type){
case 'nextStaticFolder':
{
itemsRoot = nextStaticFolderPath;
curItemPath = curItemPath.substring('/_next/static'.length);
break;
}
case 'legacyStaticFolder':
{
itemsRoot = legacyStaticFolderPath;
break;
}
case 'publicFolder':
{
itemsRoot = publicFolderPath;
break;
}
default:
{
break;
}
}
if (itemsRoot && curItemPath) {
fsPath = path.posix.join(itemsRoot, curItemPath);
}
// dynamically check fs in development so we don't
// have to wait on the watcher
if (!matchedItem && opts.dev) {
const isStaticAsset = [
'nextStaticFolder',
'publicFolder',
'legacyStaticFolder'
].includes(type);
if (isStaticAsset && itemsRoot) {
let found = fsPath && await fileExists(fsPath, FileType.File);
if (!found) {
try {
// In dev, we ensure encoded paths match
// decoded paths on the filesystem so check
// that variation as well
const tempItemPath = decodeURIComponent(curItemPath);
fsPath = path.posix.join(itemsRoot, tempItemPath);
found = await fileExists(fsPath, FileType.File);
} catch {}
if (!found) {
continue;
}
}
} else if (type === 'pageFile' || type === 'appFile') {
var _ensureFn;
const isAppFile = type === 'appFile';
if (ensureFn && await ((_ensureFn = ensureFn({
type,
itemPath: isAppFile ? normalizeMetadataRoute(curItemPath) : curItemPath
})) == null ? void 0 : _ensureFn.catch(()=>'ENSURE_FAILED')) === 'ENSURE_FAILED') {
continue;
}
} else {
continue;
}
}
// i18n locales aren't matched for app dir
if (type === 'appFile' && locale && locale !== (i18n == null ? void 0 : i18n.defaultLocale)) {
continue;
}
const itemResult = {
type,
fsPath,
locale,
itemsRoot,
itemPath: curItemPath
};
getItemsLru == null ? void 0 : getItemsLru.set(itemKey, itemResult);
return itemResult;
}
}
getItemsLru == null ? void 0 : getItemsLru.set(itemKey, null);
return null;
},
getDynamicRoutes () {
// this should include data routes
return this.dynamicRoutes;
},
getMiddlewareMatchers () {
return this.middlewareMatcher;
}
};
}
//# sourceMappingURL=filesystem.js.map