Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,249 changes: 3,609 additions & 3,640 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@nuxt/eslint": "^1.3.0",
"@nuxt/kit": "^3.17.5",
"@nuxt/kit": "^4.1.2",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/schema": "^3.17.5",
"@nuxt/test-utils": "^3.17.2",
"@nuxt/schema": "^4.1.2",
"@nuxt/test-utils": "^3.19.2",
"@playwright/test": "^1.53.1",
"@types/node": "^22.18.6",
"@vitest/coverage-v8": "^3.2.4",
Expand All @@ -88,13 +88,13 @@
"h3-compression": "^0.3.2",
"h3-fast-compression": "^1.0.1",
"happy-dom": "^14.12.0",
"nuxt": "^3.17.5",
"nuxt": "^4.1.2",
"playwright-core": "^1.44.1",
"prettier": "^3.3.2",
"simple-git": "^3.28.0",
"vite-plugin-css-injected-by-js": "^3.5.2",
"vitepress": "^1.4.1",
"vitest": "^3.1.2",
"vitest": "^3.2.4",
"vue-json-pretty": "^2.4.0",
"vue-tsc": "^2.1.6"
}
Expand Down
4 changes: 3 additions & 1 deletion playground/app/pages/cdnHeaderMerging.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const event = useRequestEvent()

const { data } = await useFetch('/api/cdnHeaders', {
onResponse(ctx) {
useCDNHeaders((cdn) => cdn.mergeFromResponse(ctx.response), event)
useCDNHeaders((cdn) => {
cdn.mergeFromResponse(ctx.response)
}, event)
},
})

Expand Down
15 changes: 4 additions & 11 deletions src/build/ModuleHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ import { relative } from 'pathe'
import type { RouterMethod } from 'h3'
import type { Nuxt, ResolvedNuxtTemplate } from 'nuxt/schema'
import type { ModuleOptions } from './options'
import type { defaultOptions } from './options/defaults'
import { logger } from './logger'
import { fileExists } from './helpers'
import type { ModuleTemplate } from './templates/defineTemplate'

type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
import { DEFAULT_API_PREFIX } from './options/defaults'

/**
* Log error message if obsolete configuration options are used.
Expand Down Expand Up @@ -50,11 +48,6 @@ function checkObsoleteOptions(options: any) {
}
}

type RequiredModuleOptions = WithRequired<
ModuleOptions,
keyof typeof defaultOptions
>

type ModuleHelperResolvers = {
/**
* Resolver for paths relative to the module root.
Expand Down Expand Up @@ -99,7 +92,7 @@ export class ModuleHelper {

public readonly isDev: boolean

public readonly options: RequiredModuleOptions
public readonly options: ModuleOptions

private nitroExternals: string[] = []
private tsPaths: Record<string, string> = {}
Expand All @@ -115,7 +108,7 @@ export class ModuleHelper {

checkObsoleteOptions(options)

this.options = options as RequiredModuleOptions
this.options = options

this.isDev = nuxt.options.dev
this.resolvers = {
Expand Down Expand Up @@ -266,7 +259,7 @@ export class ModuleHelper {
public addServerHandler(name: string, path: string, method: RouterMethod) {
addServerHandler({
handler: this.resolvers.module.resolve('./runtime/server/api/' + name),
route: this.options.api.prefix + '/' + path,
route: (this.options.api?.prefix ?? DEFAULT_API_PREFIX) + '/' + path,
method,
})
}
Expand Down
12 changes: 12 additions & 0 deletions src/build/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,16 @@ export interface ModuleOptions {
* Don't log the caching overview.
*/
disableCacheOverviewLogMessage?: boolean

