Skip to content

Commit

Permalink
Refactor server side runtime configuration to not rely on nuxt.config
Browse files Browse the repository at this point in the history
  • Loading branch information
dulnan committed Jan 22, 2023
1 parent 063e455 commit a4c3597
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 265 deletions.
6 changes: 6 additions & 0 deletions playground/app/multiCache.serverOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineMultiCacheOptions } from './../../src/module'

export default defineMultiCacheOptions({
data: {},
component: {},
})
75 changes: 74 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fileURLToPath } from 'url'
import { existsSync } from 'node:fs'
import type { NuxtModule } from '@nuxt/schema'
import { defu } from 'defu'
import {
Expand All @@ -7,8 +8,9 @@ import {
defineNuxtModule,
addComponent,
addImports,
addTemplate,
} from '@nuxt/kit'
import { NuxtMultiCacheOptions } from './runtime/types'
import { MutliCacheServerOptions, NuxtMultiCacheOptions } from './runtime/types'
import {
defaultOptions,
DEFAULT_CDN_CONTROL_HEADER,
Expand All @@ -19,6 +21,24 @@ import {
export type ModuleOptions = NuxtMultiCacheOptions
export type ModuleHooks = {}

export const fileExists = (
path?: string,
extensions = ['js', 'ts'],
): string | null => {
if (!path) {
return null
} else if (existsSync(path)) {
// If path already contains/forces the extension
return path
}

const extension = extensions.find((extension) =>
existsSync(`${path}.${extension}`),
)

return extension ? `${path}.${extension}` : null
}

export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'nuxt-multi-cache',
Expand All @@ -43,6 +63,20 @@ export default defineNuxtModule<ModuleOptions>({
options.cdn?.cacheControlHeader || DEFAULT_CDN_CONTROL_HEADER,
cacheTagHeader: options.cdn?.cacheTagHeader || DEFAULT_CDN_TAG_HEADER,
},
component: !!options.component?.enabled,
data: !!options.data?.enabled,
route: !!options.data?.enabled,
api: {
enabled: !!options.api?.enabled,
prefix: options.api?.prefix || '',
cacheTagInvalidationDelay: options.api
?.cacheTagInvalidationDelay as number,
authorizationToken:
typeof options.api?.authorization === 'string'
? options.api.authorization
: '',
authorizationDisabled: options.api?.authorization === false,
},
}

// @TODO: Why is this needed?!
Expand Down Expand Up @@ -120,6 +154,41 @@ export default defineNuxtModule<ModuleOptions>({
})
}

// Shamelessly copied and adapted from:
// https://github.com/nuxt-modules/prismic/blob/fd90dc9acaa474f79b8831db5b8f46a9a9f039ca/src/module.ts#L55
// Creates the template with runtime server configuration.
const extensions = ['js', 'mjs', 'ts']
const resolvedPath = '~/app/multiCache.serverOptions'
.replace(/^(~~|@@)/, nuxt.options.rootDir)
.replace(/^(~|@)/, nuxt.options.srcDir)

const template = (() => {
const resolvedFilename = `multiCache.serverOptions.ts`

const maybeUserFile = fileExists(resolvedPath, extensions)

if (maybeUserFile) {
return addTemplate({
filename: resolvedFilename,
write: true,
getContents: () => `export { default } from '${resolvedPath}'`,
})
}

// Else provide `undefined` fallback
return addTemplate({
filename: resolvedFilename,
write: true,
getContents: () => 'export default {}',
})
})()

nuxt.options.nitro.externals = nuxt.options.nitro.externals || {}
nuxt.options.nitro.externals.inline =
nuxt.options.nitro.externals.inline || []
nuxt.options.nitro.externals.inline.push(template.dst)
nuxt.options.alias['#multi-cache-server-options'] = template.dst

// Add cache management API if enabled.
if (options.api?.enabled) {
// Prefix is defined in default config.
Expand Down Expand Up @@ -157,3 +226,7 @@ export default defineNuxtModule<ModuleOptions>({
}
},
}) as NuxtModule<ModuleOptions>

export function defineMultiCacheOptions(options: MutliCacheServerOptions) {
return options
}
52 changes: 36 additions & 16 deletions src/runtime/serverHandler/api/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import type { H3Event } from 'h3'
import { createError, getHeader } from 'h3'
import type { Storage } from 'unstorage'
import type {
NuxtMultiCacheOptions,
MultiCacheRuntimeConfig,
MutliCacheServerOptions,
NuxtMultiCacheSSRContext,
} from './../../../types'
import { getModuleConfig } from './../../helpers'
import { getMultiCacheContext } from './../../../helpers/server'
import { useRuntimeConfig } from '#imports'
import serverOptions from '#multi-cache-server-options'

const runtimeConfig = useRuntimeConfig()

const AUTH_HEADER = 'x-nuxt-multi-cache-token'

Expand All @@ -33,35 +37,51 @@ export function getCacheInstance(event: H3Event): Storage {

/**
* Check the authorization for API endpoints.
*
* Throws an error if authorization failed.
*/
export async function checkAuth(
event: H3Event,
providedModuleConfig?: NuxtMultiCacheOptions,
providedRuntimeConfig?: MultiCacheRuntimeConfig,
providedServerOptions?: MutliCacheServerOptions,
) {
const moduleConfig = providedModuleConfig || (await getModuleConfig())
const authorization = moduleConfig?.api?.authorization
const { authorizationDisabled, authorizationToken } =
(providedRuntimeConfig || runtimeConfig.multiCache).api || {}

// Auth is disabled if it's explicity set to false.
if (authorization === false) {
// Allow if authorization is explicitly disabled.
if (authorizationDisabled) {
return
} else if (typeof authorization === 'function') {
const result = await authorization(event)
if (result) {
return
}
} else if (typeof authorization === 'string') {
// Check authorization.
}

// Check authorization using token.
if (authorizationToken) {
const headerToken = getHeader(event, AUTH_HEADER)
if (headerToken === authorization) {
if (headerToken === authorizationToken) {
return
}
} else {
// Unauthorized.
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized',
})
}

const authorization = (providedServerOptions || serverOptions).authorization

// At this stage if this method is missing, we throw an error to indicate
// that the module is not configured properly.
if (!authorization) {
throw createError({
statusCode: 500,
statusMessage: 'No authorization configuration option provided.',
})
}

const result = await authorization(event)
if (result) {
return
}

// Unauthorized.
throw createError({
statusCode: 401,
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/serverHandler/api/purgeTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { readBody, defineEventHandler, createError } from 'h3'
import { getMultiCacheContext } from './../../helpers/server'
import { DEFAULT_CACHE_TAG_INVALIDATION_DELAY } from './../../settings'
import type { NuxtMultiCacheSSRContext } from './../../types'
import { getModuleConfig } from './../helpers'
import { checkAuth } from './helpers'
import { useRuntimeConfig } from '#imports'

/**
* Get the tags to be purged from the request.
Expand Down Expand Up @@ -150,8 +150,8 @@ export default defineEventHandler(async (event) => {

if (!invalidator.cacheContext) {
invalidator.cacheContext = getMultiCacheContext(event)
const moduleConfig = await getModuleConfig()
invalidator.setDelay(moduleConfig.api?.cacheTagInvalidationDelay)
const { multiCache } = useRuntimeConfig()
invalidator.setDelay(multiCache.api.cacheTagInvalidationDelay)
}

invalidator.add(tags)
Expand Down
51 changes: 19 additions & 32 deletions src/runtime/serverHandler/cacheContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,33 @@ import {
MULTI_CACHE_ROUTE_CONTEXT_KEY,
} from './../helpers/server'
import { NuxtMultiCacheRouteCacheHelper } from './../helpers/RouteCacheHelper'
import { getModuleConfig } from './helpers'
import { loadCacheContext } from './helpers/storage'

// Cache if the enabledForRequest method is provided.
let hasEnabledForRequestMethod: null | boolean = null
import serverOptions from '#multi-cache-server-options'

/**
* Determine if the cache context should be added to the request.
*
* If the enabledForRequest method is provided, it is called with the H3 event.
* If the method returns false, the cache context is not added.
* Add the cache context singleton to the current request.
*/
export async function shouldAddCacheContext(event: H3Event): Promise<boolean> {
if (hasEnabledForRequestMethod === false) {
return true
}

const moduleConfig = await getModuleConfig()
if (!moduleConfig.enabledForRequest) {
hasEnabledForRequestMethod = false
return true
}

return !!(await moduleConfig.enabledForRequest(event))
export function addCacheContext(event: H3Event) {
// Init cache context if not already done.
// Returns a single promise so that we don't initialize it multiple times
// when multiple requests come in.
const cacheContext = loadCacheContext()

// Add the cache context object to the SSR context object.
event.context[MULTI_CACHE_CONTEXT_KEY] = cacheContext

// Add the route cache helper.
event.context[MULTI_CACHE_ROUTE_CONTEXT_KEY] =
new NuxtMultiCacheRouteCacheHelper()
}

export default defineEventHandler(async (event) => {
const shouldAdd = await shouldAddCacheContext(event)
if (!serverOptions.enabledForRequest) {
return addCacheContext(event)
}

const shouldAdd = await serverOptions.enabledForRequest(event)
if (shouldAdd) {
// Init cache context if not already done.
// Returns a single promise so that we don't initialize it multiple times
// when multiple requests come in.
const cacheContext = await loadCacheContext()

// Add the cache context object to the SSR context object.
event.context[MULTI_CACHE_CONTEXT_KEY] = cacheContext

// Add the route cache helper.
event.context[MULTI_CACHE_ROUTE_CONTEXT_KEY] =
new NuxtMultiCacheRouteCacheHelper()
addCacheContext(event)
}
})
31 changes: 0 additions & 31 deletions src/runtime/serverHandler/helpers/index.ts

This file was deleted.

42 changes: 17 additions & 25 deletions src/runtime/serverHandler/helpers/storage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { createStorage } from 'unstorage'
import type { NuxtMultiCacheSSRContext } from './../../types'
import { getModuleConfig } from './../helpers'
import { useRuntimeConfig } from '#imports'
import serverOptions from '#multi-cache-server-options'

// Store a single promise to prevent initializing caches multiple times.
let promise: Promise<NuxtMultiCacheSSRContext> | null = null
const runtimeConfig = useRuntimeConfig()

const cacheContext: NuxtMultiCacheSSRContext = {}
// Initialize all enabled caches. Explicit initialization because some
// caches might need additional configuration options and/or checks.
if (runtimeConfig.multiCache.component) {
cacheContext.component = createStorage(serverOptions.component?.storage)
}
if (runtimeConfig.multiCache.data) {
cacheContext.data = createStorage(serverOptions.data?.storage)
}
if (runtimeConfig.multiCache.route) {
cacheContext.route = createStorage(serverOptions.route?.storage)
}

/**
* Method to initialize the caches.
Expand All @@ -12,26 +25,5 @@ let promise: Promise<NuxtMultiCacheSSRContext> | null = null
* afterwards.
*/
export function loadCacheContext() {
if (promise) {
return promise
}
promise = getModuleConfig().then((config) => {
const cacheContext: NuxtMultiCacheSSRContext = {}

// Initialize all enabled caches. Explicit initialization because some
// caches might need additional configuration options and/or checks.
if (config.component && config.component.enabled) {
cacheContext.component = createStorage(config.component.storage)
}
if (config.data && config.data.enabled) {
cacheContext.data = createStorage(config.data.storage)
}
if (config.route && config.route.enabled) {
cacheContext.route = createStorage(config.route.storage)
}

return cacheContext
})

return promise
return cacheContext
}
Loading

0 comments on commit a4c3597

Please sign in to comment.