diff --git a/src/core/database.ts b/src/core/database.ts index 65695b288..d2791234d 100644 --- a/src/core/database.ts +++ b/src/core/database.ts @@ -1,6 +1,6 @@ import Loki from '@lokidb/loki' -import { QueryBuilder } from './runtime/QueryBuilder' -import { useHooks } from '.' +import { QueryBuilder } from './runtime/api/QueryBuilder' +import { useHooks } from './hooks' let _db let _items diff --git a/src/core/plugin.js b/src/core/plugin.js index f671e5f25..7592a4081 100644 --- a/src/core/plugin.js +++ b/src/core/plugin.js @@ -1,7 +1,7 @@ import { joinURL, withLeadingSlash } from 'ufo' import settings from '~docus-cache/docus-settings.json' import { createDocus, <%= options.isSSG ? "QueryBuilder" : "RemoteQueryBuilder" %> } from '~docus' -/* <% if (options.watch) { %> */ import { useWebSocket } from '~docus/websocket' /* <% } %> */ +/* <% if (options.watch) { %> */ import { useWebSocket } from '~docus/api/websocket' /* <% } %> */ /* <% if (options.isSSG) { %> */ @@ -33,9 +33,12 @@ export default async function (ctx, inject) { items = db.getCollection('items') } /* <% } %> */ - - const $docus = await createDocus(ctx, settings, process.server ? ctx.ssrContext.docus.createQuery : createQuery) + const $docus = await createDocus( + ctx, + settings, + process.server ? ctx.ssrContext.docus.createQuery : createQuery + ) inject('docus', $docus) diff --git a/src/core/runtime/QueryBuilder.ts b/src/core/runtime/api/QueryBuilder.ts similarity index 100% rename from src/core/runtime/QueryBuilder.ts rename to src/core/runtime/api/QueryBuilder.ts diff --git a/src/core/runtime/RemoteQueryBuilder.ts b/src/core/runtime/api/RemoteQueryBuilder.ts similarity index 100% rename from src/core/runtime/RemoteQueryBuilder.ts rename to src/core/runtime/api/RemoteQueryBuilder.ts diff --git a/src/core/runtime/websocket.ts b/src/core/runtime/api/websocket.ts similarity index 100% rename from src/core/runtime/websocket.ts rename to src/core/runtime/api/websocket.ts diff --git a/src/core/runtime/composables/addons.ts b/src/core/runtime/composables/addons.ts new file mode 100644 index 000000000..76bedceeb --- /dev/null +++ b/src/core/runtime/composables/addons.ts @@ -0,0 +1,38 @@ +import { DocusAddonContext } from 'src/types' + +export const useDocusAddons = (context: DocusAddonContext, addons: any[]) => { + /** + * Addons context to be spread into Docus injection + */ + const addonsContext = {} + + /** + * Setup all addons + */ + const setupAddons = async () => + await Promise.all( + addons.map(async addon => { + const addonKeys = addon(context) + + Object.entries(addonKeys).forEach(([key, value]) => { + if (key === 'init') return + + const contextKeys = [Object.keys(addonsContext), ...Object.keys(context.state)] + + // eslint-disable-next-line no-console + if (contextKeys.includes(key)) console.warn(`You duplicated the key ${key} in Docus context.`) + + addonsContext[key] = value + }) + + if ((addonKeys as any)?.init) { + return await (addonKeys as any)?.init?.() + } + }) + ) + + return { + addonsContext, + setupAddons + } +} diff --git a/src/core/runtime/composables/api.ts b/src/core/runtime/composables/api.ts new file mode 100644 index 000000000..cb432bdbc --- /dev/null +++ b/src/core/runtime/composables/api.ts @@ -0,0 +1,31 @@ +import { joinURL } from 'ufo' + +export const useDocusApi = createQuery => { + function data(path: string) { + return createQuery(joinURL('/data', path), {}).fetch() + } + + function search(path: string | any, options?) { + if (typeof path !== 'string') { + options = path + path = '' + } + + return createQuery(joinURL('/pages', path), options) + } + + function page(path: string) { + return this.search(path).fetch() + } + + function findLinkBySlug(links: any[], slug: string) { + return links.find(link => link.slug === slug) + } + + return { + data, + search, + page, + findLinkBySlug + } +} diff --git a/src/core/runtime/composables/github.ts b/src/core/runtime/composables/github.ts new file mode 100644 index 000000000..f48f676f0 --- /dev/null +++ b/src/core/runtime/composables/github.ts @@ -0,0 +1,14 @@ +import { computed } from '@nuxtjs/composition-api' +import { DocusAddonContext } from 'src/types' +import { joinURL, withoutTrailingSlash } from 'ufo' + +export const useDocusGithub = ({ state }: DocusAddonContext) => { + const previewUrl = computed(() => withoutTrailingSlash(state.settings.url) + '/preview.png') + + const repoUrl = computed(() => joinURL(state.settings.github.url, state.settings.github.repo)) + + return { + previewUrl, + repoUrl + } +} diff --git a/src/core/runtime/composables/helpers.ts b/src/core/runtime/composables/helpers.ts new file mode 100644 index 000000000..8c0c6273d --- /dev/null +++ b/src/core/runtime/composables/helpers.ts @@ -0,0 +1,40 @@ +import { DocusAddonContext } from 'src/types' +import Vue from 'vue' + +export const docusInit = async ({ context, state }: DocusAddonContext, fetch: any) => { + // HotReload on development + if (process.client && process.dev) window.onNuxtReady(() => window.$nuxt.$on('content:update', fetch)) + + // Fetch on server + if (process.server) { + await fetch() + + context.beforeNuxtRender(({ nuxtState }) => (nuxtState.docus = state)) + } + + // SPA Fallback + if (process.client && !state.settings) await fetch() +} + +export const clientAsyncData = (app, $nuxt: any) => { + if (process.client) { + window.onNuxtReady((nuxt: any) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + $nuxt = nuxt + + // Workaround since in full static mode, asyncData is not called anymore + app.router.beforeEach((_: any, __: any, next: any) => { + const payload = nuxt._pagePayload || {} + + payload.data = payload.data || [] + + if (payload.data[0]?.page?.template && typeof Vue.component(payload.data[0].page.template) === 'function') { + // Preload the component on client-side navigation + Vue.component(payload.data[0].page.template) + } + + next() + }) + }) + } +} diff --git a/src/core/runtime/composables/navigation.ts b/src/core/runtime/composables/navigation.ts new file mode 100644 index 000000000..2f01c287a --- /dev/null +++ b/src/core/runtime/composables/navigation.ts @@ -0,0 +1,219 @@ +import { DocusAddonContext } from 'src/types' +import { pascalCase } from 'scule' +import { withoutTrailingSlash, withTrailingSlash } from 'ufo' +import { computed } from '@nuxtjs/composition-api' +import Vue from 'vue' + +export const useDocusNavigation = ({ $nuxt, context, state, api }: DocusAddonContext) => { + const app = context.app + + if (!state.navigation) state.navigation = {} + + // Map locales to nav + app.i18n.locales.forEach((locale: any) => (state.navigation[locale.code] = {})) + + const currentNav = computed(() => state.navigation[app.i18n.locale]) + + async function fetchNavigation() { + // TODO: Maybe remove this + // Avoid re-fetching in production + if (process.dev === false && state.navigation[app.i18n.locale].links) return + + // Get fields + const fields = [ + 'title', + 'menu', + 'menuTitle', + 'dir', + 'nav', + 'category', + 'slug', + 'version', + 'to', + 'icon', + 'description', + 'template' + ] + + // Handle draft fields if in development and enabled from UI + const draft = state.ui?.draft ? undefined : false + if (process.dev) fields.push('draft') + + // Query pages + const pages = await api + .search({ deep: true }) + .where({ language: app.i18n.locale, draft, nav: { $ne: false } }) + .only(fields) + .sortBy('position', 'asc') + .fetch() + + let depth = 0 + + const links = [] + + const getPageLink = (page: any) => ({ + slug: page.slug, + to: withoutTrailingSlash(page.to || page.slug), + menu: page.menu, + menuTitle: page.menuTitle, + template: page.template, + title: page.title, + icon: page.icon, + description: page.description, + ...page.nav + }) + + // Add each page to navigation + pages.forEach((page: any) => { + page.nav = page.nav || {} + + if (typeof page.nav === 'string') page.nav = { slot: page.nav } + + // TODO: Ignore files directly from @nuxt/content + if (page.slug.startsWith('_')) return + + // To: '/docs/guide/hello.md' -> dirs: ['docs', 'guide'] + page.dirs = withoutTrailingSlash(page.to) + .split('/') + .filter(_ => _) + + // Remove the file part (except if index.md) + if (page.slug !== '') page.dirs = page.dirs.slice(0, -1) + + if (!page.dirs.length) { + page.nav.slot = page.nav.slot || 'header' + return links.push(getPageLink(page)) + } + + let currentLinks = links + + let link = null + + page.dirs.forEach((dir: string, index: number) => { + // If children has been disabled (nav.children = false) + if (!currentLinks) return + if (index > depth) depth = index + + link = api.findLinkBySlug(currentLinks, dir) + + if (link) { + currentLinks = link.children + } else { + link = { + slug: dir, + children: [] + } + currentLinks.push(link) + currentLinks = currentLinks[currentLinks.length - 1].children + } + }) + + if (!currentLinks) return + + // If index page, merge also with parent for metadata + if (!page.slug) { + if (page.dirs.length === 1) page.nav.slot = page.nav.slot || 'header' + + Object.assign(link, getPageLink(page)) + } else { + // Push page + currentLinks.push(getPageLink(page)) + } + }) + + // Increment navDepth for files + depth++ + + // Assign to $docus + state.navigation[app.i18n.locale] = { + depth, + links + } + + // calculate categories based on nav + const slugToTitle = title => title && title.replace(/-/g, ' ').split(' ').map(pascalCase).join(' ') + const danglingLinks = [] + const categories = state.navigation[app.i18n.locale].links + .filter(link => link.menu !== false) + .reduce((acc, link) => { + link = { ...link } + // clean up children from menu + if (link.children) { + link.children = link.children + .filter(l => l.menu !== false) + // Flatten sub-categories + .flatMap(child => (child.to ? child : child.children)) + .flatMap(child => (child.to ? child : child.children)) + .filter(l => l.to) + } + // ensure link has proper `menuTitle` + if (!link.menuTitle) { + link.menuTitle = link.title || slugToTitle(link.slug) || '' + } + + if (link.children && link.children.length) { + acc.push(link) + } else if (link.to) { + danglingLinks.push(link) + } + return acc + }, []) + + // Push other links to end of list + if (danglingLinks.length) categories.push({ to: '', children: danglingLinks }) + + state.categories[app.i18n.locale] = categories + } + + function getPageTemplate(page: any) { + let template = page.template?.self || page.template + + if (!template) { + // Fetch from nav (root to link) and fallback to settings.template + const slugs = page.to.split('/').filter(Boolean).slice(0, -1) // no need to get latest slug since it is current page + + let links = currentNav.value.links || [] + + slugs.forEach((slug: string) => { + const link = api.findLinkBySlug(links, slug) + + if (link?.template) { + template = typeof link.template === 'string' ? `${link.template}-post` : link.template?.nested + } + + if (!link?.children) { + return + } + + links = link.children + }) + + template = template || state.settings.template + } + + template = pascalCase(template) + + if (!Vue.component(template)) { + // eslint-disable-next-line no-console + console.error(`Template ${template} does not exists, fallback to Page template.`) + + template = 'Page' + } + + return template + } + + function isLinkActive(to: string) { + const path = $nuxt?.$route.path || context.route.path + + return withTrailingSlash(path) === withTrailingSlash(context.$contentLocalePath(to)) + } + + return { + getPageTemplate, + fetchNavigation, + currentNav, + isLinkActive, + init: fetchNavigation + } +} diff --git a/src/core/runtime/composables/releases.ts b/src/core/runtime/composables/releases.ts new file mode 100644 index 000000000..96dea7604 --- /dev/null +++ b/src/core/runtime/composables/releases.ts @@ -0,0 +1,21 @@ +import { DocusAddonContext } from 'src/types' + +export const useDocusReleases = ({ api, state }: DocusAddonContext) => { + async function fetchReleases() { + return (await api.data('github-releases')).releases + } + + async function fetchLastRelease() { + if (process.dev === false && state.lastRelease) return + + const [lastRelease] = await fetchReleases() + + if (lastRelease) state.lastRelease = lastRelease.name + } + + return { + fetchReleases, + fetchLastRelease, + init: fetchLastRelease + } +} diff --git a/src/core/runtime/composables/style.ts b/src/core/runtime/composables/style.ts new file mode 100644 index 000000000..f69a92105 --- /dev/null +++ b/src/core/runtime/composables/style.ts @@ -0,0 +1,109 @@ +import { computed } from '@nuxtjs/composition-api' +import { getColors } from 'theme-colors' +import { DocusAddonContext, Colors } from '../../../types' + +/** + * Parse color definition from Docus Config. + */ +function useColors(colors: Colors) { + try { + return Object.entries(colors).map(([key, color]) => [key, typeof color === 'string' ? getColors(color) : color]) + } catch (e) { + // eslint-disable-next-line no-console + console.warn('Could not parse custom colors:', e.message) + return [] + } +} + +/** + * Create a css variable store. + */ +function useCssVariableStore(scopes = ['dark']) { + scopes = ['default', ...scopes] + + const _store = scopes.reduce((obj, scope) => ({ [scope]: {}, ...obj }), {} as any) + + const getScope = (scope: string) => _store[scope] || null + + const putSingle = (key: string) => (value: string) => { + const _arr = value.split(':') + const _value = _arr.pop() + const _scope = getScope(_arr.pop() || 'default') + if (_scope) { + _scope[key] = _value + } + } + + const put = (key: string, value: string) => { + value.split(' ').map(putSingle(key)) + } + + const generateVar = ([key, value]: [string, any]) => `--${key}: ${value}` + + const generateScope = (scope: string) => { + const vars = Object.entries(getScope(scope)).map(generateVar).join(';') + return scope === 'default' ? `:root {${vars}}` : `html.${scope} {${vars}}` + } + + const generate = () => scopes.map(generateScope).join(' ') + + return { put, generate } +} + +/** + * Generate a css string from variables definition. + */ +function useCSSVariables(colors: Colors) { + const { put, generate } = useCssVariableStore(['dark']) + + const colorsList = useColors(colors) + + colorsList.forEach(([color, map]) => + Object.entries(map).forEach(([variant, value]) => put(`${color}-${variant}`, value as string)) + ) + + return generate() +} + +export const useDocusStyle = ({ context, state }: DocusAddonContext) => { + const app = context.app + + const styles = computed(() => useCSSVariables(state.theme.colors)) + + function updateHead() { + const head = typeof app.head === 'function' ? app.head() : app.head + + // Init head if absent + if (!Array.isArray(head.style)) { + head.style = [] + } + + // Init meta is absent + if (!Array.isArray(head.meta)) { + head.meta = [] + } + + // Push CSS variables + head.style.push({ + hid: 'docus-theme', + cssText: styles.value, + type: 'text/css' + }) + + // Set 'apple-mobile-web-app-title' from Docus title + head.meta = head.meta.filter(s => s.hid !== 'apple-mobile-web-app-title') + head.meta.push({ + hid: 'apple-mobile-web-app-title', + name: 'apple-mobile-web-app-title', + content: state.settings.title + }) + + head.meta = head.meta.filter(s => s.hid !== 'theme-color') + } + + return { + styles, + updateHead, + init: updateHead + } +} diff --git a/src/core/runtime/docus.ts b/src/core/runtime/docus.ts index 1b7a3c388..3a968bdf6 100644 --- a/src/core/runtime/docus.ts +++ b/src/core/runtime/docus.ts @@ -1,402 +1,64 @@ -import Vue from 'vue' -import { pascalCase } from 'scule' -import { joinURL, withTrailingSlash, withoutTrailingSlash } from 'ufo' -import { Context } from '@nuxt/types' -import { computed, reactive, toRefs } from '@nuxtjs/composition-api' -import { DocusSettings } from '../../types' -import { useCSSVariables } from '../utils/css' - -const findLinkBySlug = (links: any[], slug: string) => links.find(link => link.slug === slug) - -type PermissiveContext = Context & { [key: string]: any } - -type DocusState = { - page: any - categories: any - lastRelease: any - settings: any - theme: any - ui: any - nav: any -} - -export const createDocus = async ( - { app, $contentLocalePath, route, beforeNuxtRender }: PermissiveContext, - settings: DocusSettings, - createQuery: any -) => { - // Local instance let - let $nuxt - - /** - * State - */ - +import { reactive, toRefs } from '@nuxtjs/composition-api' +import { DocusSettings, PermissiveContext, DocusState, DocusAddonContext } from '../../types' +import { useDocusApi } from './composables/api' +import { useDocusNavigation } from './composables/navigation' +import { clientAsyncData, docusInit } from './composables/helpers' +import { useDocusGithub } from './composables/github' +import { useDocusReleases } from './composables/releases' +import { useDocusStyle } from './composables/style' +import { useDocusAddons } from './composables/addons' + +/** + * Create the $docus runtime injection instance. + */ +export const createDocus = async (context: PermissiveContext, settings: DocusSettings, createQuery: any) => { + // Nuxt instance proxy + let $nuxt: any + + // State const state = reactive({ page: {}, categories: {}, - lastRelease: null, settings: null, - theme: null, - ui: null, - nav: {} + theme: null }) as DocusState - // Map locales to nav - app.i18n.locales.forEach((locale: any) => (state.nav[locale.code] = {})) - - /** - * Computed references - */ - - const currentNav = computed(() => state.nav[app.i18n.locale]) - - const previewUrl = computed(() => withoutTrailingSlash(state.settings.url) + '/preview.png') - - const repoUrl = computed(() => joinURL(state.settings.github.url, state.settings.github.repo)) - - const styles = computed(() => useCSSVariables(state.theme.colors, { code: 'prism' })) - - /** - * Methods - */ - - function data(path: string) { - return createQuery(joinURL('/data', path), {}).fetch() - } - - function search(path: string | any, options?) { - if (typeof path !== 'string') { - options = path - path = '' - } - return createQuery(joinURL('/pages', path), options) - } - - function page(path: string) { - return this.search(path).fetch() - } - - function fetchSettings() { - const { theme, ...userSettings } = settings - - state.settings = userSettings - state.theme = theme - - // Update injected styles on HMR - if (process.dev && process.client && window.$nuxt) updateHead() - } - - async function fetch() { - fetchSettings() - - await Promise.all([fetchNavigation(), fetchLastRelease()]) - } - - async function fetchNavigation() { - // TODO: Maybe remove this - // Avoid re-fetching in production - if (process.dev === false && state.nav[app.i18n.locale].links) return - - // Get fields - const draft = state.ui?.draft ? undefined : false - const fields = [ - 'title', - 'menu', - 'menuTitle', - 'dir', - 'nav', - 'category', - 'slug', - 'version', - 'to', - 'icon', - 'description', - 'template' - ] - if (process.dev) fields.push('draft') - - // Query pages - const pages = await search({ deep: true }) - .where({ language: app.i18n.locale, draft, nav: { $ne: false } }) - .only(fields) - .sortBy('position', 'asc') - .fetch() - - let depth = 0 - - const links = [] - - const getPageLink = (page: any) => ({ - slug: page.slug, - to: withoutTrailingSlash(page.to || page.slug), - menu: page.menu, - menuTitle: page.menuTitle, - template: page.template, - title: page.title, - icon: page.icon, - description: page.description, - ...page.nav - }) - - // Add each page to navigation - pages.forEach((page: any) => { - page.nav = page.nav || {} - - if (typeof page.nav === 'string') { - page.nav = { slot: page.nav } - } - - // TODO: Ignore files directly from @nuxt/content - if (page.slug.startsWith('_')) { - return - } - - // To: '/docs/guide/hello.md' -> dirs: ['docs', 'guide'] - page.dirs = withoutTrailingSlash(page.to) - .split('/') - .filter(_ => _) - - // Remove the file part (except if index.md) - if (page.slug !== '') { - page.dirs = page.dirs.slice(0, -1) - } - - if (!page.dirs.length) { - page.nav.slot = page.nav.slot || 'header' - return links.push(getPageLink(page)) - } - - let currentLinks = links - - let link = null - - page.dirs.forEach((dir: string, index: number) => { - // If children has been disabled (nav.children = false) - if (!currentLinks) return - if (index > depth) depth = index - - link = findLinkBySlug(currentLinks, dir) - - if (link) { - currentLinks = link.children - } else { - link = { - slug: dir, - children: [] - } - currentLinks.push(link) - currentLinks = currentLinks[currentLinks.length - 1].children - } - }) - - if (!currentLinks) return - - // If index page, merge also with parent for metadata - if (!page.slug) { - if (page.dirs.length === 1) page.nav.slot = page.nav.slot || 'header' - - Object.assign(link, getPageLink(page)) - } else { - // Push page - currentLinks.push(getPageLink(page)) - } - }) - - // Increment navDepth for files - depth++ - - // Assign to $docus - state.nav[app.i18n.locale] = { - depth, - links - } - - // calculate categories based on nav - const slugToTitle = title => title && title.replace(/-/g, ' ').split(' ').map(pascalCase).join(' ') - const danglingLinks = [] - const categories = state.nav[app.i18n.locale].links - .filter(link => link.menu !== false) - .reduce((acc, link) => { - link = { ...link } - // clean up children from menu - if (link.children) { - link.children = link.children - .filter(l => l.menu !== false) - // Flatten sub-categories - .flatMap(child => (child.to ? child : child.children)) - .flatMap(child => (child.to ? child : child.children)) - .filter(l => l.to) - } - // ensure link has proper `menuTitle` - if (!link.menuTitle) { - link.menuTitle = link.title || slugToTitle(link.slug) || '' - } - - if (link.children && link.children.length) { - acc.push(link) - } else if (link.to) { - danglingLinks.push(link) - } - return acc - }, []) - - // push others links to end of list - if (danglingLinks.length) categories.push({ to: '', children: danglingLinks }) - state.categories[app.i18n.locale] = categories + // Split theme & user settings + const { theme, ...userSettings } = settings + state.settings = userSettings + state.theme = theme + + // Create API helpers + const api = useDocusApi(createQuery) + + // Create Docus Addons context + const docusAddonContext: DocusAddonContext = { + $nuxt, + context, + state, + settings, + createQuery, + api } - function getPageTemplate(page: any) { - let template = page.template?.self || page.template - - if (!template) { - // Fetch from nav (root to link) and fallback to settings.template - const slugs = page.to.split('/').filter(Boolean).slice(0, -1) // no need to get latest slug since it is current page - - let links = currentNav.value.links || [] - - slugs.forEach((slug: string) => { - const link = findLinkBySlug(links, slug) - if (link?.template) { - template = typeof link.template === 'string' ? `${link.template}-post` : link.template?.nested - } - if (!link?.children) { - return - } - links = link.children - }) - - template = template || state.settings.template - } - - template = pascalCase(template) - - if (!Vue.component(template)) { - // eslint-disable-next-line no-console - console.error(`Template ${template} does not exists, fallback to Page template.`) - - template = 'Page' - } - - return template - } - - async function fetchReleases() { - const repo = await data('github-releases') - return repo.releases - } - - async function fetchLastRelease() { - if (process.dev === false && state.lastRelease) return - - const [lastRelease] = await fetchReleases() - - if (lastRelease) state.lastRelease = lastRelease.name - } - - function updateHead() { - const head = typeof app.head === 'function' ? app.head() : app.head - - // Update when editing content/settings.json - if (process.dev && process.client && window.$nuxt) { - const style = head.style.find(s => s.hid === 'docus-theme') + // Docus default addons + const docusAddons = [useDocusStyle, useDocusNavigation, useDocusReleases, useDocusGithub] - if (style) { - style.cssText = styles.value - window.$nuxt.$meta().refresh() - } + // Addons manager + const { setupAddons, addonsContext } = useDocusAddons(docusAddonContext, docusAddons) - return - } - - // Add head keys - if (!Array.isArray(head.style)) { - head.style = [] - } - - if (!Array.isArray(head.meta)) { - head.meta = [] - } - - head.style.push({ - hid: 'docus-theme', - cssText: styles.value, - type: 'text/css' - }) - - head.meta = head.meta.filter(s => s.hid !== 'apple-mobile-web-app-title') - - head.meta.push({ - hid: 'apple-mobile-web-app-title', - name: 'apple-mobile-web-app-title', - content: state.settings.title - }) - - head.meta = head.meta.filter(s => s.hid !== 'theme-color') - - head.meta.push({ hid: 'theme-color', name: 'theme-color', content: state.theme.colors.primary }) - } - - function isLinkActive(to: string) { - const path = $nuxt?.$route.path || route.path - return withTrailingSlash(path) === withTrailingSlash($contentLocalePath(to)) - } - - /** - * Hooks and injection - */ - - if (process.client) { - window.onNuxtReady((nuxt: any) => { - $nuxt = nuxt - - // Workaround since in full static mode, asyncData is not called anymore - app.router.beforeEach((_: any, __: any, next: any) => { - const payload = nuxt._pagePayload || {} - - payload.data = payload.data || [] - - if (payload.data[0]?.page?.template && typeof Vue.component(payload.data[0].page.template) === 'function') { - // Preload the component on client-side navigation - Vue.component(payload.data[0].page.template) - } - - next() - }) - }) - } - - // HotReload on development - if (process.client && process.dev) { - window.onNuxtReady(() => window.$nuxt.$on('content:update', fetch)) - } - - if (process.server) { - await fetch() - - beforeNuxtRender(({ nuxtState }) => (nuxtState.docus = state)) - } + // Setup addons + await setupAddons() - // SPA Fallback - if (process.client && !state.settings) await fetch() + // Init Docus for every context + await docusInit(docusAddonContext, fetch) - updateHead() + // Workaround for async data + clientAsyncData(context.app, $nuxt) return { ...toRefs(state), - currentNav, - previewUrl, - repoUrl, - styles, - isLinkActive, - getPageTemplate, - fetchLastRelease, - fetchNavigation, - fetchReleases, - fetchSettings, - fetch, - search, - page, - data + ...api, + ...addonsContext } } diff --git a/src/core/runtime/index.ts b/src/core/runtime/index.ts index 3e3b5d835..659f62b7c 100644 --- a/src/core/runtime/index.ts +++ b/src/core/runtime/index.ts @@ -1,4 +1,4 @@ export * from './docus' -export * from './RemoteQueryBuilder' -export * from './QueryBuilder' +export * from './api/RemoteQueryBuilder' +export * from './api/QueryBuilder' export * from './utils' diff --git a/src/core/utils/css.ts b/src/core/utils/css.ts deleted file mode 100644 index baec000f3..000000000 --- a/src/core/utils/css.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { getColors } from 'theme-colors' -import { Alias, Colors } from '../../types' - -export function useColors(colors: Colors, aliases: Alias = {}) { - try { - return Object.entries(colors).map(([key, color]) => [ - aliases[key] || key, - typeof color === 'string' ? getColors(color) : color - ]) - } catch (e) { - // eslint-disable-next-line no-console - console.warn('Could not parse custom colors:', e.message) - return [] - } -} - -export function useCSSVariables(colors: Colors, aliases: Alias = {}) { - const { put, generate } = useCssVariableStore(['dark']) - - const colorsList = useColors(colors, aliases) - colorsList.forEach(([color, map]) => { - Object.entries(map).forEach(([variant, value]) => put(`${color}-${variant}`, value as string)) - }) - - return generate() -} - -function useCssVariableStore(scopes = ['dark']) { - scopes = ['default', ...scopes] - - const _store = scopes.reduce((obj, scope) => ({ [scope]: {}, ...obj }), {} as any) - - const getScope = (scope: string) => _store[scope] || null - - const putSingle = (key: string) => (value: string) => { - const _arr = value.split(':') - const _value = _arr.pop() - const _scope = getScope(_arr.pop() || 'default') - if (_scope) { - _scope[key] = _value - } - } - - const put = (key: string, value: string) => { - value.split(' ').map(putSingle(key)) - } - - const generateVar = ([key, value]: [string, any]) => `--${key}: ${value}` - - const generateScope = (scope: string) => { - const vars = Object.entries(getScope(scope)).map(generateVar).join(';') - return scope === 'default' ? `:root {${vars}}` : `html.${scope} {${vars}}` - } - - const generate = () => scopes.map(generateScope).join(' ') - - return { put, generate } -} diff --git a/src/core/utils/document.ts b/src/core/utils/document.ts index 56b1124ee..2bc52f849 100644 --- a/src/core/utils/document.ts +++ b/src/core/utils/document.ts @@ -9,12 +9,12 @@ export function generatePosition(path: string, document: DocusDocument): string .map(part => { const match = part.match(/^(\d+)\./) if (match) { - return paddLeft(match[1], 4) + return padLeft(match[1], 4) } - return document.position ? paddLeft(document.position, 4) : '9999' // Parts without a position are going down to the bottom + return document.position ? padLeft(document.position, 4) : '9999' // Parts without a position are going down to the bottom }) .join('') - return paddRight(position, 12) + return padRight(position, 12) } export function generateSlug(name: string): string { @@ -23,6 +23,7 @@ export function generateSlug(name: string): string { .replace(/^index/, '') .replace(/\.draft/, '') } + export function generateTo(path: string): string { return withoutTrailingSlash(path.split('/').map(generateSlug).join('/')) } @@ -69,6 +70,8 @@ export function processDocumentInfo(document: DocusDocument): DocusDocument { return document } +// Locals + function getTextContent(node: DocusMarkdownNode): string { let text = node.value || '' if (node.children) { @@ -77,10 +80,10 @@ function getTextContent(node: DocusMarkdownNode): string { return text } -function paddLeft(value: string, length: number): string { +function padLeft(value: string, length: number): string { return ('0'.repeat(length) + value).substr(String(value).length) } -function paddRight(value: string, length: number): string { +function padRight(value: string, length: number): string { return (value + '0'.repeat(length)).substr(0, length) } diff --git a/src/core/utils/path.ts b/src/core/utils/path.ts index 647c1b5cf..fb593b8e7 100644 --- a/src/core/utils/path.ts +++ b/src/core/utils/path.ts @@ -8,9 +8,11 @@ const fs = gracefulFs.promises export const r = (...args: string[]) => resolve(__dirname, '../..', ...args) const _require = jiti(__filename) -export function tryRequire(name) { + +export function tryRequire(name: string) { try { const _plugin = _require(require.resolve(name)) + return _plugin.default || _plugin } catch (e) { logger.error(e.toString()) @@ -22,7 +24,8 @@ export function readFile(path: string) { return fs.readFile(path, { encoding: 'utf8' }) } -export async function exists(path) { +export async function exists(path: string) { const pathExists = await fs.stat(path).catch(() => false) + return !!pathExists } diff --git a/src/defaultTheme/components/molecules/AsideNavigation.vue b/src/defaultTheme/components/molecules/AsideNavigation.vue index abe3591d0..4b3f57e61 100644 --- a/src/defaultTheme/components/molecules/AsideNavigation.vue +++ b/src/defaultTheme/components/molecules/AsideNavigation.vue @@ -48,7 +48,7 @@ export default defineComponent({ return this.$docus.categories.value[this.$i18n.locale] }, lastRelease() { - return this.$docus.lastRelease.value + return this.$docus.lastRelease?.value } } }) diff --git a/src/defaultTheme/components/organisms/AppHeader.vue b/src/defaultTheme/components/organisms/AppHeader.vue index 5081e6750..40f48b709 100644 --- a/src/defaultTheme/components/organisms/AppHeader.vue +++ b/src/defaultTheme/components/organisms/AppHeader.vue @@ -56,7 +56,7 @@ export default defineComponent({ const settings = computed(() => $docus.settings) - const lastRelease = computed(() => $docus.lastRelease.value) + const lastRelease = computed(() => $docus.lastRelease?.value) return { settings, diff --git a/src/defaultTheme/settings.ts b/src/defaultTheme/settings.ts index 0921b1389..1f2fbb5df 100644 --- a/src/defaultTheme/settings.ts +++ b/src/defaultTheme/settings.ts @@ -5,7 +5,7 @@ const defaultThemeSettings = { }, colors: { primary: '#3073F1', - code: { + prism: { foreground: 'inherit', background: '#fbfbfb dark:#1e1e1e', class: '#9807af dark:#E879F9', diff --git a/src/types/core.ts b/src/types/core.ts index 096bcf5a5..7ce675c5d 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1,3 +1,5 @@ +import { Context } from '@nuxt/types' +import { useDocusApi } from 'src/core/runtime/composables/api' import { MetaInfo } from 'vue-meta' import { DefaultThemeSettings } from '../defaultTheme/index.d' import { DocusRootNode } from './markdown' @@ -15,6 +17,19 @@ export interface Toc { searchDepth: number links: TocLink[] } +export type PermissiveContext = Context & { [key: string]: any } + +export type DocusState = { + // Core + settings: any + page: any + categories: any + navigation: any + theme: any + // Addons + ui: any + lastRelease: any +} export interface DocusDocument { // font-matter @@ -56,12 +71,17 @@ export interface DocusSettings { [key: string]: any } -export interface Colors { - [key: string]: string | Colors +export interface DocusAddonContext { + context: PermissiveContext + state: DocusState + settings: DocusSettings + createQuery: any + api: ReturnType + $nuxt?: any } -export interface Alias { - [key: string]: string +export interface Colors { + [key: string]: string | Colors } // Storage