vike
Version:
The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.
206 lines (205 loc) • 8.67 kB
JavaScript
export { execHook };
export { execHookGlobal };
export { execHookList };
export { execHookSingle };
export { execHookSingleSync };
export { execHookSingleWithReturn };
export { execHookSingleWithoutPageContext };
export { getPageContext_sync };
export { providePageContext };
export { isUserHookError };
import { assert, getProjectError, assertWarning, assertUsage } from '../../utils/assert.js';
import { getGlobalObject } from '../../utils/getGlobalObject.js';
import { humanizeTime } from '../../utils/humanizeTime.js';
import { isObject } from '../../utils/isObject.js';
import { getHooksFromPageConfigGlobalCumulative, getHooksFromPageContextNew } from './getHook.js';
import { getPageContextPublicShared } from '../getPageContextPublicShared.js';
const globalObject = getGlobalObject('utils/execHook.ts', {
userHookErrors: new WeakMap(),
pageContext: null,
});
async function execHook(hookName, pageContext, getPageContextPublic) {
const hooks = getHooksFromPageContextNew(hookName, pageContext);
return await execHookList(hooks, pageContext, getPageContextPublic);
}
async function execHookGlobal(hookName, globalContext, getGlobalContextPublic) {
const hooks = getHooksFromPageConfigGlobalCumulative(globalContext._pageConfigGlobal, hookName);
const globalContextPublic = getGlobalContextPublic(globalContext);
await Promise.all(hooks.map(async (hook) => {
await execHookBaseAsync(() => hook.hookFn(globalContextPublic), hook, globalContext, null);
}));
}
async function execHookList(hooks, pageContext, getPageContextPublic) {
if (!hooks.length)
return [];
const pageContextPublic = getPageContextPublic(pageContext);
const hooksWithResult = await Promise.all(hooks.map(async (hook) => {
const hookReturn = await execHookBaseAsync(() => hook.hookFn(pageContextPublic), hook, pageContext._globalContext, pageContextPublic);
return { ...hook, hookReturn };
}));
return hooksWithResult;
}
async function execHookSingle(hook, pageContext, getPageContextPublic) {
const hooksWithResult = await execHookList([hook], pageContext, getPageContextPublic);
const { hookReturn } = hooksWithResult[0];
assertUsage(hookReturn === undefined, `The ${hook.hookName}() hook defined by ${hook.hookFilePath} isn't allowed to return a value`);
}
async function execHookSingleWithReturn(hook, pageContext, getPageContextPublic) {
const hooksWithResult = await execHookList([hook], pageContext, getPageContextPublic);
const { hookReturn } = hooksWithResult[0];
return { hookReturn };
}
function isUserHookError(err) {
if (!isObject(err))
return false;
return globalObject.userHookErrors.get(err) ?? false;
}
async function execHookSingleWithoutPageContext(hook, globalContext, hookFnCaller) {
const { hookName, hookFilePath, hookTimeout } = hook;
const hookReturn = await execHookBaseAsync(hookFnCaller, { hookName, hookFilePath, hookTimeout }, globalContext, null);
return hookReturn;
}
function execHookSingleSync(hook, globalContext, pageContext, getPageContextPublic, hookFnCaller) {
const pageContextPublic = pageContext && getPageContextPublic(pageContext);
hookFnCaller ?? (hookFnCaller = () => hook.hookFn(pageContextPublic));
const hookReturn = execHookBase(hookFnCaller, hook, globalContext, pageContextPublic);
return { hookReturn };
}
function execHookBaseAsync(hookFnCaller, hook, globalContext, pageContextPublic) {
const { hookName, hookFilePath, hookTimeout: { error: timeoutErr, warning: timeoutWarn }, } = hook;
let resolve;
let reject;
const promise = new Promise((resolve_, reject_) => {
resolve = (ret) => {
clearTimeouts();
resolve_(ret);
};
reject = (err) => {
clearTimeouts();
reject_(err);
};
});
const clearTimeouts = () => {
if (currentTimeoutWarn)
clearTimeout(currentTimeoutWarn);
if (currentTimeoutErr)
clearTimeout(currentTimeoutErr);
};
const currentTimeoutWarn = isNotDisabled(timeoutWarn) &&
setTimeout(() => {
assertWarning(false, `The ${hookName}() hook defined by ${hookFilePath} is slow: it's taking more than ${humanizeTime(timeoutWarn)} (https://vike.dev/hooksTimeout)`, { onlyOnce: false });
}, timeoutWarn);
const currentTimeoutErr = isNotDisabled(timeoutErr) &&
setTimeout(() => {
const err = getProjectError(`The ${hookName}() hook defined by ${hookFilePath} timed out: it didn't finish after ${humanizeTime(timeoutErr)} (https://vike.dev/hooksTimeout)`);
reject(err);
}, timeoutErr);
(async () => {
try {
const ret = await execHookBase(hookFnCaller, hook, globalContext, pageContextPublic);
resolve(ret);
}
catch (err) {
if (isObject(err)) {
globalObject.userHookErrors.set(err, { hookName, hookFilePath });
}
reject(err);
}
})();
return promise;
}
// Every execHook* function should be based on this
function execHookBase(hookFnCaller, hook, globalContext, pageContext) {
const { hookName, hookFilePath } = hook;
assert(hookName !== 'onHookCall'); // ensure no infinite loop
const configValue = globalContext._pageConfigGlobal.configValues['onHookCall'];
const callOriginal = () => {
providePageContextInternal(pageContext);
return hookFnCaller();
};
// +onHookCall doesn't exist
if (!configValue?.value)
return callOriginal();
// +onHookCall wrapping
let originalCalled = false;
let originalReturn;
let originalError;
let call = () => {
originalCalled = true;
try {
originalReturn = callOriginal();
}
catch (err) {
originalError = err;
throw err;
}
return originalReturn;
};
for (const onHookCall of configValue.value) {
const hookPublic = { name: hookName, filePath: hookFilePath, call };
// Recursively wrap callOriginal() so +onHookCall can use async hooks. (E.g. vike-react-sentry integrates Sentry's `Tracer.startActiveSpan()`.)
call = () => {
;
(async () => {
try {
await onHookCall(hookPublic, pageContext);
}
catch (err) {
if (err !== originalError) {
console.error(err);
/* TO-DO/eventually: use dependency injection to be able to use logErrorServer() when this function runs on the server-side.
if (
!globalThis.__VIKE__IS_CLIENT &&
pageContext &&
// Avoid infinite loop
hookName !== 'onError'
) {
assert(!pageContext.isClientSide)
logErrorServer(err, pageContext)
} else {
logErrorClient(err)
}
//*/
}
}
})();
// +onHookCall must run hook.call() before any `await` — https://github.com/vikejs/vike/pull/2978#discussion_r2645232953
assertUsage(originalCalled, 'onHookCall() must run hook.call()');
return originalReturn;
};
}
// Start the call() chain
call();
if (originalError)
throw originalError;
return originalReturn;
}
function isNotDisabled(timeout) {
return !!timeout && timeout !== Infinity;
}
function getPageContext_sync() {
const { pageContext } = globalObject;
if (!pageContext)
return null;
const pageContextPublic = pageContext._isProxyObject
? // providePageContext() is called on the user-land (e.g. it's called by `vike-{react,vue,solid}`) thus it's already a proxy
pageContext
: getPageContextPublicShared(pageContext);
return pageContextPublic;
}
/**
* Provide `pageContext` for universal hooks.
*
* https://vike.dev/getPageContext
*/
function providePageContext(pageContext) {
providePageContextInternal(pageContext);
}
function providePageContextInternal(pageContext) {
globalObject.pageContext = pageContext;
// Promise.resolve() is quicker than process.nextTick() and setImmediate()
// https://stackoverflow.com/questions/67949576/process-nexttick-before-promise-resolve-then
Promise.resolve().then(() => {
globalObject.pageContext = null;
});
}