refactor: migrate default theme to use script-setup (#137)

Co-authored-by: Kia King Ishii <kia.king.08@gmail.com>
pull/156/head
Evan You 4 years ago committed by GitHub
parent 136a56e74d
commit b127aeeaf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -62,8 +62,8 @@
"url": "https://github.com/vuejs/vitepress/issues"
},
"dependencies": {
"@vue/compiler-sfc": "^3.0.2",
"@vue/server-renderer": "^3.0.2",
"@vue/compiler-sfc": "^3.0.3",
"@vue/server-renderer": "^3.0.3",
"chalk": "^4.1.0",
"debug": "^4.1.1",
"diacritics": "^1.3.0",
@ -82,8 +82,8 @@
"prismjs": "^1.20.0",
"rollup": "^2.33.3",
"slash": "^3.0.0",
"vite": "^1.0.0-rc.9",
"vue": "^3.0.2"
"vite": "^1.0.0-rc.13",
"vue": "^3.0.3"
},
"devDependencies": {
"@types/fs-extra": "^9.0.1",

@ -45,7 +45,7 @@
<Debug />
</template>
<script>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import NavBar from './components/NavBar.vue'
import Home from './components/Home.vue'
@ -54,76 +54,56 @@ import SideBar from './components/SideBar.vue'
import Page from './components/Page.vue'
import { useRoute, useSiteData, useSiteDataByRoute } from 'vitepress'
export default {
components: {
Home,
NavBar,
ToggleSideBarButton,
SideBar,
Page
},
const route = useRoute()
const siteData = useSiteData()
const siteRouteData = useSiteDataByRoute()
setup() {
const route = useRoute()
const siteData = useSiteData()
const siteRouteData = useSiteDataByRoute()
const openSideBar = ref(false)
const enableHome = computed(() => !!route.data.frontmatter.home)
const openSideBar = ref(false)
const enableHome = computed(() => !!route.data.frontmatter.home)
const showNavbar = computed(() => {
const { themeConfig } = siteRouteData.value
const { frontmatter } = route.data
if (
frontmatter.navbar === false
|| themeConfig.navbar === false) {
return false
}
return (
siteData.value.title
|| themeConfig.logo
|| themeConfig.repo
|| themeConfig.nav
)
})
const showSidebar = computed(() => {
const { frontmatter } = route.data
const { themeConfig } = siteRouteData.value
return (
!frontmatter.home
&& frontmatter.sidebar !== false
&& ((typeof themeConfig.sidebar === 'object') && (Object.keys(themeConfig.sidebar).length != 0)
|| (Array.isArray(themeConfig.sidebar) && themeConfig.sidebar.length != 0))
)
})
const showNavbar = computed(() => {
const { themeConfig } = siteRouteData.value
const { frontmatter } = route.data
if (frontmatter.navbar === false || themeConfig.navbar === false) {
return false
}
return (
siteData.value.title ||
themeConfig.logo ||
themeConfig.repo ||
themeConfig.nav
)
})
const pageClasses = computed(() => {
return [{
'no-navbar': !showNavbar.value,
'sidebar-open': openSideBar.value,
'no-sidebar': !showSidebar.value
}]
})
const showSidebar = computed(() => {
const { frontmatter } = route.data
const { themeConfig } = siteRouteData.value
return (
!frontmatter.home &&
frontmatter.sidebar !== false &&
((typeof themeConfig.sidebar === 'object' &&
Object.keys(themeConfig.sidebar).length != 0) ||
(Array.isArray(themeConfig.sidebar) && themeConfig.sidebar.length != 0))
)
})
const toggleSidebar = (to) => {
openSideBar.value = typeof to === 'boolean' ? to : !openSideBar.value
const pageClasses = computed(() => {
return [
{
'no-navbar': !showNavbar.value,
'sidebar-open': openSideBar.value,
'no-sidebar': !showSidebar.value
}
]
})
const hideSidebar = toggleSidebar.bind(null, false)
// close the sidebar when navigating to a different location
watch(route, hideSidebar)
// TODO: route only changes when the pathname changes
// listening to hashchange does nothing because it's prevented in router
return {
showNavbar,
showSidebar,
openSideBar,
pageClasses,
enableHome,
toggleSidebar
}
}
const toggleSidebar = (to) => {
openSideBar.value = typeof to === 'boolean' ? to : !openSideBar.value
}
const hideSidebar = toggleSidebar.bind(null, false)
// close the sidebar when navigating to a different location
watch(route, hideSidebar)
// TODO: route only changes when the pathname changes
// listening to hashchange does nothing because it's prevented in router
</script>

@ -2,15 +2,11 @@
<div class="theme">
<h1>404</h1>
<blockquote>{{ getMsg() }}</blockquote>
<a :href="$site.base" aria-label="go to home">
Take me home.
</a>
<a :href="$site.base" aria-label="go to home">Take me home.</a>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
const msgs = [
`There's nothing here.`,
`How did we get here?`,
@ -18,11 +14,7 @@ const msgs = [
`Looks like we've got some broken links.`
]
export default defineComponent({
setup: () => ({
getMsg() {
return msgs[Math.floor(Math.random() * msgs.length)]
}
})
})
function getMsg() {
return msgs[Math.floor(Math.random() * msgs.length)]
}
</script>

@ -6,25 +6,11 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { useEditLink } from '../composables/editLink'
import OutboundLink from './icons/OutboundLink.vue'
export default defineComponent({
components: {
OutboundLink
},
setup() {
const { url, text } = useEditLink()
return {
url,
text
}
}
})
const { url, text } = useEditLink()
</script>
<style scoped>

@ -4,26 +4,17 @@
v-if="data.heroImage"
:src="heroImageSrc"
:alt="data.heroAlt || 'hero'"
>
/>
<h1
v-if="data.heroText !== null"
id="main-title"
>
<h1 v-if="data.heroText !== null" id="main-title">
{{ data.heroText || siteTitle || 'Hello' }}
</h1>
<p
v-if="data.tagline !== null"
class="description"
>
<p v-if="data.tagline !== null" class="description">
{{ data.tagline || siteDescription || 'Welcome to your VitePress site' }}
</p>
<p
v-if="data.actionText && data.actionLink"
class="action"
>
<p v-if="data.actionText && data.actionLink" class="action">
<a class="action-link" :href="actionLink.link">
{{ actionLink.text }}
</a>
@ -31,58 +22,35 @@
<slot name="hero" />
</header>
<div
v-if="data.features && data.features.length"
class="features"
>
<div
v-for="(feature, index) in data.features"
:key="index"
class="feature"
>
<div v-if="data.features && data.features.length" class="features">
<div v-for="(feature, index) in data.features" :key="index" class="feature">
<h2>{{ feature.title }}</h2>
<p>{{ feature.details }}</p>
</div>
<slot name="features" />
</div>
<div
v-if="data.footer"
class="footer"
>
<div v-if="data.footer" class="footer">
{{ data.footer }}
<slot name="footer" />
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useSiteData } from 'vitepress'
import { withBase } from '../utils'
export default defineComponent({
setup() {
const route = useRoute()
const siteData = useSiteData()
const data = computed(() => route.data.frontmatter)
const actionLink = computed(() => ({
link: data.value.actionLink,
text: data.value.actionText
}))
const heroImageSrc = computed(() => withBase(data.value.heroImage))
const siteTitle = computed(() => siteData.value.title)
const siteDescription = computed(() => siteData.value.description)
return {
data,
actionLink,
heroImageSrc,
siteTitle,
siteDescription
}
}
})
const route = useRoute()
const siteData = useSiteData()
const data = computed(() => route.data.frontmatter)
const actionLink = computed(() => ({
link: data.value.actionLink,
text: data.value.actionText
}))
const heroImageSrc = computed(() => withBase(data.value.heroImage))
const siteTitle = computed(() => siteData.value.title)
const siteDescription = computed(() => siteData.value.description)
</script>
<style scoped>

@ -10,17 +10,9 @@
<slot name="search" />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import NavBarTitle from './NavBarTitle.vue'
import NavBarLinks from './NavBarLinks.vue'
export default defineComponent({
components: {
NavBarTitle,
NavBarLinks
}
})
</script>
<style scoped>

@ -1,77 +0,0 @@
import { defineComponent, computed, PropType } from 'vue'
import { useRoute } from 'vitepress'
import { withBase, isExternal } from '../utils'
import { DefaultTheme } from '../config'
import OutboundLink from './icons/OutboundLink.vue'
const normalizePath = (path: string): string => {
path = path
.replace(/#.*$/, '')
.replace(/\?.*$/, '')
.replace(/\.html$/, '')
if (path.endsWith('/')) {
path += 'index'
}
return path
}
export default defineComponent({
components: {
OutboundLink
},
props: {
item: {
type: Object as PropType<DefaultTheme.NavItemWithLink>,
required: true
}
},
setup(props) {
const item = props.item
const route = useRoute()
const classes = computed(() => ({
active: isActiveLink.value,
external: isExternalLink.value
}))
const isActiveLink = computed(() => {
return normalizePath(withBase(item.link)) === normalizePath(route.path)
})
const isExternalLink = computed(() => {
return isExternal(item.link)
})
const href = computed(() => {
return isExternalLink.value ? item.link : withBase(item.link)
})
const target = computed(() => {
if (item.target) {
return item.target
}
return isExternalLink.value ? '_blank' : ''
})
const rel = computed(() => {
if (item.rel) {
return item.rel
}
return isExternalLink.value ? 'noopener noreferrer' : ''
})
return {
classes,
isActiveLink,
isExternalLink,
href,
target,
rel
}
}
})

@ -13,7 +13,63 @@
</div>
</template>
<script src="./NavBarLink"></script>
<script setup lang="ts">
import { computed, defineProps } from 'vue'
import { useRoute } from 'vitepress'
import { withBase, isExternal } from '../utils'
import type { DefaultTheme } from '../config'
import OutboundLink from './icons/OutboundLink.vue'
const { item } = defineProps<{
item: DefaultTheme.NavItemWithLink
}>()
const normalizePath = (path: string): string => {
path = path
.replace(/#.*$/, '')
.replace(/\?.*$/, '')
.replace(/\.html$/, '')
if (path.endsWith('/')) {
path += 'index'
}
return path
}
const route = useRoute()
const classes = computed(() => ({
active: isActiveLink.value,
external: isExternalLink.value
}))
const isActiveLink = computed(() => {
return normalizePath(withBase(item.link)) === normalizePath(route.path)
})
const isExternalLink = computed(() => {
return isExternal(item.link)
})
const href = computed(() => {
return isExternalLink.value ? item.link : withBase(item.link)
})
const target = computed(() => {
if (item.target) {
return item.target
}
return isExternalLink.value ? '_blank' : ''
})
const rel = computed(() => {
if (item.rel) {
return item.rel
}
return isExternalLink.value ? 'noopener noreferrer' : ''
})
</script>
<style scoped>
.navbar-link {

@ -1,101 +0,0 @@
import { computed } from 'vue'
import { useSiteData, useSiteDataByRoute, useRoute } from 'vitepress'
import { inBrowser } from '/@app/utils'
import NavBarLink from './NavBarLink.vue'
import NavDropdownLink from './NavDropdownLink.vue'
import { DefaultTheme } from '../config'
const platforms = ['GitHub', 'GitLab', 'Bitbucket'].map(
(platform) => [platform, new RegExp(platform, 'i')] as const
)
export default {
components: {
NavBarLink,
NavDropdownLink
},
setup() {
const siteDataByRoute = useSiteDataByRoute()
const siteData = useSiteData()
const route = useRoute()
const repoInfo = computed(() => {
const theme = siteData.value.themeConfig as DefaultTheme.Config
const repo = theme.docsRepo || theme.repo
let text: string | undefined = theme.repoLabel
if (repo) {
const link = /^https?:/.test(repo) ? repo : `https://github.com/${repo}`
if (!text) {
// if no label is provided, deduce it from the repo url
const repoHosts = link.match(/^https?:\/\/[^/]+/)
if (repoHosts) {
const repoHost = repoHosts[0]
const foundPlatform = platforms.find(([_platform, re]) =>
re.test(repoHost)
)
text = foundPlatform && foundPlatform[0]
}
}
return { link, text: text || 'Source' }
}
return null
})
const localeCandidates = computed(() => {
const locales = siteData.value.themeConfig.locales
if (!locales) {
return null
}
const localeKeys = Object.keys(locales)
if (localeKeys.length <= 1) {
return null
}
// handle site base
const siteBase = inBrowser ? siteData.value.base : '/'
const siteBaseWithoutSuffix = siteBase.endsWith('/')
? siteBase.slice(0, -1)
: siteBase
// remove site base in browser env
const routerPath = route.path.slice(siteBaseWithoutSuffix.length)
const currentLangBase = localeKeys.find((v) => {
if (v === '/') {
return false
}
return routerPath.startsWith(v)
})
const currentContentPath = currentLangBase
? routerPath.substring(currentLangBase.length - 1)
: routerPath
const candidates = localeKeys.map((v) => {
const localePath = v.endsWith('/') ? v.slice(0, -1) : v
return {
text: locales[v].label || locales[v].lang,
link: `${localePath}${currentContentPath}`
}
})
const currentLangKey = currentLangBase ? currentLangBase : '/'
const selectText = locales[currentLangKey].selectText
? locales[currentLangKey].selectText
: 'Languages'
return {
text: selectText,
items: candidates
}
})
const navData = computed(() => {
return siteDataByRoute.value.themeConfig.nav
})
return {
navData,
repoInfo,
localeCandidates
}
}
}

@ -11,7 +11,94 @@
</nav>
</template>
<script src="./NavBarLinks"></script>
<script setup lang="ts">
import { computed } from 'vue'
import { useSiteData, useSiteDataByRoute, useRoute } from 'vitepress'
import { inBrowser } from '/@app/utils'
import NavBarLink from './NavBarLink.vue'
import NavDropdownLink from './NavDropdownLink.vue'
import type { DefaultTheme } from '../config'
const platforms = ['GitHub', 'GitLab', 'Bitbucket'].map(
(platform) => [platform, new RegExp(platform, 'i')] as const
)
const siteDataByRoute = useSiteDataByRoute()
const siteData = useSiteData()
const route = useRoute()
const repoInfo = computed(() => {
const theme = siteData.value.themeConfig as DefaultTheme.Config
const repo = theme.docsRepo || theme.repo
let text: string | undefined = theme.repoLabel
if (repo) {
const link = /^https?:/.test(repo) ? repo : `https://github.com/${repo}`
if (!text) {
// if no label is provided, deduce it from the repo url
const repoHosts = link.match(/^https?:\/\/[^/]+/)
if (repoHosts) {
const repoHost = repoHosts[0]
const foundPlatform = platforms.find(([_platform, re]) =>
re.test(repoHost)
)
text = foundPlatform && foundPlatform[0]
}
}
return { link, text: text || 'Source' }
}
return null
})
const localeCandidates = computed(() => {
const locales = siteData.value.themeConfig.locales
if (!locales) {
return null
}
const localeKeys = Object.keys(locales)
if (localeKeys.length <= 1) {
return null
}
// handle site base
const siteBase = inBrowser ? siteData.value.base : '/'
const siteBaseWithoutSuffix = siteBase.endsWith('/')
? siteBase.slice(0, -1)
: siteBase
// remove site base in browser env
const routerPath = route.path.slice(siteBaseWithoutSuffix.length)
const currentLangBase = localeKeys.find((v) => {
if (v === '/') {
return false
}
return routerPath.startsWith(v)
})
const currentContentPath = currentLangBase
? routerPath.substring(currentLangBase.length - 1)
: routerPath
const candidates = localeKeys.map((v) => {
const localePath = v.endsWith('/') ? v.slice(0, -1) : v
return {
text: locales[v].label || locales[v].lang,
link: `${localePath}${currentContentPath}`
}
})
const currentLangKey = currentLangBase ? currentLangBase : '/'
const selectText = locales[currentLangKey].selectText
? locales[currentLangKey].selectText
: 'Languages'
return {
text: selectText,
items: candidates
}
})
const navData = computed(() => {
return siteDataByRoute.value.themeConfig.nav
})
</script>
<style scoped>
.navbar-links {

@ -1,42 +0,0 @@
import NavBarLink from './NavBarLink.vue'
import { defineComponent, ref, watch, PropType } from 'vue'
import { useRoute } from 'vitepress'
import { DefaultTheme } from '../config'
export default defineComponent({
name: 'DropdownLink',
components: {
NavBarLink
},
props: {
item: {
type: Object as PropType<DefaultTheme.NavItemWithChildren>,
required: true
}
},
setup(props) {
const open = ref(false)
const route = useRoute()
watch(
() => route.path,
() => {
open.value = false
}
)
const setOpen = (value: boolean) => {
open.value = value
}
const isLastItemOfArray = <T>(item: T, array: T[]) => {
return array.length && array.indexOf(item) === array.length - 1
}
return {
open,
setOpen,
isLastItemOfArray
}
}
})

@ -11,7 +11,11 @@
</button>
<ul class="nav-dropdown">
<li v-for="(subItem, index) in item.items" :key="subItem.link || index" class="dropdown-item">
<li
v-for="(subItem, index) in item.items"
:key="subItem.link || index"
class="dropdown-item"
>
<h4 v-if="subItem.items">{{ subItem.text }}</h4>
<ul v-if="subItem.items" class="dropdown-subitem-wrapper">
<li
@ -22,10 +26,10 @@
<NavBarLink
:item="childSubItem"
@focusout="
isLastItemOfArray(childSubItem, subItem.items) &&
isLastItemOfArray(subItem, item.items) &&
setOpen(false)
"
isLastItemOfArray(childSubItem, subItem.items) &&
isLastItemOfArray(subItem, item.items) &&
setOpen(false)
"
/>
</li>
</ul>
@ -40,7 +44,34 @@
</div>
</template>
<script src="./NavDropdownLink"></script>
<script setup lang="ts">
import NavBarLink from './NavBarLink.vue'
import { ref, watch, defineProps } from 'vue'
import { useRoute } from 'vitepress'
import type { DefaultTheme } from '../config'
defineProps<{
item: DefaultTheme.NavItemWithChildren
}>()
const open = ref(false)
const route = useRoute()
watch(
() => route.path,
() => {
open.value = false
}
)
const setOpen = (value: boolean) => {
open.value = value
}
const isLastItemOfArray = <T>(item: T, array: T[]) => {
return array.length && array.indexOf(item) === array.length - 1
}
</script>
<style>
.dropdown-wrapper {

@ -17,30 +17,13 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useNextAndPrevLinks } from '../composables/nextAndPrevLinks'
<script setup lang="ts">
import { withBase } from '../utils'
import { useNextAndPrevLinks } from '../composables/nextAndPrevLinks'
import ArrowLeft from './icons/ArrowLeft.vue'
import ArrowRight from './icons/ArrowRight.vue'
export default defineComponent({
components: {
ArrowLeft,
ArrowRight
},
setup () {
const { hasLinks, prev, next } = useNextAndPrevLinks()
return {
hasLinks,
prev,
next,
withBase,
}
}
})
const { hasLinks, prev, next } = useNextAndPrevLinks()
</script>
<style scoped>
@ -96,6 +79,10 @@ export default defineComponent({
transform: translateY(1px);
}
.icon-prev { margin-right: 8px; }
.icon-next { margin-left: 8px; }
.icon-prev {
margin-right: 8px;
}
.icon-next {
margin-left: 8px;
}
</style>

@ -14,17 +14,9 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import PageFooter from './PageFooter.vue'
import NextAndPrevLinks from './NextAndPrevLinks.vue'
export default defineComponent({
components: {
PageFooter,
NextAndPrevLinks
}
})
</script>
<style scoped>

@ -12,20 +12,13 @@
</aside>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { defineProps } from 'vue'
import NavBarLinks from './NavBarLinks.vue'
import SideBarLinks from './SideBarLinks.vue'
export default defineComponent({
components: {
NavBarLinks,
SideBarLinks
},
props: {
open: { type: Boolean, required: true }
}
defineProps({
open: { type: Boolean, required: true }
})
</script>
@ -41,7 +34,7 @@ export default defineComponent({
background-color: var(--c-bg);
overflow-y: auto;
transform: translateX(-100%);
transition: transform .25s ease;
transition: transform 0.25s ease;
}
@media (min-width: 720px) {

@ -1,29 +1,12 @@
<template>
<ul v-if="items.length > 0" class="sidebar-links">
<SideBarLink
v-for="item of items"
:key="item.text"
:item="item"
/>
<SideBarLink v-for="item of items" :key="item.text" :item="item" />
</ul>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { useSideBar } from '../composables/sideBar'
import { SideBarLink } from './SideBarLink'
export default defineComponent({
components: {
SideBarLink
},
setup() {
const items = useSideBar()
return {
items
}
}
})
const items = useSideBar()
</script>

@ -15,6 +15,8 @@ export type BuildOptions = Pick<
>
export async function build(buildOptions: BuildOptions = {}) {
const start = Date.now()
process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(buildOptions.root)
@ -66,4 +68,6 @@ export async function build(buildOptions: BuildOptions = {}) {
} finally {
await fs.remove(siteConfig.tempDir)
}
console.log(`build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.`)
}

@ -85,10 +85,14 @@ export async function bundle(
},
generateBundle(_options, bundle) {
if (!isClientBuild) {
return
}
// for each .md entry chunk, adjust its name to its correct path.
for (const name in bundle) {
const chunk = bundle[name]
if (isPageChunk(chunk) && isClientBuild) {
if (isPageChunk(chunk)) {
// record page -> hash relations
const hash = chunk.fileName.match(hashRE)![1]
const pageName = chunk.fileName.replace(hashRE, '')
@ -150,26 +154,15 @@ export async function bundle(
let clientResult, serverResult
const spinner = ora()
spinner.start('building client bundle...')
try {
clientResult = await build(viteOptions)
} catch (e) {
spinner.stopAndPersist({
symbol: failMark
})
throw e
}
spinner.stopAndPersist({
symbol: okMark
})
spinner.start('building server bundle...')
isClientBuild = false
spinner.start('building client + server bundles...')
try {
serverResult = await ssrBuild({
...viteOptions,
outDir: config.tempDir
})
;[clientResult, serverResult] = await Promise.all([
build(viteOptions),
ssrBuild({
...viteOptions,
outDir: config.tempDir
})
])
} catch (e) {
spinner.stopAndPersist({
symbol: failMark

@ -63,6 +63,7 @@ export function createMarkdownToVueRenderFn(
}
const scriptRE = /<\/script>/
const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/
@ -70,7 +71,11 @@ function injectPageData(tags: string[], data: PageData) {
const code = `\nexport const __pageData = ${JSON.stringify(
JSON.stringify(data)
)}`
const existingScriptIndex = tags.findIndex((tag) => scriptRE.test(tag))
const existingScriptIndex = tags.findIndex((tag) => {
return scriptRE.test(tag) && !scriptSetupRE.test(tag)
})
if (existingScriptIndex > -1) {
const tagSrc = tags[existingScriptIndex]
// user has <script> tag inside markdown
@ -82,7 +87,7 @@ function injectPageData(tags: string[], data: PageData) {
code + (hasDefaultExport ? `` : `\nexport default{}\n`) + `</script>`
)
} else {
tags.push(`<script>${code}\nexport default {}</script>`)
tags.unshift(`<script>${code}\nexport default {}</script>`)
}
return tags

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save