Skip to content

Commit

Permalink
feat: refactor content storages (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
farnabaz authored Nov 10, 2021
1 parent 2f6456c commit c0a39f6
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 94 deletions.
9 changes: 6 additions & 3 deletions src/module.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function setupDevTarget(options: any, nuxt: Nuxt) {
storage.mount(
'content',
fsDriver({
base: resolve(nuxt.options.srcDir, 'content'),
base: resolve(nuxt.options.rootDir, 'content'),
ignore: ignoreList
})
)
Expand All @@ -39,7 +39,10 @@ export default function setupDevTarget(options: any, nuxt: Nuxt) {
storage.watch(
createDebounceContentWatcher(async (event: WatchEvent, key: string) => {
// call reload api: clear database and navigation
await fetch(joinURL(url, 'api', options.apiBase, 'reload', key))
await fetch(joinURL(url, 'api', options.apiBase, 'reload'), {
method: 'POST',
body: JSON.stringify({ event, key })
})

/**
* Broadcast a message to the server to refresh the page
Expand All @@ -60,7 +63,7 @@ function createDebounceContentWatcher(callback: WatchCallback) {
return (event: WatchEvent, key: string) => {
if (key.endsWith('.md') && ['content'].some(mount => key.startsWith(mount))) {
handleEvent(event, key)
logger.info(`${key} ${event}`)
logger.info(`[DOCUS]: ${key} ${event}`)
}
}
}
44 changes: 29 additions & 15 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default defineNuxtModule((nuxt: Nuxt) => ({
configKey: 'content',
defaults: {
apiBase: '_docus',
dirs: ['content'],
watch: nuxt.options.dev,
database: {
provider: 'local'
Expand Down Expand Up @@ -63,13 +64,31 @@ export default defineNuxtModule((nuxt: Nuxt) => ({

// Register API
nuxt.hook('nitro:context', (ctx: NitroContext) => {
ctx.assets.dirs.content = {
dir: resolve(nuxt.options.rootDir, 'content'),
meta: true
if (ctx.preset === 'dev') {
for (const dir of options.dirs) {
ctx.storage.mounts[`docus:source:${dir}`] = {
driver: 'fs',
driverOptions: {
base: resolve(nuxt.options.rootDir, dir)
}
}
}
// prefix `docus:build` with assets to match production keys
ctx.storage.mounts['assets:docus:build'] = {
driver: 'fs',
driverOptions: {
base: resolve(nuxt.options.buildDir, 'docus/build')
}
}
} else {
ctx.assets.dirs['docus:build'] = {
dir: resolve(nuxt.options.buildDir, 'docus/build'),
meta: true
}
}
// Set preview storage as memory if not set
if (!ctx.storage.mounts.preview) {
ctx.storage.mounts.preview = {
if (!ctx.storage.mounts['docus:preview']) {
ctx.storage.mounts['docus:preview'] = {
driver: 'memory'
}
}
Expand All @@ -94,16 +113,11 @@ export default defineNuxtModule((nuxt: Nuxt) => ({
addPlugin(resolveTemplateDir('content'))

// Add Docus context template
for (const target of ['server', 'client']) {
addTemplate({
src: resolveModule('./context', { paths: templateDir }),
filename: `docus/context.${target}.mjs`,
options: {
target,
context: docusContext
}
})
}
addTemplate({
src: resolveModule('./context', { paths: templateDir }),
filename: 'docus/context.mjs',
options: docusContext
})

// Setup dev target
if (nuxt.options.dev) {
Expand Down
11 changes: 1 addition & 10 deletions src/runtime/context.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { getContext } from 'unctx'
import type { DocusContext } from 'types'
// TODO: dynamically load server/client context
// @ts-ignore
import context from '#build/docus/context.client.mjs'

/**
const client = typeof window !== 'undefined'
const context = client // @ts-ignore
? import('#build/docus/context.client.mjs').then(c => c.default || c) // @ts-ignore
: import('#build/docus/context.server.mjs').then(c => c.default || c)
*/
import context from '#build/docus/context.mjs'

const ctx = getContext<DocusContext>('docus_context')

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export async function generateNavigation(contents?: any[]) {

if (!contents) {
// Query pages
contents = await (globalThis as any).$fetch('/api/_docus/list/content').then(({ items }: any) => items)
contents = await (globalThis as any).$fetch('/api/_docus/list').then(({ items }: any) => items)
}

// sort items
Expand Down
20 changes: 17 additions & 3 deletions src/runtime/server/api/reload.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
// import { clearDatabase } from '../../database'
import { IncomingMessage } from 'http'
import { useBody } from 'h3'
import { buildContent, buildStorage } from '../content'

interface Body {
key: string
event: 'remove' | 'update'
}

/**
* Reload the database.
*/
export default () => {
// clearDatabase()
export default async (req: IncomingMessage) => {
const { key, event } = await useBody<Body>(req)

if (event === 'remove') {
buildStorage.removeItem(key)
} else if (await buildStorage.hasItem(key)) {
// Rebuild/Reload content
await buildContent(key)
}

return true
}
159 changes: 101 additions & 58 deletions src/runtime/server/content.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { prefixStorage } from 'unstorage'
import { prefixStorage as unstoragePrefixStorage } from 'unstorage'
import type { Storage } from 'unstorage'
import micromatch from 'micromatch'
import { getTransformer } from '../transformers'
import { createDatabase } from '../database'
Expand All @@ -7,86 +8,83 @@ import { useDocusContext } from '../context'
import { cachify } from './utils/cache'
// @ts-ignore
import { storage } from '#storage'
import type { MDCRoot } from 'types'

export const assetsStorage = prefixStorage(storage, 'assets')
export const previewStorage = prefixStorage(storage, 'preview')
interface DocusContent {
body: MDCRoot
meta: Record<string, any>
}

const isProduction = process.env.NODE_ENV === 'production'
const withCache = (name: string, fn: any, force = false) =>
isProduction || force ? cachify(fn, { name, swr: true, ttl: 60000, integrity: 'docus' }) : fn
// TODO: Fix in upstream
function prefixStorage(storage: Storage, base: string): Storage {
const nsStorage = unstoragePrefixStorage(storage, base)
const prefixRegex = new RegExp(`^${base}:`)
nsStorage.getKeys = (id: string = '') => {
return storage.getKeys(base + ':' + id).then(keys => keys.map(key => key.replace(prefixRegex, '')))
}
return nsStorage
}

// Remove prefix from key
const removePrefix = (key: string) => key.split(':').slice(1).join(':')
export const contentStorage = prefixStorage(storage, 'docus:source')
export const buildStorage = prefixStorage(storage, 'assets:docus:build')
export const previewStorage = prefixStorage(storage, 'docus:preview')

const getContentData = withCache('getContentData', async (id: string) => {
let body = await assetsStorage.getItem(id)
const meta = await assetsStorage.getMeta(id)
const isProduction = process.env.NODE_ENV === 'production'
const withCache = (name: string, fn: any) =>
isProduction ? cachify(fn, { name, swr: true, ttl: 60000, integrity: 'docus' }) : fn

/**
* Unstorage tries to parse content as JSON
* The following logic will ensure that the content is always a string
*/
// Stringify objects
if (typeof body === 'object') {
body = JSON.stringify(typeof (body as any).default !== 'undefined' ? (body as any).default : body)
}
// Ensure body is a string
if (typeof body !== 'string') {
body = body + ''
}
return {
body,
meta
// Get data from storage
async function getData(id: string, previewKey?: string) {
let body
let meta
// Use updated data if it exists
if (previewKey) {
body = await previewStorage.getItem(`${previewKey}:${id}`)
meta = await previewStorage.getMeta(`${previewKey}:${id}`)
}
})

async function getPreviewData(id: string, previewKey: string) {
let body = await previewStorage.getItem(`${previewKey}:${id}`)
const meta = await previewStorage.getMeta(`${previewKey}:${id}`)
// Fallback to original data
if (!body) {
body = await contentStorage.getItem(id)
meta = await contentStorage.getMeta(id)
}

/**
* Unstorage tries to parse content as JSON
* The following logic will ensure that the content is always a string
*/
// Stringify objects
if (typeof body === 'object') {
if (body && typeof body === 'object') {
body = JSON.stringify(typeof (body as any).default !== 'undefined' ? (body as any).default : body)
}

// Ensure body is a string
if (typeof body !== 'string') {
body = body + ''
}

return {
body,
body: body as string,
meta
}
}

// Get data from storage
function getData(id: string, previewKey?: string) {
// Use updated data if it exists
if (previewKey) {
return getPreviewData(id, previewKey)
}

return getContentData(id)
}
const getContentKeys = async (id?: string) => {
let keys: string[] = await buildStorage.getKeys(id)

const getContentKeys = withCache('getContentKeys', async (id?: string) => {
let keys: string[] = await assetsStorage.getKeys(id)
// Remove assets prefix
keys = keys.map(removePrefix)
if (keys.length === 0) {
keys = await contentStorage.getKeys(id)

// filter out ignored contents
const context = useDocusContext()
keys = micromatch.not(keys, context?.ignoreList || [])
// filter out ignored contents
const context = useDocusContext()
keys = micromatch.not(keys, context?.ignoreList || [])
}

return keys
})
}

const getPreviewKeys = async (id?: string, previewKey?: string) => {
let keys = (await previewStorage.getKeys(id ? `${previewKey}:${id}` : previewKey)).map((key: string) =>
removePrefix(key.replace(`${previewKey}:`, ''))
key.replace(`${previewKey}:`, '')
)

// filter out ignored contents
Expand All @@ -96,6 +94,13 @@ const getPreviewKeys = async (id?: string, previewKey?: string) => {
return keys
}

/**
* Find list of content keys
*
* @param id Base if for lookup
* @param previewKey Preview ID
* @returns List of content keys in given base path
*/
async function getKeys(id?: string, previewKey?: string) {
let keys: string[] = await getContentKeys(id)

Expand All @@ -111,24 +116,55 @@ async function getKeys(id?: string, previewKey?: string) {
return keys
}

// Get content
const transform = withCache('transform', (id: string, body: string) => getTransformer(id)(id, body), true)
export async function getContent(id: string, previewKey?: string) {
/**
* Read content form source file, generate and cache the content
*
* @param id Content ID
* @param previewKey Preview id
* @returns Transformed content
*/
export async function buildContent(id: string, previewKey?: string): Promise<DocusContent> {
const data = await getData(id, previewKey)
if (typeof data.body === 'undefined' || data.body === null) {
throw new Error(`Content not found: ${id}`)
}
const transformResult = await transform(id, data.body as any)
return {
const transformResult = await getTransformer(id)(id, data.body)
const content = {
meta: {
...data.meta,
...transformResult.meta
},
body: transformResult.body
}
if (!previewKey) {
await buildStorage.setItem(id, content)
}
return content
}

/**
* Return transformed conntent if it exists in cache or build it on demand
*
* @param id Content ID
* @param previewKey Preview ID
* @returns Transformed content
*/
export async function getContent(id: string, previewKey?: string): Promise<DocusContent> {
let content = !previewKey && ((await buildStorage.getItem(id)) as DocusContent)

if (!content) {
content = await buildContent(id, previewKey)
}
return content
}

// Get list of content
/**
* Find list of contents
*
* @param id Base if for lookup
* @param previewKey Preview ID
* @returns List of content in given base path
*/
export async function getList(id?: string, previewKey?: string) {
const keys: string[] = await getKeys(id, previewKey)
return Promise.all(
Expand All @@ -142,7 +178,14 @@ export async function getList(id?: string, previewKey?: string) {
)
}

// Get content
/**
* Search content database for a given query
*
* @param to Base path for search
* @param body Search options
* @param previewKey Preview ID
* @returns Single content of list of contents that matchs the search
*/
export async function searchContent(to: string, body: any, previewKey?: string) {
const navigation = await getNavigation(previewKey)

Expand Down
Loading

0 comments on commit c0a39f6

Please sign in to comment.