vike
Version:
The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.
683 lines (682 loc) • 29.9 kB
JavaScript
import '../assertEnvClient.js';
export { renderPageClient };
export { getRenderCount };
export { disableClientRouting };
export { firstRenderStartPromise };
export { getPageContextClient };
import { assert, assertInfo, assertWarning } from '../../utils/assert.js';
import { catchInfiniteLoop } from '../../utils/catchInfiniteLoop.js';
import { genPromise } from '../../utils/genPromise.js';
import { getGlobalObject } from '../../utils/getGlobalObject.js';
import { hasProp } from '../../utils/hasProp.js';
import { isCallable } from '../../utils/isCallable.js';
import { isReact } from '../../utils/isReact.js';
import { objectAssign } from '../../utils/objectAssign.js';
import { redirectHard } from '../../utils/redirectHard.js';
import { updateType } from '../../utils/updateType.js';
import { getPageContextFromHooksClient, getPageContextFromHooksServer, getPageContextFromHooksClient_firstRender, getPageContextFromHooksServer_firstRender, setPageContextInitIsPassedToClient, } from './getPageContextFromHooks.js';
import { createPageContextClient } from './createPageContextClient.js';
import { addLinkPrefetchHandlers, addLinkPrefetchHandlers_unwatch, addLinkPrefetchHandlers_watch, getPageContextPrefetched, populatePageContextPrefetchCache, } from './prefetch.js';
import { execHookOnRenderClient } from '../shared/execHookOnRenderClient.js';
import { isErrorFetchingStaticAssets, loadPageConfigsLazyClientSide, } from '../shared/loadPageConfigsLazyClientSide.js';
import { pushHistoryState } from './history.js';
import { addNewPageContextAborted, getPageContextAddendumAbort, isAbortError, logAbort, } from '../../shared-server-client/route/abort.js';
import { route } from '../../shared-server-client/route/index.js';
import { isClientSideRoutable } from './isClientSideRoutable.js';
import { setScrollPosition } from './setScrollPosition.js';
import { scrollRestoration_initialRenderIsDone } from './scrollRestoration.js';
import { getErrorPageId, isErrorPage } from '../../shared-server-client/error-page.js';
import { setPageContextCurrent } from './getPageContextCurrent.js';
import { getRouteStringParameterList } from '../../shared-server-client/route/resolveRouteString.js';
import { getCurrentUrl } from '../shared/getCurrentUrl.js';
import { execHookList, execHook } from '../../shared-server-client/hooks/execHook.js';
import { getPageContextPublicClient } from './getPageContextPublicClient.js';
import { getHooksFromPageContextNew } from '../../shared-server-client/hooks/getHook.js';
import { getPageContextPublicClientMinimal } from '../shared/getPageContextPublicClientShared.js';
import { logErrorClient } from './logErrorClient.js';
const globalObject = getGlobalObject('runtime-client-routing/renderPageClient.ts', (() => {
const { promise: firstRenderStartPromise, resolve: firstRenderStartPromiseResolve } = genPromise();
return {
renderCounter: 0,
firstRenderStartPromise,
firstRenderStartPromiseResolve,
};
})());
const { firstRenderStartPromise } = globalObject;
async function renderPageClient(renderArgs) {
catchInfiniteLoop('renderPageClient()');
const { urlOriginal = getCurrentUrl(), overwriteLastHistoryEntry = false, isBackwardNavigation = false, isHistoryNavigation = false, doNotRenderIfSamePage, isClientSideNavigation = true, pageContextInitClient, pageContextsAborted = [], } = renderArgs;
let { scrollTarget } = renderArgs;
const { previousPageContext } = globalObject;
addLinkPrefetchHandlers_unwatch();
const { isRenderOutdated, setHydrationCanBeAborted, isFirstRender } = getIsRenderOutdated();
const pageContextBeginArgs = {
urlOriginal,
isBackwardNavigation,
isHistoryNavigation,
pageContextsAborted,
isClientSideNavigation,
pageContextInitClient,
isFirstRender,
};
if (globalObject.clientRoutingIsDisabled) {
redirectHard(urlOriginal);
return;
}
globalObject.firstRenderStartPromiseResolve();
if (isRenderOutdated())
return;
return await renderPageNominal();
async function renderPageNominal() {
const onError = async (err) => {
await handleError({ err });
};
const pageContext = await getPageContextBegin(false, pageContextBeginArgs);
if (isRenderOutdated())
return;
// onPageTransitionStart()
if (globalObject.isFirstRenderDone) {
assert(previousPageContext);
// We use the hook of the previous page in order to be able to call onPageTransitionStart() before fetching the files of the next page.
// https://github.com/vikejs/vike/issues/1560
if (!globalObject.isTransitioning) {
globalObject.isTransitioning = true;
const hooks = getHooksFromPageContextNew('onPageTransitionStart', previousPageContext);
try {
await execHookList(hooks, pageContext, getPageContextPublicClientMinimal);
}
catch (err) {
await onError(err);
return;
}
if (isRenderOutdated())
return;
}
}
// Get pageContext serialized in <script id="vike_pageContext" type="application/json">
if (isFirstRender) {
const pageContextSerialized = getPageContextFromHooksServer_firstRender();
// TO-DO/eventually: create helper assertPageContextFromHook()
assert(!('urlOriginal' in pageContextSerialized));
objectAssign(pageContext, pageContextSerialized);
// TO-DO/pageContext-prefetch: remove or change, because this only makes sense for a pre-rendered page
populatePageContextPrefetchCache(pageContext, { pageContextFromHooksServer: pageContextSerialized });
}
// Route
// - We must also run it upon hydration to call the onBeforeRoute() hook, which is needed for i18n URL locale extraction.
{
let pageContextFromRoute;
try {
pageContextFromRoute = await route(pageContext);
}
catch (err) {
await onError(err);
return;
}
if (isRenderOutdated())
return;
// TO-DO/eventually: create helper assertPageContextFromHook()
assert(!('urlOriginal' in pageContextFromRoute));
if (isFirstRender) {
// Set pageContext properties set by onBeforeRoute()
// - But we skip pageId and routeParams because routing may have been aborted by a server-side `throw render()`
const { pageId: _, routeParams: __, ...rest } = pageContextFromRoute;
objectAssign(pageContext, rest);
assert(hasProp(pageContext, 'routeParams', 'string{}')); // Help TS
}
else {
objectAssign(pageContext, pageContextFromRoute);
}
if (!isFirstRender) {
if (!pageContextFromRoute.pageId) {
/*
// We don't use the client router to render the 404 page:
// - So that the +redirects setting (https://vike.dev/redirects) can be applied.
// - This is the main argument.
// - See also failed CI: https://github.com/vikejs/vike/pull/1871
// - So that server-side error tracking can track 404 links?
// - We do use the client router for rendering the error page, so I don't think this is much of an argument.
await renderErrorPage({ is404: true })
*/
redirectHard(urlOriginal);
return;
}
const isClientRoutable = await isClientSideRoutable(pageContextFromRoute.pageId, pageContext);
if (isRenderOutdated())
return;
if (!isClientRoutable) {
redirectHard(urlOriginal);
return;
}
const isSamePage = pageContextFromRoute.pageId &&
previousPageContext?.pageId &&
pageContextFromRoute.pageId === previousPageContext.pageId;
if (doNotRenderIfSamePage && isSamePage) {
// Skip's Vike's rendering; let the user handle the navigation
return;
}
}
}
assert(hasProp(pageContext, 'pageId', 'string')); // Help TS
const res = await loadPageConfigsLazyClientSideAndExecHook(pageContext, isFirstRender, isRenderOutdated);
/* Already called inside loadPageConfigsLazyClientSideAndExecHook()
if (isRenderOutdated()) return
*/
if (res.skip)
return;
if ('err' in res) {
await onError(res.err);
return;
}
updateType(pageContext, res.pageContext);
setPageContextCurrent(pageContext);
// Set global hydrationCanBeAborted
if (pageContext.exports.hydrationCanBeAborted) {
setHydrationCanBeAborted();
}
else {
assertWarning(!isReact(), 'You seem to be using React; we recommend setting hydrationCanBeAborted to true, see https://vike.dev/hydrationCanBeAborted', { onlyOnce: true });
}
// There wasn't any `await` but the isRenderOutdated() return value may have changed because we called setHydrationCanBeAborted()
if (isRenderOutdated())
return;
// Get pageContext from hooks (fetched from server, and/or directly called on the client-side)
if (isFirstRender) {
assert(hasProp(pageContext, '_hasPageContextFromServer', 'true'));
let pageContextAugmented;
try {
pageContextAugmented = await getPageContextFromHooksClient_firstRender(pageContext);
}
catch (err) {
await onError(err);
return;
}
if (isRenderOutdated())
return;
updateType(pageContext, pageContextAugmented);
// Render page view
return await renderPageView(pageContext);
}
else {
// Fetch pageContext from server-side hooks
let pageContextFromHooksServer;
const pageContextPrefetched = getPageContextPrefetched(pageContext);
if (pageContextPrefetched) {
pageContextFromHooksServer = pageContextPrefetched;
}
else {
try {
const result = await getPageContextFromHooksServer(pageContext, false);
if (result.is404ServerSideRouted)
return;
pageContextFromHooksServer = result.pageContextFromHooksServer;
// TO-DO/pageContext-prefetch: remove or change, because this only makes sense for a pre-rendered page
populatePageContextPrefetchCache(pageContext, result);
}
catch (err) {
await onError(err);
return;
}
}
if (isRenderOutdated())
return;
// TO-DO/eventually: create helper assertPageContextFromHook()
assert(!('urlOriginal' in pageContextFromHooksServer));
objectAssign(pageContext, pageContextFromHooksServer);
// Get pageContext from client-side hooks
let pageContextFromHooksClient;
try {
pageContextFromHooksClient = await getPageContextFromHooksClient(pageContext, false);
}
catch (err) {
await onError(err);
return;
}
if (isRenderOutdated())
return;
updateType(pageContext, pageContextFromHooksClient);
return await renderPageView(pageContext);
}
}
// 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 handleError(args) {
const { err } = args;
assert(err);
// Logging
if (!isAbortError(err)) {
// We don't swallow 404 errors:
// - On the server-side, Vike swallows / doesn't show any 404 error log because it's expected that a user may go to some random non-existent URL. (We don't want to flood the app's error tracking with 404 logs.)
// - On the client-side, if the user navigates to a 404 then it means that the UI has a broken link. (It isn't expected that users can go to some random URL using the client-side router, as it would require, for example, the user to manually change the URL of a link by manually manipulating the DOM which highly unlikely.)
logErrorClient(err);
}
else {
// We swallow throw redirect()/render() called by client-side hooks onBeforeRender()/data()/guard()
// We handle the abort error down below.
}
// pageContext
const pageContext = await getPageContextBegin(true, pageContextBeginArgs);
if (isRenderOutdated())
return;
objectAssign(pageContext, {
errorWhileRendering: err,
});
// throw redirect()/render()
let pageContextAbort;
if (isAbortError(err)) {
const res = await handleAbort(err, pageContext);
if (res.skip)
return;
pageContextAbort = res.pageContextAbort;
}
// Render error page
await renderErrorPage(pageContext, args, pageContextAbort);
}
async function renderErrorPage(pageContext, args, pageContextAbort) {
const onError = (err) => {
/* When we can't render the error page, we prefer showing a blank page over letting the server-side try because otherwise:
- We risk running into an infinite loop of reloads which would overload the server.
- An infinite reloading page is a even worse UX than a blank page.
redirectHard(urlOriginal)
*/
logErrorClient(err);
};
const errorPageId = getErrorPageId(pageContext._pageFilesAll, pageContext._globalContext._pageConfigs);
if (!errorPageId)
throw new Error('No error page defined.');
objectAssign(pageContext, {
pageId: errorPageId,
routeParams: {},
});
// throw render(statusCode)
if (pageContextAbort) {
assert(pageContextAbort.abortStatusCode);
assert(!('urlOriginal' in pageContextAbort));
objectAssign(pageContext, pageContextAbort);
objectAssign(pageContext, { is404: pageContextAbort.abortStatusCode === 404 });
}
else {
objectAssign(pageContext, { is404: false });
}
const isClientRoutable = await isClientSideRoutable(pageContext.pageId, pageContext);
if (isRenderOutdated())
return;
if (!isClientRoutable) {
redirectHard(urlOriginal);
return;
}
if (import.meta.env.DEV || globalThis.__VIKE__IS_DEBUG) {
assertInfo(false, `Rendering error page ${errorPageId}`, { onlyOnce: false });
}
const res = await loadPageConfigsLazyClientSideAndExecHook(pageContext, isFirstRender, isRenderOutdated);
/* Already called inside loadPageConfigsLazyClientSideAndExecHook()
if (isRenderOutdated()) return
*/
if (res.skip)
return;
if ('err' in res) {
onError(res.err);
return;
}
updateType(pageContext, res.pageContext);
setPageContextCurrent(pageContext);
let pageContextFromHooksServer;
try {
const result = await getPageContextFromHooksServer(pageContext, true);
if (result.is404ServerSideRouted)
return;
pageContextFromHooksServer = result.pageContextFromHooksServer;
}
catch (err) {
onError(err);
return;
}
if (isRenderOutdated())
return;
// TO-DO/eventually: create helper assertPageContextFromHook()
assert(!('urlOriginal' in pageContextFromHooksServer));
objectAssign(pageContext, pageContextFromHooksServer);
let pageContextFromHooksClient;
try {
pageContextFromHooksClient = await getPageContextFromHooksClient(pageContext, true);
}
catch (err) {
onError(err);
return;
}
if (isRenderOutdated())
return;
updateType(pageContext, pageContextFromHooksClient);
await renderPageView(pageContext, args);
}
async function handleAbort(err, pageContext) {
const errAbort = err;
logAbort(err, !import.meta.env.DEV, pageContext);
const pageContextAbort = errAbort._pageContextAbort;
addNewPageContextAborted(pageContextsAborted, pageContext, pageContextAbort);
// throw render('/some-url')
if (pageContextAbort._urlRewrite) {
await renderPageClient({
...renderArgs,
scrollTarget: undefined,
pageContextsAborted,
});
return { skip: true };
}
// throw redirect('/some-url')
if (pageContextAbort._urlRedirect) {
const urlRedirect = pageContextAbort._urlRedirect.url;
if (!urlRedirect.startsWith('/')) {
// External redirection
redirectHard(urlRedirect);
return { skip: true };
}
else {
await renderPageClient({
...renderArgs,
scrollTarget: undefined,
urlOriginal: urlRedirect,
overwriteLastHistoryEntry: false,
pageContextsAborted,
});
}
return { skip: true };
}
// throw render(statusCode)
return { pageContextAbort };
}
async function renderPageView(pageContext, isErrorPage) {
const onError = async (err) => {
if (!isErrorPage) {
await handleError({ err });
}
else {
logErrorClient(err);
}
};
// We use globalObject.onRenderClientPreviousPromise in order to ensure that there is never two concurrent onRenderClient() calls
if (globalObject.onRenderClientPreviousPromise) {
// Make sure that the previous render has finished
await globalObject.onRenderClientPreviousPromise;
assert(globalObject.onRenderClientPreviousPromise === undefined);
if (isRenderOutdated())
return;
}
changeUrl(urlOriginal, overwriteLastHistoryEntry);
globalObject.previousPageContext = pageContext;
assert(globalObject.onRenderClientPreviousPromise === undefined);
const onRenderClientPromise = (async () => {
let onRenderClientError;
try {
await execHookOnRenderClient(pageContext, getPageContextPublicClient);
}
catch (err) {
assert(err);
onRenderClientError = err;
}
globalObject.onRenderClientPreviousPromise = undefined;
globalObject.isFirstRenderDone = true;
return onRenderClientError;
})();
globalObject.onRenderClientPreviousPromise = onRenderClientPromise;
const onRenderClientError = await onRenderClientPromise;
assert(globalObject.onRenderClientPreviousPromise === undefined);
if (onRenderClientError) {
await onError(onRenderClientError);
if (!isErrorPage)
return;
}
/* We don't abort in order to ensure that onHydrationEnd() is called: we abort only after onHydrationEnd() is called.
if (isRenderOutdated(true)) return
*/
// onHydrationEnd()
if (isFirstRender && !onRenderClientError) {
try {
await execHook('onHydrationEnd', pageContext, getPageContextPublicClient);
}
catch (err) {
await onError(err);
if (!isErrorPage)
return;
}
if (isRenderOutdated(true))
return;
}
// We purposely abort *after* onHydrationEnd() is called (see comment above).
if (isRenderOutdated(true))
return;
// onPageTransitionEnd()
if (globalObject.isTransitioning) {
globalObject.isTransitioning = undefined;
assert(previousPageContext);
const hooks = getHooksFromPageContextNew('onPageTransitionEnd', previousPageContext);
try {
await execHookList(hooks, pageContext, getPageContextPublicClient);
}
catch (err) {
await onError(err);
if (!isErrorPage)
return;
}
if (isRenderOutdated(true))
return;
}
if (!scrollTarget && previousPageContext) {
const keepScrollPositionPrev = getKeepScrollPositionSetting(previousPageContext);
const keepScrollPositionNext = getKeepScrollPositionSetting(pageContext);
if (keepScrollPositionNext !== false &&
keepScrollPositionPrev !== false &&
areKeysEqual(keepScrollPositionNext, keepScrollPositionPrev)) {
scrollTarget = { preserveScroll: true };
}
}
// Page scrolling
setScrollPosition(scrollTarget, urlOriginal);
scrollRestoration_initialRenderIsDone();
if (pageContext._hasPageContextFromServer)
setPageContextInitIsPassedToClient(pageContext);
// Add link prefetch handlers
addLinkPrefetchHandlers_watch();
addLinkPrefetchHandlers();
globalObject.renderedPageContext = pageContext;
stampFinished(urlOriginal);
return pageContext;
}
}
async function getPageContextBegin(isForErrorPage, { urlOriginal, isBackwardNavigation, isHistoryNavigation, pageContextsAborted, isClientSideNavigation, pageContextInitClient, isFirstRender, }) {
const previousPageContext = globalObject.previousPageContext ?? null;
const pageContext = await createPageContextClient(urlOriginal);
objectAssign(pageContext, {
isBackwardNavigation,
isHistoryNavigation,
isClientSideNavigation,
isHydration: isFirstRender && !isForErrorPage,
previousPageContext,
pageContextsAborted,
...pageContextInitClient,
});
globalObject.currentPageContext = pageContext;
// TO-DO/next-major-release: remove
Object.defineProperty(pageContext, '_previousPageContext', {
get() {
assertWarning(false, 'pageContext._previousPageContext has been renamed pageContext.previousPageContext', {
showStackTrace: true,
onlyOnce: true,
});
return previousPageContext;
},
enumerable: false,
});
{
const pageContextAddendumAbort = getPageContextAddendumAbort(pageContextsAborted);
assert(!pageContextAddendumAbort || !('urlOriginal' in pageContextAddendumAbort));
objectAssign(pageContext, pageContextAddendumAbort);
}
return pageContext;
}
// For Vike tests (but also potentially for Vike users)
// https://github.com/vikejs/vike/blob/ffbc5cf16407bcc075f414447e50d997c87c0c94/test/playground/pages/nested-layout/e2e-test.ts#L59
function stampFinished(urlOriginal) {
window._vike ?? (window._vike = {});
window._vike.fullyRenderedUrl = urlOriginal;
}
function changeUrl(url, overwriteLastHistoryEntry) {
if (getCurrentUrl() === url)
return;
pushHistoryState(url, overwriteLastHistoryEntry);
}
function disableClientRouting(err, log) {
globalObject.clientRoutingIsDisabled = true;
assert(isErrorFetchingStaticAssets(err));
if (log) {
// We purposely don't use console.error() to avoid flooding error trackers such as Sentry
console.log(err);
}
assertInfo(false, [
'Failed to fetch static asset.',
import.meta.env.PROD ? 'This usually happens when a new frontend is deployed.' : null,
'Falling back to Server Routing.',
'(The next page navigation will use Server Routing instead of Client Routing.)',
]
.filter(Boolean)
.join(' '), { onlyOnce: true });
}
function getIsRenderOutdated() {
const renderNumber = ++globalObject.renderCounter;
assert(renderNumber >= 1);
let hydrationCanBeAborted = false;
const setHydrationCanBeAborted = () => {
hydrationCanBeAborted = true;
};
/** Whether the rendering should be aborted because a new rendering has started. We should call this after each `await`. */
const isRenderOutdated = (isRenderCleanup) => {
// Never abort first render if `hydrationCanBeAborted` isn't `true`
{
const isFirstRender = renderNumber === 1;
if (isFirstRender && !hydrationCanBeAborted && !isRenderCleanup) {
return false;
}
}
// If there is a newer rendering, we should abort all previous renderings
return renderNumber !== globalObject.renderCounter;
};
return {
isRenderOutdated,
setHydrationCanBeAborted,
isFirstRender: renderNumber === 1,
};
}
function getRenderCount() {
return globalObject.renderCounter;
}
function getKeepScrollPositionSetting(pageContext) {
const c = pageContext.from.configsStandard.keepScrollPosition;
if (!c)
return false;
let val = c.value;
const configDefinedAt = c.definedAt;
assert(configDefinedAt);
const routeParameterList = getRouteStringParameterList(configDefinedAt);
if (isCallable(val))
val = val(pageContext, {
configDefinedAt: c.definedAt,
/* We don't pass routeParameterList because it's useless: the user knows the parameter list.
routeParameterList
*/
});
if (val === true) {
return [
configDefinedAt,
...routeParameterList.map((param) => {
const val = pageContext.routeParams[param];
assert(val);
return val;
}),
];
}
// We skip validation and type-cast instead of assertUsage() in order to save client-side KBs
return val;
}
function areKeysEqual(key1, key2) {
if (key1 === key2)
return true;
if (!Array.isArray(key1) || !Array.isArray(key2))
return false;
return key1.length === key2.length && key1.every((_, i) => key1[i] === key2[i]);
}
/**
* Get the `pageContext` object on the client-side.
*
* https://vike.dev/getPageContextClient
*/
function getPageContextClient() {
const pageContext = globalObject.currentPageContext;
if (!pageContext)
return null;
return getPageContextPublicClient(pageContext);
}
async function loadPageConfigsLazyClientSideAndExecHook(pageContext, isFirstRender, isRenderOutdated) {
let hasErr = false;
let err;
let pageContextAddendum;
try {
pageContextAddendum = await loadPageConfigsLazyClientSide(pageContext.pageId, pageContext._pageFilesAll, pageContext._globalContext._pageConfigs, pageContext._globalContext._pageConfigGlobal);
}
catch (err_) {
err = err_;
hasErr = true;
if (handleErrorFetchingStaticAssets(err, pageContext, isFirstRender)) {
return { skip: true };
}
else {
// Syntax error in user file
}
}
if (isRenderOutdated())
return { skip: true };
if (hasErr)
return { err };
objectAssign(pageContext, pageContextAddendum);
try {
await execHook('onCreatePageContext', pageContext, getPageContextPublicClient);
}
catch (err_) {
err = err;
hasErr = true;
}
if (isRenderOutdated())
return { skip: true };
if (hasErr)
return { err };
return { pageContext };
}
function handleErrorFetchingStaticAssets(err, pageContext, isFirstRender) {
if (!isErrorFetchingStaticAssets(err)) {
return false;
}
if (isFirstRender) {
disableClientRouting(err, false);
// This may happen if the frontend was newly deployed during hydration.
// Ideally: re-try a couple of times by reloading the page (not entirely trivial to implement since `localStorage` is needed.)
throw err;
}
else {
disableClientRouting(err, true);
}
redirectHard(pageContext.urlOriginal);
return true;
}
// [HMR] If error page is shown => re-render whole page
if (import.meta.env.DEV && import.meta.hot)
import.meta.hot.on('vite:afterUpdate', () => {
const pageContext = globalObject.renderedPageContext;
if (pageContext?.pageId && isErrorPage(pageContext.pageId, pageContext._globalContext._pageConfigs)) {
renderPageClient({
scrollTarget: { preserveScroll: false },
urlOriginal: getCurrentUrl(),
overwriteLastHistoryEntry: true,
});
}
});