diff --git a/src/errors.test.ts b/src/errors.test.ts new file mode 100644 index 0000000..744503c --- /dev/null +++ b/src/errors.test.ts @@ -0,0 +1,47 @@ +import {isDynamicServerError} from '@/errors'; + +describe('errors', () => { + type TypeGuardScenario = { + error: any, + expected: boolean, + }; + + it.each([ + { + error: null, + expected: false, + }, + { + error: undefined, + expected: false, + }, + { + error: {}, + expected: false, + }, + { + error: new Error(), + expected: false, + }, + { + error: new class { + public readonly digest = 'DYNAMIC_SERVER_USAGE'; + }(), + expected: false, + }, + { + error: new class DynamicServerError { + public readonly digest = 'foo'; + }(), + expected: false, + }, + { + error: new class DynamicServerError { + public readonly digest = 'DYNAMIC_SERVER_USAGE'; + }(), + expected: true, + }, + ])('should return $expected for $error identifying a DynamicServerError', ({error, expected}) => { + expect(isDynamicServerError(error)).toBe(expected); + }); +}); diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..2c6fffb --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,11 @@ +export declare class DynamicServerError extends Error { + public readonly digest = 'DYNAMIC_SERVER_USAGE'; +} + +export function isDynamicServerError(error: unknown): error is DynamicServerError { + return typeof error === 'object' + && error !== null + && error.constructor.name === 'DynamicServerError' + && 'digest' in error + && error.digest === 'DYNAMIC_SERVER_USAGE'; +} diff --git a/src/hooks/useContent.test.ts b/src/hooks/useContent.test.ts index 7b9f7fb..d3c1969 100644 --- a/src/hooks/useContent.test.ts +++ b/src/hooks/useContent.test.ts @@ -53,6 +53,18 @@ describe('useContent', () => { expect(useContentMock).toHaveBeenCalledWith('id', undefined); }); + it('should ignore router errors', () => { + jest.mocked(useContentMock).mockReturnValue({}); + + jest.mocked(useRouter).mockImplementation(() => { + throw new Error(); + }); + + useContent('id'); + + expect(useContentMock).toHaveBeenCalledWith('id', undefined); + }); + it('should not override the specified locale', () => { jest.mocked(useContentMock).mockReturnValue({}); diff --git a/src/hooks/useContent.ts b/src/hooks/useContent.ts index 91779dd..530b2cc 100644 --- a/src/hooks/useContent.ts +++ b/src/hooks/useContent.ts @@ -1,5 +1,5 @@ import {useContent as useContentReact, UseContentOptions, SlotContent, VersionedSlotId} from '@croct/plug-react'; -import {useRouter} from 'next/router'; +import {NextRouter, useRouter as usePageRouter} from 'next/router'; export type {UseContentOptions} from '@croct/plug-react'; @@ -14,4 +14,12 @@ function useContentNext(id: VersionedSlotId, options?: UseContentOptions { + try { + return usePageRouter(); + } catch { + return {}; + } +} + export const useContent: typeof useContentReact = useContentNext; diff --git a/src/server/evaluate.test.ts b/src/server/evaluate.test.ts index 91a7716..59382dc 100644 --- a/src/server/evaluate.test.ts +++ b/src/server/evaluate.test.ts @@ -4,7 +4,6 @@ import {ApiKey, ApiKey as MockApiKey} from '@croct/sdk/apiKey'; import {FilteredLogger} from '@croct/sdk/logging/filteredLogger'; import {headers} from 'next/headers'; import {NextRequest, NextResponse} from 'next/server'; -import {DynamicServerError} from 'next/dist/client/components/hooks-server-context'; import {cql, evaluate, EvaluationOptions} from './evaluate'; import {resolveRequestContext, RequestContext} from '@/config/context'; import {getDefaultFetchTimeout} from '@/config/timeout'; @@ -188,7 +187,15 @@ describe('evaluation', () => { }); it('should rethrow dynamic server errors', async () => { - const error = new DynamicServerError('cause'); + const error = new class DynamicServerError extends Error { + public readonly digest = 'DYNAMIC_SERVER_USAGE'; + + public constructor() { + super('cause'); + + Object.setPrototypeOf(this, new.target.prototype); + } + }(); jest.mocked(resolveRequestContext).mockImplementation(() => { throw error; diff --git a/src/server/evaluate.ts b/src/server/evaluate.ts index 16cc8f6..e2f9590 100644 --- a/src/server/evaluate.ts +++ b/src/server/evaluate.ts @@ -3,12 +3,12 @@ import type {JsonValue} from '@croct/plug-react'; import {FilteredLogger} from '@croct/sdk/logging/filteredLogger'; import {ConsoleLogger} from '@croct/sdk/logging/consoleLogger'; import {formatCause} from '@croct/sdk/error'; -import {isDynamicServerError} from 'next/dist/client/components/hooks-server-context'; import {getApiKey} from '@/config/security'; import {RequestContext, resolveRequestContext} from '@/config/context'; import {getDefaultFetchTimeout} from '@/config/timeout'; import {isAppRouter, RouteContext} from '@/headers'; import {getEnvEntry, getEnvFlag} from '@/config/env'; +import {isDynamicServerError} from '@/errors'; export type EvaluationOptions = Omit, 'apiKey' | 'appId'> & { route?: RouteContext, diff --git a/src/server/fetchContent.test.ts b/src/server/fetchContent.test.ts index 63bd2c6..b7ed433 100644 --- a/src/server/fetchContent.test.ts +++ b/src/server/fetchContent.test.ts @@ -4,7 +4,6 @@ import {FetchResponse} from '@croct/plug/plug'; import {ApiKey, ApiKey as MockApiKey} from '@croct/sdk/apiKey'; import {FilteredLogger} from '@croct/sdk/logging/filteredLogger'; import type {NextRequest, NextResponse} from 'next/server'; -import {DynamicServerError} from 'next/dist/client/components/hooks-server-context'; import {fetchContent, FetchOptions} from './fetchContent'; import {RequestContext, resolvePreferredLocale, resolveRequestContext} from '@/config/context'; import {getDefaultFetchTimeout} from '@/config/timeout'; @@ -291,7 +290,15 @@ describe('fetchContent', () => { }); it('should rethrow dynamic server errors', async () => { - const error = new DynamicServerError('cause'); + const error = new class DynamicServerError extends Error { + public readonly digest = 'DYNAMIC_SERVER_USAGE'; + + public constructor() { + super('cause'); + + Object.setPrototypeOf(this, new.target.prototype); + } + }(); jest.mocked(resolveRequestContext).mockImplementation(() => { throw error; diff --git a/src/server/fetchContent.ts b/src/server/fetchContent.ts index 9eda835..34fef47 100644 --- a/src/server/fetchContent.ts +++ b/src/server/fetchContent.ts @@ -8,12 +8,12 @@ import type {SlotContent, VersionedSlotId, JsonObject} from '@croct/plug-react'; import {FilteredLogger} from '@croct/sdk/logging/filteredLogger'; import {ConsoleLogger} from '@croct/sdk/logging/consoleLogger'; import {formatCause} from '@croct/sdk/error'; -import {isDynamicServerError} from 'next/dist/client/components/hooks-server-context'; import {getApiKey} from '@/config/security'; import {RequestContext, resolvePreferredLocale, resolveRequestContext} from '@/config/context'; import {getDefaultFetchTimeout} from '@/config/timeout'; import {RouteContext} from '@/headers'; import {getEnvEntry, getEnvFlag} from '@/config/env'; +import {isDynamicServerError} from '@/errors'; export type DynamicContentOptions = Omit, 'apiKey' | 'appId'>;