Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: unique api calls per build #1705

Merged
merged 3 commits into from
Dec 1, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ export default defineNuxtModule<ModuleOptions>({
const { resolve } = createResolver(import.meta.url)
const resolveRuntimeModule = (path: string) => resolveModule(path, { paths: resolve('./runtime') })

// Disable cache in dev mode
const buildIntegrity = nuxt.options.dev ? undefined : Date.now()

if (options.base) {
logger.warn('content.base is deprecated. Use content.api.baseURL instead.')
options.api.baseURL = withLeadingSlash(joinURL('api', options.base))
Expand Down Expand Up @@ -326,7 +329,7 @@ export default defineNuxtModule<ModuleOptions>({
)

if (!nuxt.options.dev) {
nitroConfig.prerender.routes.unshift(`${options.api.baseURL}/cache.json`)
nitroConfig.prerender.routes.unshift(`${options.api.baseURL}/cache.${buildIntegrity}.json`)
}

// Register source storages
Expand Down Expand Up @@ -583,10 +586,9 @@ export default defineNuxtModule<ModuleOptions>({
contentContext.markdown = processMarkdownOptions(contentContext.markdown)

nuxt.options.runtimeConfig.public.content = defu(nuxt.options.runtimeConfig.public.content, {
integrity: buildIntegrity,
clientDB: {
isSPA: options.experimental.clientDB && nuxt.options.ssr === false,
// Disable cache in dev mode
integrity: nuxt.options.dev ? undefined : Date.now()
isSPA: options.experimental.clientDB && nuxt.options.ssr === false
},
api: {
baseURL: options.api.baseURL
Expand Down
28 changes: 14 additions & 14 deletions src/runtime/composables/client-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createQuery } from '../query/query'
import type { NavItem, ParsedContent, ParsedContentMeta, QueryBuilderParams } from '../types'
import { createNav } from '../server/navigation'

const withContentBase = url => withBase(url, useRuntimeConfig().public.content.api.baseURL)
const withContentBase = (url: string) => withBase(url, useRuntimeConfig().public.content.api.baseURL)

export const contentStorage = prefixStorage(createStorage({ driver: memoryDriver() }), '@content')

Expand Down Expand Up @@ -49,16 +49,16 @@ export function createDB (storage: Storage) {
}
}

let contentDatabase
let contentDatabaseInitPromise
let contentDatabase: ReturnType<typeof createDB> | null = null
let contentDatabaseInitPromise: ReturnType<typeof initContentDatabase> | null = null
export async function useContentDatabase () {
if (contentDatabaseInitPromise) {
await contentDatabaseInitPromise
} else if (!contentDatabase) {
contentDatabaseInitPromise = initContentDatabase()
contentDatabase = await contentDatabaseInitPromise
}
return contentDatabase
return contentDatabase!
}

/**
Expand All @@ -68,20 +68,20 @@ export async function useContentDatabase () {
*/
async function initContentDatabase () {
const nuxtApp = useNuxtApp()
const { clientDB } = useRuntimeConfig().public.content
const { content } = useRuntimeConfig().public

const _contentDatabase = createDB(contentStorage)
const integrity = await _contentDatabase.storage.getItem('integrity')
if (clientDB.integrity !== +integrity) {
const { contents, navigation } = await $fetch(withContentBase('cache.json')) as any
if (content.integrity !== +(integrity || 0)) {
const { contents, navigation } = await $fetch(withContentBase(`cache.${content.integrity}.json`)) as any

await Promise.all(
contents.map(content => _contentDatabase.storage.setItem(`cache:${content._id}`, content))
contents.map((content: ParsedContent) => _contentDatabase.storage.setItem(`cache:${content._id}`, content))
)

await _contentDatabase.storage.setItem('navigation', navigation)

await _contentDatabase.storage.setItem('integrity', clientDB.integrity)
await _contentDatabase.storage.setItem('integrity', content.integrity)
}

// call `content:storage` hook to allow plugins to fill storage
Expand All @@ -91,11 +91,11 @@ async function initContentDatabase () {
return _contentDatabase
}

export async function generateNavigation (query): Promise<Array<NavItem>> {
export async function generateNavigation (query?: QueryBuilderParams): Promise<Array<NavItem>> {
const db = await useContentDatabase()

if (!getPreview() && Object.keys(query || {}).length === 0) {
return db.storage.getItem('navigation')
return db.storage.getItem('navigation') as Promise<Array<NavItem>>
}

const contents = await db.query(query)
Expand All @@ -116,11 +116,11 @@ export async function generateNavigation (query): Promise<Array<NavItem>> {

const dirConfigs = await db.query().where({ _path: /\/_dir$/i, _partial: true }).find()

const configs = dirConfigs.reduce((configs, conf) => {
if (conf.title.toLowerCase() === 'dir') {
const configs = dirConfigs.reduce((configs, conf: ParsedContent) => {
if (conf.title?.toLowerCase() === 'dir') {
conf.title = undefined
}
const key = conf._path.split('/').slice(0, -1).join('/') || '/'
const key = conf._path!.split('/').slice(0, -1).join('/') || '/'
configs[key] = {
...conf,
// Extract meta from body. (non MD files)
Expand Down
14 changes: 7 additions & 7 deletions src/runtime/composables/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { hash } from 'ohash'
import { useCookie } from '#app'
import { useCookie, useRuntimeConfig } from '#app'
import type { NavItem, QueryBuilder, QueryBuilderParams } from '../types'
import { jsonStringify } from '../utils/json'
import { addPrerenderPath, shouldUseClientDB, withContentBase } from './utils'

export const fetchContentNavigation = async (queryBuilder?: QueryBuilder | QueryBuilderParams): Promise<Array<NavItem>> => {
let params = queryBuilder
const { content } = useRuntimeConfig().public

// When params is an instance of QueryBuilder then we need to pick the params explicitly
if (typeof params?.params === 'function') { params = params.params() }
const params: QueryBuilderParams = typeof queryBuilder?.params === 'function' ? queryBuilder.params() : queryBuilder

const apiPath = withContentBase(params ? `/navigation/${hash(params)}.json` : '/navigation')
const apiPath = withContentBase(params ? `/navigation/${hash(params)}.${content.integerity}.json` : '/navigation')

// Add `prefetch` to `<head>` in production
if (!process.dev && process.server) {
Expand All @@ -19,10 +19,10 @@ export const fetchContentNavigation = async (queryBuilder?: QueryBuilder | Query

if (shouldUseClientDB()) {
const generateNavigation = await import('./client-db').then(m => m.generateNavigation)
return generateNavigation(params || {})
return generateNavigation(params)
}

const data = await $fetch(apiPath, {
const data = await $fetch<NavItem[]>(apiPath, {
method: 'GET',
responseType: 'json',
params: {
Expand All @@ -33,7 +33,7 @@ export const fetchContentNavigation = async (queryBuilder?: QueryBuilder | Query

// On SSG, all url are redirected to `404.html` when not found, so we need to check the content type
// to know if the response is a valid JSON or not
if (typeof data === 'string' && data.startsWith('<!DOCTYPE html>')) {
if (typeof data === 'string' && (data as string).startsWith('<!DOCTYPE html>')) {
throw new Error('Not found')
}

Expand Down
7 changes: 4 additions & 3 deletions src/runtime/composables/query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { joinURL, withLeadingSlash, withoutTrailingSlash } from 'ufo'
import { hash } from 'ohash'
import { useCookie } from '#app'
import { useCookie, useRuntimeConfig } from '#app'
import { createQuery } from '../query/query'
import type { ParsedContent, QueryBuilder, QueryBuilderParams } from '../types'
import { jsonStringify } from '../utils/json'
Expand All @@ -10,6 +10,7 @@ import { addPrerenderPath, shouldUseClientDB, withContentBase } from './utils'
* Query fetcher
*/
export const createQueryFetch = <T = ParsedContent>(path?: string) => async (query: QueryBuilder<T>) => {
const { content } = useRuntimeConfig().public
if (path) {
if (query.params().first && (query.params().where || []).length === 0) {
// If query contains `path` and does not contain any `where` condition
Expand All @@ -26,7 +27,7 @@ export const createQueryFetch = <T = ParsedContent>(path?: string) => async (que

const params = query.params()

const apiPath = withContentBase(process.dev ? '/query' : `/query/${hash(params)}.json`)
const apiPath = withContentBase(process.dev ? '/query' : `/query/${hash(params)}.${content.integerity}.json`)

// Prefetch the query
if (!process.dev && process.server) {
Expand All @@ -35,7 +36,7 @@ export const createQueryFetch = <T = ParsedContent>(path?: string) => async (que

if (shouldUseClientDB()) {
const db = await import('./client-db').then(m => m.useContentDatabase())
return db.fetch(query)
return db.fetch(query as QueryBuilder<ParsedContent>)
}

const data = await $fetch(apiPath as any, {
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/server/api/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ const resolveTheme = (theme: string | Record<string, string>): Record<string, Th
}

return Object.entries(theme).reduce((acc, [key, value]) => {
acc[key] = BUNDLED_THEMES.find(t => t === value)
acc[key] = BUNDLED_THEMES.find(t => t === value)!
return acc
}, {})
}, {} as Record<string, Theme>)
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/runtime/server/api/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineEventHandler } from 'h3'
import { cacheStorage, serverQueryContent } from '../storage'
import { createNav } from '../navigation'
import { ParsedContentMeta } from '../../types'
import { ParsedContent, ParsedContentMeta } from '../../types'
import { getContentQuery } from '../../utils/query'
import { isPreview } from '../preview'

Expand Down Expand Up @@ -34,11 +34,11 @@ export default defineEventHandler(async (event) => {

const dirConfigs = await serverQueryContent(event).where({ _path: /\/_dir$/i, _partial: true }).find()

const configs = dirConfigs.reduce((configs, conf) => {
if (conf.title.toLowerCase() === 'dir') {
const configs = dirConfigs.reduce((configs, conf: ParsedContent) => {
if (conf.title?.toLowerCase() === 'dir') {
conf.title = undefined
}
const key = conf._path.split('/').slice(0, -1).join('/') || '/'
const key = conf._path!.split('/').slice(0, -1).join('/') || '/'
configs[key] = {
...conf,
// Extract meta from body. (non MD files)
Expand Down