UNPKG

vike

Version:

The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.

497 lines (496 loc) 25.6 kB
export { renderPage }; export { renderPage_addAsyncHookwrapper }; import { renderPageAlreadyRouted } from './renderPage/renderPageAlreadyRouted.js'; import { createPageContextServerSide, createPageContextServerSideWithoutGlobalContext, } from './renderPage/createPageContextServerSide.js'; import { route } from '../../shared/route/index.js'; import { assert, hasProp, objectAssign, isUrl, parseUrl, onSetupRuntime, assertWarning, getGlobalObject, checkType, assertUsage, normalizeUrlPathname, removeBaseServer, modifyUrlPathname, prependBase, removeUrlOrigin, setUrlOrigin, isUri, getUrlPretty, augmentType, } from './utils.js'; import { assertNoInfiniteAbortLoop, getPageContextFromAllRewrites, isAbortError, logAbortErrorHandled, } from '../../shared/route/abort.js'; import { getGlobalContextServerInternal, initGlobalContext_renderPage, } from './globalContext.js'; import { handlePageContextRequestUrl } from './renderPage/handlePageContextRequestUrl.js'; import { createHttpResponse404, createHttpResponseRedirect, createHttpResponsePageContextJson, createHttpResponseError, createHttpResponseErrorWithoutGlobalContext, createHttpResponseBaseIsMissing, } from './renderPage/createHttpResponse.js'; import { logRuntimeError, logRuntimeInfo } from './loggerRuntime.js'; import { isNewError } from './renderPage/isNewError.js'; import { assertArguments } from './renderPage/assertArguments.js'; import { log404 } from './renderPage/log404/index.js'; import pc from '@brillout/picocolors'; import { getPageContextClientSerializedAbort, getPageContextClientSerialized } from './html/serializeContext.js'; import { getErrorPageId } from '../../shared/error-page.js'; import { handleErrorWithoutErrorPage } from './renderPage/handleErrorWithoutErrorPage.js'; import { loadPageConfigsLazyServerSideAndExecHook } from './renderPage/loadPageConfigsLazyServerSide.js'; import { resolveRedirects } from './renderPage/resolveRedirects.js'; import { getVikeConfigError } from '../shared/getVikeConfigError.js'; const globalObject = getGlobalObject('runtime/renderPage.ts', { httpRequestsCount: 0 }); // `renderPage()` calls `renderPageNominal()` while ensuring that errors are `console.error(err)` instead of `throw err`, so that Vike never triggers a server shut down. (Throwing an error in an Express.js middleware shuts down the whole Express.js server.) async function renderPage(pageContextInit) { assertArguments(...arguments); assert(hasProp(pageContextInit, 'urlOriginal', 'string')); // assertUsage() already implemented at assertArguments() assertIsUrl(pageContextInit.urlOriginal); onSetupRuntime(); const pageContextSkipRequest = getPageContextSkipRequest(pageContextInit); if (pageContextSkipRequest) return pageContextSkipRequest; const httpRequestId = getRequestId(); const urlOriginalPretty = getUrlPretty(pageContextInit.urlOriginal); logHttpRequest(urlOriginalPretty, httpRequestId); const { pageContextReturn } = await asyncHookWrapper(httpRequestId, () => renderPagePrepare(pageContextInit, httpRequestId)); logHttpResponse(urlOriginalPretty, httpRequestId, pageContextReturn); checkType(pageContextReturn); assert(pageContextReturn.httpResponse); return pageContextReturn; } // Fallback wrapper if node:async_hooks isn't available let asyncHookWrapper = async (_httpRequestId, ret) => ({ pageContextReturn: await ret(), }); // Add node:async_hooks wrapper function renderPage_addAsyncHookwrapper(wrapper) { asyncHookWrapper = wrapper; } async function renderPagePrepare(pageContextInit, httpRequestId) { // Invalid config { const vikeConfigError = getVikeConfigError(); if (vikeConfigError) { return getPageContextInvalidVikeConfig(vikeConfigError.err, pageContextInit, httpRequestId); } } // Prepare context try { await initGlobalContext_renderPage(); } catch (err) { // Errors are expected since assertUsage() is used in initGlobalContext_renderPage() such as: // ```bash // Re-build your app (you're using 1.2.3 but your app was built with 1.2.2) // ``` // initGlobalContext_renderPage() doesn't call any user hook => err isn't thrown from user code. assert(!isAbortError(err)); logRuntimeError(err, httpRequestId); const pageContextWithError = getPageContextHttpResponseErrorWithoutGlobalContext(err, pageContextInit); return pageContextWithError; } { const vikeConfigError = getVikeConfigError(); if (vikeConfigError) { return getPageContextInvalidVikeConfig(vikeConfigError.err, pageContextInit, httpRequestId); } else { // `globalContext` now contains the entire Vike config and getVikeConfig() isn't called anymore for this request. } } const { globalContext } = await getGlobalContextServerInternal(); const pageContextBegin = await getPageContextBegin(pageContextInit, globalContext, httpRequestId); // Check Base URL { const pageContextHttpResponse = await checkBaseUrl(pageContextBegin, globalContext); if (pageContextHttpResponse) return pageContextHttpResponse; } // Normalize URL { const pageContextHttpResponse = await normalizeUrl(pageContextBegin, globalContext, httpRequestId); if (pageContextHttpResponse) return pageContextHttpResponse; } // Permanent redirects (HTTP status code `301`) { const pageContextHttpResponse = await getPermanentRedirect(pageContextBegin, globalContext, httpRequestId); if (pageContextHttpResponse) return pageContextHttpResponse; } return await renderPageAlreadyPrepared(pageContextBegin, globalContext, httpRequestId, []); } async function renderPageAlreadyPrepared(pageContextBegin, globalContext, httpRequestId, pageContextsFromRewrite) { const pageContextNominalPageBegin = forkPageContext(pageContextBegin); assertNoInfiniteAbortLoop(pageContextsFromRewrite.length, // There doesn't seem to be a way to count the number of HTTP redirects (vike don't have access to the HTTP request headers/cookies) // https://stackoverflow.com/questions/9683007/detect-infinite-http-redirect-loop-on-server-side 0); let pageContextNominalPageSuccess; const pageContextFromAllRewrites = getPageContextFromAllRewrites(pageContextsFromRewrite); // This is where pageContext._urlRewrite is set assert(pageContextFromAllRewrites._urlRewrite === null || typeof pageContextFromAllRewrites._urlRewrite === 'string'); objectAssign(pageContextNominalPageBegin, pageContextFromAllRewrites); let errNominalPage; { try { pageContextNominalPageSuccess = await renderPageNominal(pageContextNominalPageBegin); } catch (err) { errNominalPage = err; assert(errNominalPage); logRuntimeError(errNominalPage, httpRequestId); } if (!errNominalPage) { assert(pageContextNominalPageSuccess === pageContextNominalPageBegin); } } // Log upon 404 if (pageContextNominalPageSuccess && 'is404' in pageContextNominalPageSuccess && pageContextNominalPageSuccess.is404 === true) { await log404(pageContextNominalPageSuccess); } if (errNominalPage === undefined) { assert(pageContextNominalPageSuccess); return pageContextNominalPageSuccess; } else { assert(errNominalPage); assert(pageContextNominalPageSuccess === undefined); return await renderPageOnError(errNominalPage, pageContextBegin, pageContextNominalPageBegin, globalContext, httpRequestId, pageContextsFromRewrite); } } // When the normal page threw an error // - Can be a URL rewrite upon `throw render('/some-url')` // - Can be rendering the error page // - Can be rendering Vike's generic error page (if no error page is defined, or if the error page throws an error) async function renderPageOnError(errNominalPage, pageContextBegin, pageContextNominalPageBegin, globalContext, httpRequestId, pageContextsFromRewrite) { assert(pageContextNominalPageBegin); assert(hasProp(pageContextNominalPageBegin, 'urlOriginal', 'string')); const pageContextErrorPageInit = await getPageContextErrorPageInit(pageContextBegin, errNominalPage, pageContextNominalPageBegin); // Handle `throw redirect()` and `throw render()` while rendering nominal page if (isAbortError(errNominalPage)) { const handled = await handleAbortError(errNominalPage, pageContextsFromRewrite, pageContextBegin, pageContextNominalPageBegin, httpRequestId, pageContextErrorPageInit, globalContext); if (handled.pageContextReturn) { // - throw redirect() // - throw render(url) // - throw render(abortStatusCode) if .pageContext.json request return handled.pageContextReturn; } else { // - throw render(abortStatusCode) if not .pageContext.json request } Object.assign(pageContextErrorPageInit, handled.pageContextAbort); } { const errorPageId = getErrorPageId(globalContext._pageFilesAll, globalContext._pageConfigs); if (!errorPageId) { objectAssign(pageContextErrorPageInit, { pageId: null }); return handleErrorWithoutErrorPage(pageContextErrorPageInit); } else { objectAssign(pageContextErrorPageInit, { pageId: errorPageId }); } } let pageContextErrorPage; try { pageContextErrorPage = await renderPageAlreadyRouted(pageContextErrorPageInit); } catch (errErrorPage) { // Handle `throw redirect()` and `throw render()` while rendering error page if (isAbortError(errErrorPage)) { const handled = await handleAbortError(errErrorPage, pageContextsFromRewrite, pageContextBegin, pageContextNominalPageBegin, httpRequestId, pageContextErrorPageInit, globalContext); // throw render(abortStatusCode) if (!handled.pageContextReturn) { const pageContextAbort = errErrorPage._pageContextAbort; assertWarning(false, `Failed to render error page because ${pc.cyan(pageContextAbort._abortCall)} was called: make sure ${pc.cyan(pageContextAbort._abortCaller)} doesn't occur while the error page is being rendered.`, { onlyOnce: false }); const pageContextHttpWithError = getPageContextHttpResponseError(errNominalPage, pageContextBegin); return pageContextHttpWithError; } // `throw redirect()` / `throw render(url)` return handled.pageContextReturn; } if (isNewError(errErrorPage, errNominalPage)) { logRuntimeError(errErrorPage, httpRequestId); } const pageContextWithError = getPageContextHttpResponseError(errNominalPage, pageContextBegin); return pageContextWithError; } return pageContextErrorPage; } function logHttpRequest(urlOriginal, httpRequestId) { logRuntimeInfo?.(getRequestInfoMessage(urlOriginal), httpRequestId, 'info'); } function getRequestInfoMessage(urlOriginal) { return `HTTP request: ${prettyUrl(urlOriginal)}`; } function logHttpResponse(urlOriginalPretty, httpRequestId, pageContextReturn) { const statusCode = pageContextReturn.httpResponse?.statusCode ?? null; let msg; let isNominal; { const { errorWhileRendering } = pageContextReturn; const isSkipped = statusCode === null && !errorWhileRendering; if (isSkipped) { // - URL doesn't include Base URL // - Can we abort earlier so that `logHttpResponse()` and `logHttpRequest()` aren't even called? // - Error loading a Vike config file // - We should show `HTTP response ${urlOriginalPretty} ERR` instead. // - Maybe we can/should make the error available at pageContext.errorWhileRendering assert(errorWhileRendering === null || errorWhileRendering === undefined); msg = `HTTP response ${prettyUrl(urlOriginalPretty)} ${pc.dim('null')}`; // Erroneous value (it should sometimes be `false`) but it's fine as it doesn't seem to have much of an impact. isNominal = true; } else { const isSuccess = statusCode !== null && statusCode >= 200 && statusCode <= 399; isNominal = isSuccess || statusCode === 404; const color = (s) => pc.bold(isSuccess ? pc.green(String(s)) : pc.red(String(s))); const isRedirect = statusCode && 300 <= statusCode && statusCode <= 399; const type = isRedirect ? 'redirect' : 'response'; if (isRedirect) { assert(pageContextReturn.httpResponse); const headerRedirect = pageContextReturn.httpResponse.headers .slice() .reverse() .find((header) => header[0] === 'Location'); assert(headerRedirect); const urlRedirect = headerRedirect[1]; urlOriginalPretty = urlRedirect; } msg = `HTTP ${type} ${prettyUrl(urlOriginalPretty)} ${color(statusCode ?? 'ERR')}`; } } logRuntimeInfo?.(msg, httpRequestId, isNominal ? 'info' : 'error'); } function prettyUrl(url) { try { url = decodeURI(url); } catch { // https://github.com/vikejs/vike/pull/2367#issuecomment-2800967564 } return pc.bold(url); } function getPageContextHttpResponseError(err, pageContextBegin) { const pageContextWithError = forkPageContext(pageContextBegin); const httpResponse = createHttpResponseError(pageContextBegin); objectAssign(pageContextWithError, { httpResponse, errorWhileRendering: err, }); return pageContextWithError; } function getPageContextHttpResponseErrorWithoutGlobalContext(err, pageContextInit) { const pageContextWithError = createPageContextServerSideWithoutGlobalContext(pageContextInit); const httpResponse = createHttpResponseErrorWithoutGlobalContext(); objectAssign(pageContextWithError, { httpResponse, errorWhileRendering: err, }); return pageContextWithError; } async function renderPageNominal(pageContext) { objectAssign(pageContext, { errorWhileRendering: null }); // Route { const pageContextFromRoute = await route(pageContext); objectAssign(pageContext, pageContextFromRoute); objectAssign(pageContext, { is404: pageContext.pageId ? null : true }); if (pageContext.pageId === null) { const errorPageId = getErrorPageId(pageContext._globalContext._pageFilesAll, pageContext._globalContext._pageConfigs); if (!errorPageId) { assert(hasProp(pageContext, 'pageId', 'null')); return handleErrorWithoutErrorPage(pageContext); } objectAssign(pageContext, { pageId: errorPageId }); } } assert(hasProp(pageContext, 'pageId', 'string')); assert(pageContext.errorWhileRendering === null); // Render const pageContextAfterRender = await renderPageAlreadyRouted(pageContext); assert(pageContext === pageContextAfterRender); return pageContextAfterRender; } async function getPageContextErrorPageInit(pageContextBegin, errNominalPage, pageContextNominalPagePartial) { const pageContext = forkPageContext(pageContextBegin); assert(errNominalPage); objectAssign(pageContext, { is404: false, errorWhileRendering: errNominalPage, routeParams: {}, }); objectAssign(pageContext, { _debugRouteMatches: pageContextNominalPagePartial._debugRouteMatches || 'ROUTING_ERROR', }); assert(pageContext.errorWhileRendering); return pageContext; } async function getPageContextBegin(pageContextInit, globalContext, httpRequestId) { const { isClientSideNavigation, _urlHandler } = handlePageContextUrl(pageContextInit.urlOriginal); const pageContextBegin = await createPageContextServerSide(pageContextInit, globalContext, { isPrerendering: false, ssr: { urlHandler: _urlHandler, isClientSideNavigation, }, }); objectAssign(pageContextBegin, { _httpRequestId: httpRequestId }); return pageContextBegin; } function handlePageContextUrl(urlOriginal) { const { isPageContextRequest } = handlePageContextRequestUrl(urlOriginal); return { isClientSideNavigation: isPageContextRequest, _urlHandler: (url) => handlePageContextRequestUrl(url).urlWithoutPageContextRequestSuffix, }; } function getRequestId() { const httpRequestId = ++globalObject.httpRequestsCount; assert(httpRequestId >= 1); return httpRequestId; } function assertIsUrl(urlOriginal) { assertUsage(isUrl(urlOriginal), `${pc.code('renderPage(pageContextInit)')} (https://vike.dev/renderPage) called with ${pc.code(`pageContextInit.urlOriginal===${JSON.stringify(urlOriginal)}`)} which isn't a valid URL.`); } function assertIsNotViteRequest(urlPathname, urlOriginal) { const isViteRequest = urlPathname.startsWith('/@vite/client') || urlPathname.startsWith('/@fs/') || urlPathname.startsWith('/__vite_ping'); if (!isViteRequest) return; assertUsage(false, `${pc.code('renderPage(pageContextInit)')} called with ${pc.code(`pageContextInit.urlOriginal===${JSON.stringify(urlOriginal)}`)} which is unexpected because the URL ${pc.bold(urlOriginal)} should have already been handled by the development middleware: make sure the ${pc.cyan('createDevMiddleware()')} middleware is executed *before* the ${pc.cyan('renderPage()')} middleware, see ${pc.underline('https://vike.dev/renderPage')}`); } async function normalizeUrl(pageContextBegin, globalContext, httpRequestId) { const pageContext = forkPageContext(pageContextBegin); const { trailingSlash, disableUrlNormalization } = globalContext.config; if (disableUrlNormalization) return null; const { urlOriginal } = pageContext; const { isPageContextRequest } = handlePageContextRequestUrl(urlOriginal); if (isPageContextRequest) return null; const urlNormalized = normalizeUrlPathname(urlOriginal, trailingSlash ?? false, globalContext.baseServer); if (!urlNormalized) return null; logRuntimeInfo?.(`URL normalized from ${pc.cyan(urlOriginal)} to ${pc.cyan(urlNormalized)} (https://vike.dev/url-normalization)`, httpRequestId, 'info'); const httpResponse = createHttpResponseRedirect({ url: urlNormalized, statusCode: 301 }, pageContext); objectAssign(pageContext, { httpResponse }); return pageContext; } async function getPermanentRedirect(pageContextBegin, globalContext, httpRequestId) { const pageContext = forkPageContext(pageContextBegin); const urlWithoutBase = removeBaseServer(pageContext.urlOriginal, globalContext.baseServer); let origin = null; let urlTargetExternal = null; let urlTarget = modifyUrlPathname(urlWithoutBase, (urlPathname) => { const urlTarget = resolveRedirects(globalContext.config.redirects ?? [], urlPathname); if (urlTarget === null) return null; if (!isUrl(urlTarget)) { // E.g. `urlTarget === 'mailto:some@example.com'` assert(isUri(urlTarget)); urlTargetExternal = urlTarget; return null; } const { urlModified, origin: origin_ } = removeUrlOrigin(urlTarget); origin = origin_; return urlModified; }); if (urlTargetExternal) { urlTarget = urlTargetExternal; } else { let originChanged = false; if (origin) { const urlModified = setUrlOrigin(urlTarget, origin); if (urlModified !== false) { originChanged = true; urlTarget = urlModified; } } if (normalize(urlTarget) === normalize(urlWithoutBase)) return null; if (!originChanged) urlTarget = prependBase(urlTarget, globalContext.baseServer); assert(urlTarget !== pageContext.urlOriginal); } logRuntimeInfo?.(`Permanent redirection defined by config.redirects (https://vike.dev/redirects)`, httpRequestId, 'info'); const httpResponse = createHttpResponseRedirect({ url: urlTarget, statusCode: 301 }, pageContext); objectAssign(pageContext, { httpResponse }); return pageContext; } function normalize(url) { return url || '/'; } async function handleAbortError(errAbort, pageContextsFromRewrite, pageContextBegin, // handleAbortError() creates a new pageContext object and we don't merge pageContextNominalPageBegin to it: we only use some pageContextNominalPageBegin information. pageContextNominalPageBegin, httpRequestId, pageContextErrorPageInit, globalContext) { logAbortErrorHandled(errAbort, globalContext._isProduction, pageContextNominalPageBegin); const pageContextAbort = errAbort._pageContextAbort; let pageContextSerialized; if (pageContextNominalPageBegin.isClientSideNavigation) { if (pageContextAbort.abortStatusCode) { const errorPageId = getErrorPageId(globalContext._pageFilesAll, globalContext._pageConfigs); const abortCall = pageContextAbort._abortCall; assert(abortCall); assertUsage(errorPageId, `You called ${pc.cyan(abortCall)} but you didn't define an error page, make sure to define one https://vike.dev/error-page`); const pageContext = forkPageContext(pageContextBegin); objectAssign(pageContext, { pageId: errorPageId }); objectAssign(pageContext, pageContextAbort); objectAssign(pageContext, pageContextErrorPageInit, true); augmentType(pageContext, await loadPageConfigsLazyServerSideAndExecHook(pageContext)); // We include pageContextInit: we don't only serialize pageContextAbort because the error page may need to access pageContextInit pageContextSerialized = getPageContextClientSerialized(pageContext); } else { pageContextSerialized = getPageContextClientSerializedAbort(pageContextAbort); } const httpResponse = await createHttpResponsePageContextJson(pageContextSerialized); const pageContextReturn = { httpResponse }; return { pageContextReturn }; } if (pageContextAbort._urlRewrite) { const pageContextReturn = await renderPageAlreadyPrepared(pageContextBegin, globalContext, httpRequestId, [ ...pageContextsFromRewrite, pageContextAbort, ]); Object.assign(pageContextReturn, pageContextAbort); return { pageContextReturn }; } if (pageContextAbort._urlRedirect) { const pageContextReturn = forkPageContext(pageContextBegin); objectAssign(pageContextReturn, pageContextAbort); const httpResponse = createHttpResponseRedirect(pageContextAbort._urlRedirect, pageContextBegin); objectAssign(pageContextReturn, { httpResponse }); return { pageContextReturn }; } assert(pageContextAbort.abortStatusCode); return { pageContextAbort }; } async function checkBaseUrl(pageContextBegin, globalContext) { const pageContext = forkPageContext(pageContextBegin); const { baseServer } = globalContext; const { urlOriginal } = pageContext; const { isBaseMissing } = parseUrl(urlOriginal, baseServer); if (!isBaseMissing) return; const httpResponse = createHttpResponseBaseIsMissing(urlOriginal, baseServer); objectAssign(pageContext, { httpResponse, isBaseMissing: true, }); checkType(pageContext); return pageContext; } function getPageContextSkipRequest(pageContextInit) { const urlPathnameWithBase = parseUrl(pageContextInit.urlOriginal, '/').pathname; assertIsNotViteRequest(urlPathnameWithBase, pageContextInit.urlOriginal); let errMsg404; if (urlPathnameWithBase.endsWith('/favicon.ico')) { errMsg404 = 'No favicon.ico found'; } if (urlPathnameWithBase.endsWith('.well-known/appspecific/com.chrome.devtools.json')) { // https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md // https://www.reddit.com/r/node/comments/1kcr0wh/odd_request_coming_into_my_localhost_server_from/ errMsg404 = 'Not supported'; } if (!errMsg404) return; const pageContext = createPageContextServerSideWithoutGlobalContext(pageContextInit); const httpResponse = createHttpResponse404(errMsg404); objectAssign(pageContext, { httpResponse }); checkType(pageContext); return pageContext; } function getPageContextInvalidVikeConfig(err, pageContextInit, httpRequestId) { logRuntimeInfo?.(pc.bold(pc.red('Error loading Vike config — see error above')), httpRequestId, 'error'); const pageContextWithError = getPageContextHttpResponseErrorWithoutGlobalContext(err, pageContextInit); return pageContextWithError; } // Create pageContext forks to avoid leaks: upon an error (bug or abort) a brand new pageContext object is created, in order to avoid previous pageContext modifications that are now obsolete to leak to the new pageContext object. function forkPageContext(pageContextBegin) { const pageContext = {}; objectAssign(pageContext, pageContextBegin, true); return pageContext; }