/**
* Enables test mode.
*
* The following is possible during test mode:
* - Current time override
* Send the X-Nuxt-Multi-Cache-Date-Override HTTP header to override the
* "current time" for anything nuxt-multi-cache related. This allows
* you to write tests for cache-related behaviour.
* The date should be in the ISO format returned by date.toISOString().
*/
enableTestMode?: boolean
}
25 changes: 21 additions & 4 deletions src/build/templates/definitions/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,28 @@ export default defineTemplate(
path: 'nuxt-multi-cache/config',
},
(helper) => {
// The module options syntax is such that if the key of a feature is
// completely undefined, then the feature is completely disabled.
// If any object is provided, then the feature is generally enabled,
// meaning that its functionality is included (such as composables or
// components), however it's not actually fully enabled.
// Whether a feature is fully enabled is decided using runtime config.
const cdnEnabled = !!helper.options.cdn
const routeCacheEnabled = !!helper.options.route
const componentCacheEnabled = !!helper.options.component
const dataCacheEnabled = !!helper.options.data

return `
export const debug = ${JSON.stringify(!!helper.options.debug)}
export const cdnCacheControlHeader = import.meta.server ? ${JSON.stringify(helper.options.cdn?.cacheControlHeader || DEFAULT_CDN_CONTROL_HEADER)} : ''
export const cdnCacheTagHeader = import.meta.server ? ${JSON.stringify(helper.options.cdn?.cacheTagHeader || DEFAULT_CDN_TAG_HEADER)} : ''
export const cdnEnabled = ${JSON.stringify(!!helper.options.cdn?.enabled)}
export const routeCacheEnabled = ${JSON.stringify(!!helper.options.route?.enabled)}
export const componentCacheEnabled = ${JSON.stringify(!!helper.options.component?.enabled)}
export const dataCacheEnabled = ${JSON.stringify(!!helper.options.data?.enabled)}
export const cdnEnabled = ${JSON.stringify(cdnEnabled)}
export const routeCacheEnabled = ${JSON.stringify(routeCacheEnabled)}
export const componentCacheEnabled = ${JSON.stringify(componentCacheEnabled)}
export const dataCacheEnabled = ${JSON.stringify(dataCacheEnabled)}
export const shouldLogCacheOverview = ${JSON.stringify(!helper.options.disableCacheOverviewLogMessage)}
export const cacheTagInvalidationDelay = ${JSON.stringify(helper.options.api?.cacheTagInvalidationDelay || DEFAULT_CACHE_TAG_INVALIDATION_DELAY)}
export const isTestMode = ${JSON.stringify(!!helper.options.enableTestMode)}
export const isServer = import.meta.server
`
},
Expand Down Expand Up @@ -82,6 +94,11 @@ export declare const shouldLogCacheOverview: boolean
* Alias for import.meta.server, used for mocking in tests.
*/
export declare const isServer: boolean

