UNPKG

vike

Version:

(Replaces Next.js/Nuxt) 🔨 Composable framework to build advanced applications with flexibility and stability.

238 lines (237 loc) • 13.2 kB
export { getPageContextFromHooksClient }; export { getPageContextFromHooksClient_firstRender }; export { getPageContextFromHooksServer }; export { getPageContextFromHooksServer_firstRender }; export { setPageContextInitIsPassedToClient }; import { assert, assertUsage, getProjectError } from '../../utils/assert.js'; import { getGlobalObject } from '../../utils/getGlobalObject.js'; import { hasProp } from '../../utils/hasProp.js'; import { isObject } from '../../utils/isObject.js'; import { objectAssign } from '../../utils/objectAssign.js'; import { redirectHard } from '../../utils/redirectHard.js'; import { parse } from '@brillout/json-serializer/parse'; import { getPageContextSerializedInHtml } from '../shared/getJsonSerializedInHtml.js'; import { analyzePageServerSide } from '../../shared-server-client/getPageFiles/analyzePageServerSide.js'; import { removeBuiltInOverrides } from './getPageContext/removeBuiltInOverrides.js'; import { getPageContextRequestUrl } from '../../shared-server-client/getPageContextRequestUrl.js'; import { getPageConfig } from '../../shared-server-client/page-configs/helpers.js'; import { getConfigValueRuntime } from '../../shared-server-client/page-configs/getConfigValueRuntime.js'; import { assertOnBeforeRenderHookReturn } from '../../shared-server-client/assertOnBeforeRenderHookReturn.js'; import { execHookGuard } from '../../shared-server-client/route/execHookGuard.js'; import { AbortRender, isAbortPageContext } from '../../shared-server-client/route/abort.js'; import { pageContextInitIsPassedToClient } from '../../shared-server-client/misc/pageContextInitIsPassedToClient.js'; import { isServerSideError } from '../../shared-server-client/misc/isServerSideError.js'; import { execHook } from '../../shared-server-client/hooks/execHook.js'; import { getPageContextPublicClient } from './getPageContextPublicClient.js'; import '../assertEnvClient.js'; const globalObject = getGlobalObject('getPageContextFromHooks.ts', {}); // TO-DO/soon/cumulative-hooks: filter & execute all client-only hooks (see other TO-DO/soon/cumulative-hooks comments) // - The client-side needs to know what hooks are client-only // - Possible implementation: new computed prop `clientOnlyHooks: string[]` (list of hook ids) and add `hookId` to serialized config values const clientHooks = ['guard', 'data', 'onBeforeRender']; // Get `pageContext` values from `<script id="vike_pageContext" type="application/json">` function getPageContextFromHooksServer_firstRender() { const pageContextSerialized = getPageContextSerializedInHtml(); processPageContextFromServer(pageContextSerialized); objectAssign(pageContextSerialized, { _hasPageContextFromServer: true, }); return pageContextSerialized; } async function getPageContextFromHooksClient_firstRender(pageContext) { for (const hookName of clientHooks) { if (!hookClientOnlyExists(hookName, pageContext)) continue; if (hookName === 'guard') { await execHookGuardClient(pageContext); } else { await execHookDataLike(hookName, pageContext); } } return pageContext; } async function getPageContextFromHooksServer(pageContext, isErrorPage) { const pageContextFromHooksServer = { _hasPageContextFromServer: false, }; // If pageContextInit has some client data or if one of the hooks guard(), data() or onBeforeRender() is server-side // only, then we need to fetch pageContext from the server. // We do it before executing any client-side hook, because it contains pageContextInit which may be needed for guard() / data() / onBeforeRender(), for example pageContextInit.user is crucial for guard() if ( // For the error page, we cannot fetch pageContext from the server because the pageContext JSON request is based on the URL !isErrorPage && // true if pageContextInit has some client data or at least one of the data() and onBeforeRender() hooks is server-side only: (await hasPageContextServer(pageContext))) { const res = await fetchPageContextFromServer(pageContext); if ('is404ServerSideRouted' in res) return { is404ServerSideRouted: true }; const { pageContextFromServer } = res; pageContextFromHooksServer._hasPageContextFromServer = true; // Already handled assert(!(isServerSideError in pageContextFromServer)); assert(!('serverSideError' in pageContextFromServer)); objectAssign(pageContextFromHooksServer, pageContextFromServer); } // We cannot return the whole pageContext because this function is used for prefetching `pageContext` (which requires a partial pageContext to be merged with the future pageContext created upon rendering the page in the future). return { pageContextFromHooksServer }; } async function getPageContextFromHooksClient(pageContext, isErrorPage) { let dataHookExecuted = false; // At this point, we need to call the client-side guard(), data() and onBeforeRender() hooks, if they exist on client // env. However if we have fetched pageContext from the server, some of them might have run already on the // server-side, so we run only the client-only ones in this case. // Note: for the error page, we also execute the client-side data() and onBeforeRender() hooks, but maybe we // shouldn't? The server-side does it as well (but maybe it shouldn't). for (const hookName of clientHooks) { if (!hookClientOnlyExists(hookName, pageContext) && pageContext._hasPageContextFromServer) continue; if (hookName === 'guard') { if (isErrorPage) continue; await execHookGuardClient(pageContext); } else { if (hookName === 'data') dataHookExecuted = true; await execHookDataLike(hookName, pageContext); } } // Execute +onData const dataHookEnv = getHookEnv('data', pageContext); if ((dataHookExecuted && dataHookEnv.client) || (pageContext._hasPageContextFromServer && dataHookEnv.server)) { await execHookClient('onData', pageContext); } const pageContextFromHooksClient = pageContext; return pageContextFromHooksClient; } async function execHookClient(hookName, pageContext) { return await execHook(hookName, pageContext, (p) => getPageContextPublicClient(p)); } // It's a no-op if: // - No hook has been defined, or // - The hook's `env.client` is `false` async function execHookDataLike(hookName, pageContext) { let pageContextFromHook; if (hookName === 'data') { pageContextFromHook = await execHookData(pageContext); } else { pageContextFromHook = await execHookOnBeforeRender(pageContext); } Object.assign(pageContext, pageContextFromHook); } async function execHookData(pageContext) { const res = await execHookClient('data', pageContext); const hook = res[0]; // TO-DO/soon/cumulative-hooks: support cumulative if (!hook) return; const { hookReturn } = hook; const pageContextAddendum = { data: hookReturn }; return pageContextAddendum; } async function execHookOnBeforeRender(pageContext) { const res = await execHookClient('onBeforeRender', pageContext); const hook = res[0]; // TO-DO/soon/cumulative-hooks: support cumulative if (!hook) return; const { hookReturn, hookFilePath } = hook; const pageContextFromHook = {}; assertOnBeforeRenderHookReturn(hookReturn, hookFilePath); // Note: hookReturn looks like { pageContext: { ... } } const pageContextFromOnBeforeRender = hookReturn?.pageContext; if (pageContextFromOnBeforeRender) { objectAssign(pageContextFromHook, pageContextFromOnBeforeRender); } return pageContextFromHook; } // Workaround for the fact that the client-side cannot known whether a pageContext JSON request is needed in order to fetch pageContextInit data passed to the client. // - The workaround is reliable as long as the user sets additional pageContextInit to undefined instead of not defining the property: // ```diff // - // Breaks the workaround: // - const pageContextInit = { urlOriginal: req.url } // - if (user) pageContextInit.user = user // + // Makes the workaround reliable: // + const pageContextInit = { urlOriginal: req.url, user } // ``` // - We can show a warning to users when the pageContextInit keys aren't always the same. (We didn't implement the waning yet because it would require a new doc page https://vike.dev/pageContextInit#avoid-conditional-properties // - Workaround cannot be made completely reliable because the workaround assumes that passToClient is always the same, but the user may set a different passToClient value for another page // - Alternatively, we could define a new config `alwaysFetchPageContextFromServer: boolean` function setPageContextInitIsPassedToClient(pageContext) { if (pageContext[pageContextInitIsPassedToClient]) { globalObject.pageContextInitIsPassedToClient = true; } } // TO-DO/next-major-release: make it sync async function hasPageContextServer(pageContext) { if (isOldDesign(pageContext)) { const { hasOnBeforeRenderServerSideOnlyHook } = await analyzePageServerSide(pageContext._pageFilesAll, pageContext.pageId); // data() hooks didn't exist in the V0.4 design return hasOnBeforeRenderServerSideOnlyHook; } return !!globalObject.pageContextInitIsPassedToClient || hasServerOnlyHook(pageContext); } function hasServerOnlyHook(pageContext) { if (isOldDesign(pageContext)) return false; const pageConfig = getPageConfig(pageContext.pageId, pageContext._globalContext._pageConfigs); const val = getConfigValueRuntime(pageConfig, `hasServerOnlyHook`)?.value; assert(val === true || val === false); return val; } function hookClientOnlyExists(hookName, pageContext) { const hookEnv = getHookEnv(hookName, pageContext); return !!hookEnv.client && !hookEnv.server; } function getHookEnv(hookName, pageContext) { if (isOldDesign(pageContext)) { // Client-only onBeforeRender(), data(), or guard() hooks were never supported for the V0.4 design return { client: false, server: true }; } const pageConfig = getPageConfig(pageContext.pageId, pageContext._globalContext._pageConfigs); // No runtime validation to save client-side KBs const hookEnv = (getConfigValueRuntime(pageConfig, `${hookName}Env`)?.value ?? {}); return hookEnv; } async function fetchPageContextFromServer(pageContext) { let pageContextUrl = getPageContextRequestUrl(pageContext._urlRewrite ?? pageContext.urlOriginal); /* TO-DO/soon/once: pass & use previousUrl pageContextUrl = modifyUrlSameOrigin(pageContextUrl, { search: { _vike: JSON.stringify({ previousUrl: pageContext.previousPageContext.urlOriginal }) } }) */ const response = await fetch(pageContextUrl); { const contentType = response.headers.get('content-type'); const contentTypeCorrect = 'application/json'; const isCorrect = contentType && contentType.includes(contentTypeCorrect); // Static hosts + page doesn't exist if (!isCorrect && response.status === 404) { redirectHard(pageContext.urlOriginal); return { is404ServerSideRouted: true }; } assertUsage(isCorrect, `Wrong Content-Type for ${pageContextUrl}: it should be ${contentTypeCorrect} but it's ${contentType} instead. Make sure to properly use pageContext.httpResponse.headers, see https://vike.dev/renderPage`); } const responseText = await response.text(); const pageContextFromServer = parse(responseText); assert(isObject(pageContextFromServer)); if (isAbortPageContext(pageContextFromServer)) { throw AbortRender(pageContextFromServer); } // Is there a reason for having two different properties? Can't we use only one property? I guess/think the isServerSideError property was an attempt (a bad idea really) for rendering the error page even though an error occurred on the server-side (which is a bad idea because the added complexity is non-negligible while the added value is minuscule since the error page usually doesn't have any (meaningful / server-side) hooks). if ('serverSideError' in pageContextFromServer || isServerSideError in pageContextFromServer) { throw getProjectError(`pageContext couldn't be fetched because an error occurred on the server-side`); } processPageContextFromServer(pageContextFromServer); return { pageContextFromServer }; } function processPageContextFromServer(pageContext) { assertUsage(!('urlOriginal' in pageContext), "Adding 'urlOriginal' to passToClient is forbidden"); assert(hasProp(pageContext, 'pageId', 'string')); removeBuiltInOverrides(pageContext); } // TO-DO/next-major-release: remove function isOldDesign(pageContext) { return pageContext._globalContext._pageConfigs.length === 0; } async function execHookGuardClient(pageContext) { await execHookGuard(pageContext, (pageContext) => getPageContextPublicClient(pageContext)); }