/**
* If test mode is enabled.
*/
export declare const isTestMode: boolean
`
},
)
2 changes: 1 addition & 1 deletion src/build/templates/definitions/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default defineTemplate(
{ path: 'nuxt-multi-cache/nitro' },
null,
(helper) => {
const serverApiPrefix = helper.options.api.prefix
const serverApiPrefix = helper.options.api?.prefix
const endpoints: string[] = []

const caches = ['data', 'route', 'component']
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/composables/useCDNHeaders.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { H3Event } from 'h3'
import type { NuxtMultiCacheCDNHelper } from './../helpers/CDNHelper'
import { useCDNHeaders as serverUseCdnHeaders } from './../server/utils/useCDNHeaders'
import { useRequestEvent } from '#imports'
import { isServer } from '#nuxt-multi-cache/config'
import { useCDNHeadersImplementation } from '../shared/useCDNHeaders'

/**
* Return the helper to be used for interacting with the CDN headers feature.
Expand Down Expand Up @@ -30,5 +30,5 @@ export function useCDNHeaders(
return
}

serverUseCdnHeaders(cb, event, applyToEvent)
useCDNHeadersImplementation(cb, event, applyToEvent)
}
4 changes: 2 additions & 2 deletions src/runtime/composables/useCacheAwareFetchInterceptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { FetchContext, ResponseType, FetchResponse } from 'ofetch'
import { useRequestEvent, useRuntimeConfig } from '#imports'
import { useCDNHeaders } from './../server/utils/useCDNHeaders'
import { useRouteCache } from './../server/utils/useRouteCache'
import {
ROUTE_CACHE_TAGS_HEADER,
Expand All @@ -12,6 +11,7 @@ import {
routeCacheEnabled,
} from '#nuxt-multi-cache/config'
import type { BubbleCacheability } from '../types'
import { useCDNHeadersImplementation } from '../shared/useCDNHeaders'

type OnResponseInterceptor = (
ctx: FetchContext<unknown, ResponseType> & {
Expand Down Expand Up @@ -61,7 +61,7 @@ export function useCacheAwareFetchInterceptor(
config.multiCache.cdn &&
(bubbleCacheability === true || bubbleCacheability === 'cdn')
) {
useCDNHeaders(
useCDNHeadersImplementation(
(cdn) => {
cdn.mergeFromResponse(ctx.response)
},
Expand Down
12 changes: 4 additions & 8 deletions src/runtime/composables/useCachedAsyncData.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type { NuxtApp, AsyncDataOptions, AsyncData, NuxtError } from 'nuxt/app'
import type { MaybeRefOrGetter } from 'vue'
import type {
DefaultAsyncDataErrorValue,
DefaultAsyncDataValue,
} from '#app/defaults'
import type { PickFrom } from '#app/composables/asyncData'
import {
useAsyncData,
Expand Down Expand Up @@ -32,7 +28,7 @@ type CachedAsyncDataOptions<
ResT,
DataT = ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = DefaultAsyncDataValue,
DefaultT = undefined,
> = Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'getCachedData'> & {
/**
* The client-side max age in seconds.
Expand Down Expand Up @@ -118,7 +114,7 @@ export function useCachedAsyncData<
| (NuxtErrorDataT extends Error | NuxtError
? NuxtErrorDataT
: NuxtError<NuxtErrorDataT>)
| DefaultAsyncDataErrorValue
| undefined
>

export function useCachedAsyncData<
Expand All @@ -136,7 +132,7 @@ export function useCachedAsyncData<
| (NuxtErrorDataT extends Error | NuxtError
? NuxtErrorDataT
: NuxtError<NuxtErrorDataT>)
| DefaultAsyncDataErrorValue
| undefined
>

/**
Expand Down Expand Up @@ -172,7 +168,7 @@ export function useCachedAsyncData<
| (NuxtErrorDataT extends Error | NuxtError
? NuxtErrorDataT
: NuxtError<NuxtErrorDataT>)
| DefaultAsyncDataErrorValue
| undefined
> {
const reactiveKey = computed(() => toValue(_key))

Expand Down
12 changes: 6 additions & 6 deletions src/runtime/composables/useDataCache.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { H3Event } from 'h3'
import { logger } from '../helpers/multi-cache-logger'
import type { DataCacheCallbackContext } from '../types'
import {
useDataCache as serverUseDataCache,
type UseDataCacheOptions,
} from './../server/utils/useDataCache'
import { useNuxtApp } from '#imports'
import { debug, isServer } from '#nuxt-multi-cache/config'
import {
useDataCacheImplementation,
type UseDataCacheOptions,
} from '../shared/useDataCache'

export async function useDataCache<T>(
key: string,
Expand All @@ -25,7 +25,7 @@ export async function useDataCache<T>(
return dummy
}

const event = providedEvent || useNuxtApp().ssrContext?.event
const event = providedEvent ?? useNuxtApp().ssrContext?.event

if (!event) {
if (debug) {
Expand All @@ -38,5 +38,5 @@ export async function useDataCache<T>(
return Promise.resolve(dummy)
}

return serverUseDataCache(key, event, options)
return useDataCacheImplementation(key, event, options)
}
6 changes: 3 additions & 3 deletions src/runtime/composables/useDataCacheCallback.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { H3Event } from 'h3'
import type { UseDataCacheCallbackCallback } from '../server/utils/useDataCacheCallback'
import { useDataCacheCallback as serverUseDataCacheCallback } from '../server/utils/useDataCacheCallback'
import { useNuxtApp } from '#imports'
import { logger } from '../helpers/multi-cache-logger'
import { debug, isServer } from '#nuxt-multi-cache/config'
import type { UseDataCacheOptions } from '../server/utils/useDataCache'
import type { UseDataCacheOptions } from '../shared/useDataCache'
import { useDataCacheCallbackImplementation } from '../shared/useDataCacheCallback'

export async function useDataCacheCallback<T>(
key: string,
Expand All @@ -16,7 +16,7 @@ export async function useDataCacheCallback<T>(
const event = providedEvent || useNuxtApp().ssrContext?.event

if (event) {
return serverUseDataCacheCallback(key, cb, event, options)
return useDataCacheCallbackImplementation(key, cb, event, options)
} else {
if (debug) {
logger.warn(
Expand Down
16 changes: 12 additions & 4 deletions src/runtime/helpers/bubbleCacheability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ import { cdnEnabled, routeCacheEnabled } from '#nuxt-multi-cache/config'
import type { BubbleCacheability, CacheableItemInterface } from '../types'
import type { H3Event } from 'h3'
import { useRouteCache } from './../server/utils/useRouteCache'
import { useCDNHeaders } from './../server/utils/useCDNHeaders'
import type { CacheabilityInterface } from './CacheabilityInterface'
import { useCDNHeadersImplementation } from '../shared/useCDNHeaders'

export function bubbleCacheability(
item: CacheableItemInterface | CacheabilityInterface,
event: H3Event,
value?: BubbleCacheability,
): void {
if (routeCacheEnabled && (value === true || value === 'route')) {
if (
routeCacheEnabled &&
(value === true || value === 'route') &&
event.context.multiCacheApp?.config.route
) {
useRouteCache((route) => {
if ('getMaxAge' in item) {
route.mergeFromCacheability(item)
Expand All @@ -20,8 +24,12 @@ export function bubbleCacheability(
}, event)
}

if (cdnEnabled && (value === true || value === 'cdn')) {
useCDNHeaders((cdn) => {
if (
cdnEnabled &&
(value === true || value === 'cdn') &&
event.context.multiCacheApp?.config.cdn
) {
useCDNHeadersImplementation((cdn) => {
if ('getMaxAge' in item) {
cdn.mergeFromCacheability(item)
} else {
Expand Down
1 change: 1 addition & 0 deletions src/runtime/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const SERVER_REQUEST_HEADER = 'x-nuxt-multi-cache-request'
export const ROUTE_CACHE_TAGS_HEADER = 'x-nuxt-multi-cache-cache-tags'
export const TEST_MODE_DATE_OVERRIDE_HEADER = 'x-nuxt-multi-cache-date-override'
30 changes: 27 additions & 3 deletions src/runtime/helpers/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { type H3Event, getRequestURL } from 'h3'
import type { MultiCacheInstances } from './../types'
import type { CacheTagRegistry } from './../types/CacheTagRegistry'
import type { NuxtMultiCacheRouteCacheHelper } from './RouteCacheHelper'
import { isServer } from '#nuxt-multi-cache/config'
import { isServer, isTestMode } from '#nuxt-multi-cache/config'
import { getRequestHeader } from 'h3'
import { SERVER_REQUEST_HEADER } from './constants'
import {
SERVER_REQUEST_HEADER,
TEST_MODE_DATE_OVERRIDE_HEADER,
} from './constants'
import { toTimestamp } from './maxAge'

export const MULTI_CACHE_CONTEXT_KEY = 'multiCacheApp'
Expand Down Expand Up @@ -98,10 +101,31 @@ export async function getCacheKeyWithPrefix(
return prefix ? `${prefix}--${cacheKey}` : cacheKey
}

function determineCurrentTime(event: H3Event): Date {
// Allow overriding the "current time" in test mode.
if (isTestMode) {
const override = getRequestHeader(event, TEST_MODE_DATE_OVERRIDE_HEADER)
if (override) {
const date = new Date(override)
if (Number.isNaN(date.getTime())) {
throw new Error(
`Invalid date provided during test mode in "${TEST_MODE_DATE_OVERRIDE_HEADER}" header.`,
)
}

return date
}
}

return new Date()
}

export function getRequestTimestamp(event: H3Event): number {
event.context ||= {}
event.context.multiCache ||= {}
event.context.multiCache.requestTimestamp ||= toTimestamp(new Date())
event.context.multiCache.requestTimestamp ||= toTimestamp(
determineCurrentTime(event),
)
return event.context.multiCache.requestTimestamp
}

Expand Down
Loading