mirror of https://github.com/vuejs/vitepress
Merge efd64d8a7b into 3e681e2ffd
commit
2e4c614e6b
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: 页面未找到
|
||||
---
|
||||
|
||||
# 页面未找到
|
||||
|
||||
这个页面不存在。[回到首页](/zh/)
|
||||
@ -0,0 +1,3 @@
|
||||
# 首页
|
||||
|
||||
中文首页。
|
||||
@ -0,0 +1,152 @@
|
||||
import { SUB_PREFIX } from './constants'
|
||||
import { newPage, realErrors, waitForHydration, type TestPage } from './helpers'
|
||||
|
||||
// the cdn build, served at the root by a host that picks the nearest 404.html
|
||||
const nearest = () => `http://localhost:${process.env['PAGES_PORT']}`
|
||||
// the plain build, served by a host that only knows the root 404.html
|
||||
const rootOnly = () => `http://localhost:${process.env['PLAIN_PORT']}`
|
||||
// the relative build, mounted under a prefix
|
||||
const sub = () => `http://localhost:${process.env['SUB_PORT']}${SUB_PREFIX}`
|
||||
|
||||
let t: TestPage
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await newPage()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await t.page.close()
|
||||
await t.browser.close()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
t.errors.length = 0
|
||||
})
|
||||
|
||||
const h1 = () => t.page.textContent('h1')
|
||||
// the document of a miss is a 404 by design; anything else is a real error
|
||||
const errors = () => realErrors(t.errors, [t.page.url()])
|
||||
const lang = () => t.page.evaluate(() => document.documentElement.lang)
|
||||
const mark = () => t.page.evaluate(() => ((window as any).__spa_marker = 1))
|
||||
const marked = () => t.page.evaluate(() => (window as any).__spa_marker === 1)
|
||||
|
||||
describe('not-found page on a host serving the nearest 404.html', () => {
|
||||
test('a miss shows the theme page when the site has no 404.md', async () => {
|
||||
const res = await t.page.goto(`${nearest()}/nowhere.html`)
|
||||
expect(res?.status()).toBe(404)
|
||||
await waitForHydration(t.page)
|
||||
expect(await t.page.textContent('.NotFound .title')).toBe('PAGE NOT FOUND')
|
||||
expect(await t.page.title()).toBe('404 | Base Fixture')
|
||||
expect(await t.page.getAttribute('.NotFound .link', 'href')).toBe('/')
|
||||
expect(new URL(t.page.url()).pathname).toBe('/nowhere.html')
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
|
||||
test("a miss under a locale shows that locale's 404.md", async () => {
|
||||
const res = await t.page.goto(`${nearest()}/zh/guide/nowhere.html`)
|
||||
expect(res?.status()).toBe(404)
|
||||
await waitForHydration(t.page)
|
||||
expect(await h1()).toContain('页面未找到')
|
||||
expect(await lang()).toBe('zh-CN')
|
||||
expect(await t.page.title()).toBe('页面未找到 | Base Fixture')
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
|
||||
test('the theme page uses the locale text of the miss', async () => {
|
||||
// the root build has no zh text of its own: only the client knows the
|
||||
// locale, from the url
|
||||
await t.page.goto(`${nearest()}/nowhere.html`)
|
||||
await waitForHydration(t.page)
|
||||
expect(await t.page.textContent('.NotFound .title')).toBe('PAGE NOT FOUND')
|
||||
expect(await lang()).toBe('en')
|
||||
})
|
||||
|
||||
test('a 404 served for a url that has a page renders that page', async () => {
|
||||
// the static host has no `index` -> `index.html` rule, so this is a miss
|
||||
// for the server and a real page for the client
|
||||
const res = await t.page.goto(`${nearest()}/sub/index`)
|
||||
expect(res?.status()).toBe(404)
|
||||
await waitForHydration(t.page)
|
||||
expect(await h1()).toContain('Sub index')
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
|
||||
test('the not-found page renders at its own url', async () => {
|
||||
const res = await t.page.goto(`${nearest()}/zh/404.html`)
|
||||
expect(res?.status()).toBe(200)
|
||||
await waitForHydration(t.page)
|
||||
expect(await h1()).toContain('页面未找到')
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
|
||||
test('client-side navigation to a miss keeps the url and recovers', async () => {
|
||||
await t.page.goto(`${nearest()}/`)
|
||||
await waitForHydration(t.page)
|
||||
await mark()
|
||||
await t.page.evaluate(() => {
|
||||
const a = document.createElement('a')
|
||||
a.href = '/zh/nowhere.html'
|
||||
a.id = 'to-nowhere'
|
||||
a.textContent = 'nowhere'
|
||||
// above the fixed nav and sidebar, so the click reaches it
|
||||
a.style.cssText = 'position:fixed;right:0;bottom:0;z-index:1000'
|
||||
document.body.appendChild(a)
|
||||
})
|
||||
await t.page.click('#to-nowhere')
|
||||
await t.page.waitForFunction(() =>
|
||||
document.querySelector('h1')?.textContent?.includes('页面未找到')
|
||||
)
|
||||
expect(new URL(t.page.url()).pathname).toBe('/zh/nowhere.html')
|
||||
expect(await lang()).toBe('zh-CN')
|
||||
expect(await marked()).toBe(true)
|
||||
|
||||
await t.page.click('.vp-doc a[href="/zh/"]')
|
||||
await t.page.waitForFunction(() =>
|
||||
document.querySelector('h1')?.textContent?.includes('首页')
|
||||
)
|
||||
expect(await marked()).toBe(true)
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('not-found page on a host serving only the root 404.html', () => {
|
||||
test("a miss under a locale still shows that locale's page", async () => {
|
||||
const res = await t.page.goto(`${rootOnly()}/zh/nowhere.html`)
|
||||
expect(res?.status()).toBe(404)
|
||||
await waitForHydration(t.page)
|
||||
expect(await h1()).toContain('页面未找到')
|
||||
expect(await lang()).toBe('zh-CN')
|
||||
expect(await t.page.title()).toBe('页面未找到 | Base Fixture')
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
|
||||
test('a miss outside any locale shows the theme page', async () => {
|
||||
await t.page.goto(`${rootOnly()}/deep/nowhere.html`)
|
||||
await waitForHydration(t.page)
|
||||
expect(await t.page.textContent('.NotFound .title')).toBe('PAGE NOT FOUND')
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('not-found page with a relative base', () => {
|
||||
test('a miss under a locale is styled and localized', async () => {
|
||||
const res = await t.page.goto(`${sub()}zh/nowhere.html`)
|
||||
expect(res?.status()).toBe(404)
|
||||
await waitForHydration(t.page)
|
||||
expect(await h1()).toContain('页面未找到')
|
||||
expect(await t.page.evaluate(() => (window as any).__VP_SITE_ROOT__)).toBe(
|
||||
sub()
|
||||
)
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
|
||||
test('a root-level miss shows the theme page', async () => {
|
||||
await t.page.goto(`${sub()}nowhere.html`)
|
||||
await waitForHydration(t.page)
|
||||
expect(await t.page.textContent('.NotFound .title')).toBe('PAGE NOT FOUND')
|
||||
expect(await t.page.getAttribute('.NotFound .link', 'href')).toBe(
|
||||
SUB_PREFIX
|
||||
)
|
||||
expect(errors()).toEqual([])
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Not found
|
||||
---
|
||||
|
||||
# Custom not found
|
||||
|
||||
This page does not exist. [Go home](/)
|
||||
@ -0,0 +1,3 @@
|
||||
# Accueil
|
||||
|
||||
Page française.
|
||||
@ -0,0 +1,118 @@
|
||||
const origin = () => `http://localhost:${process.env['PORT']}`
|
||||
|
||||
const status = (path: string) =>
|
||||
page.evaluate(
|
||||
async (path) =>
|
||||
(await fetch(path, { headers: { accept: 'text/html' } })).status,
|
||||
path
|
||||
)
|
||||
|
||||
describe('not found page', () => {
|
||||
test('a missing url renders the site 404.md with a 404 status', async () => {
|
||||
const res = await page.goto(`${origin()}/missing`)
|
||||
expect(res?.status()).toBe(404)
|
||||
await page.waitForSelector('#app .Layout')
|
||||
|
||||
expect(await page.textContent('h1')).toContain('Custom not found')
|
||||
expect(await page.title()).toBe('Not found | Example')
|
||||
// the address bar keeps what the visitor typed
|
||||
expect(new URL(page.url()).pathname).toBe('/missing')
|
||||
// a doc page without the doc chrome
|
||||
expect(
|
||||
await page.locator('.VPContent').getAttribute('class')
|
||||
).not.toContain('has-sidebar')
|
||||
expect(await page.locator('.VPDocFooter .pager-link').count()).toBe(0)
|
||||
expect(await page.locator('.VPDocFooter .edit-link').count()).toBe(0)
|
||||
})
|
||||
|
||||
test('a locale without its own 404.md inherits the root one', async () => {
|
||||
const res = await page.goto(`${origin()}/fr/guide/missing`)
|
||||
expect(res?.status()).toBe(404)
|
||||
await page.waitForSelector('#app .Layout')
|
||||
|
||||
expect(await page.textContent('h1')).toContain('Custom not found')
|
||||
expect(await page.title()).toBe('Not found | Example')
|
||||
expect(await page.evaluate(() => document.documentElement.lang)).toBe('fr')
|
||||
// the locale menu marks the locale of the miss
|
||||
expect(
|
||||
await page.locator('.VPNavBarTranslations .items a').first().textContent()
|
||||
).toContain('English')
|
||||
})
|
||||
|
||||
test('the not-found page also renders at its own url', async () => {
|
||||
await goto('/404')
|
||||
expect(await page.textContent('h1')).toContain('Custom not found')
|
||||
})
|
||||
|
||||
test('client-side navigation to a missing url keeps the url', async () => {
|
||||
await goto('/')
|
||||
await page.evaluate(() => {
|
||||
;(window as any).__spa_marker = 1
|
||||
const a = document.createElement('a')
|
||||
a.href = '/nested/nowhere'
|
||||
a.id = 'to-nowhere'
|
||||
a.textContent = 'nowhere'
|
||||
// above the fixed nav and sidebar, so the click reaches it
|
||||
a.style.cssText = 'position:fixed;right:0;bottom:0;z-index:1000'
|
||||
document.body.appendChild(a)
|
||||
})
|
||||
await page.click('#to-nowhere')
|
||||
await page.waitForFunction(() =>
|
||||
document.querySelector('h1')?.textContent?.includes('Custom not found')
|
||||
)
|
||||
expect(new URL(page.url()).pathname).toBe('/nested/nowhere.html')
|
||||
expect(await page.evaluate(() => (window as any).__spa_marker)).toBe(1)
|
||||
|
||||
// and back to a real page
|
||||
await page.click('.vp-doc a[href="/"]')
|
||||
await page.waitForSelector('.VPHome')
|
||||
expect(new URL(page.url()).pathname).toBe('/')
|
||||
expect(await page.evaluate(() => (window as any).__spa_marker)).toBe(1)
|
||||
})
|
||||
|
||||
test('the server answers with the right status codes', async () => {
|
||||
await goto('/')
|
||||
expect(await status('/missing')).toBe(404)
|
||||
expect(await status('/missing.html')).toBe(404)
|
||||
expect(await status('/nested/nowhere/')).toBe(404)
|
||||
expect(await status('/')).toBe(200)
|
||||
expect(await status('/home.html')).toBe(200)
|
||||
expect(await status('/404.html')).toBe(200)
|
||||
})
|
||||
|
||||
test.runIf(process.env['VITE_TEST_BUILD'])(
|
||||
'the emitted page is pre-rendered, marked and not indexed',
|
||||
async () => {
|
||||
await goto('/')
|
||||
const html = await page.evaluate(async () =>
|
||||
(await fetch('/missing', { headers: { accept: 'text/html' } })).text()
|
||||
)
|
||||
expect(html).toContain('<div id="app" data-vp-not-found>')
|
||||
expect(html).toContain('Custom not found')
|
||||
expect(html).toContain('<meta name="robots" content="noindex">')
|
||||
|
||||
const frHtml = await page.evaluate(async () =>
|
||||
(await fetch('/fr/404.html')).text()
|
||||
)
|
||||
expect(frHtml).toContain('<html lang="fr"')
|
||||
expect(frHtml).toContain('Custom not found')
|
||||
expect(frHtml).toContain('<title>Not found | Example</title>')
|
||||
}
|
||||
)
|
||||
|
||||
test.runIf(process.env['VITE_TEST_BUILD'])(
|
||||
'a pre-rendered miss loads the page chunk once',
|
||||
async () => {
|
||||
await page.goto(`${origin()}/missing`)
|
||||
await page.waitForSelector('#app .Layout')
|
||||
const chunks = await page.evaluate(() =>
|
||||
performance
|
||||
.getEntriesByType('resource')
|
||||
.map((e) => e.name)
|
||||
.filter((name) => /\/404\.md\./.test(name))
|
||||
)
|
||||
expect(chunks).toHaveLength(1)
|
||||
expect(chunks[0]).not.toContain('.lean.js')
|
||||
}
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,136 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { resolveConfig } from 'node/config'
|
||||
import { notFoundPlugin } from 'node/plugins/notFoundPlugin'
|
||||
import { normalizePath } from 'vite'
|
||||
|
||||
const locales = `locales: { root: { label: 'English', lang: 'en' }, zh: { label: '中文', lang: 'zh-CN' } }`
|
||||
|
||||
async function site(files: Record<string, string>, config = '') {
|
||||
const root = mkdtempSync(join(tmpdir(), 'vp-not-found-'))
|
||||
mkdirSync(join(root, '.vitepress'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(root, '.vitepress/config.mjs'),
|
||||
`export default { ${locales}, ${config} }`
|
||||
)
|
||||
for (const [file, content] of Object.entries(files)) {
|
||||
mkdirSync(join(root, file, '..'), { recursive: true })
|
||||
writeFileSync(join(root, file), content)
|
||||
}
|
||||
const siteConfig = await resolveConfig(root, 'build', 'production')
|
||||
const plugin = notFoundPlugin(siteConfig)
|
||||
const hooks = {
|
||||
resolveId: (id: string, importer?: string) =>
|
||||
(plugin.resolveId as any).handler.call(undefined, id, importer, {}),
|
||||
load: (id: string) => (plugin.load as any).handler.call(undefined, id)
|
||||
}
|
||||
const file = (page: string) => normalizePath(join(siteConfig.srcDir, page))
|
||||
return {
|
||||
siteConfig,
|
||||
...hooks,
|
||||
file,
|
||||
cleanup: () => rmSync(root, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
describe('node/plugins/notFoundPlugin', () => {
|
||||
test('synthesizes the pages nobody wrote', async () => {
|
||||
const s = await site({ 'index.md': '# Home' })
|
||||
try {
|
||||
expect(s.siteConfig.notFoundPages).toEqual([
|
||||
{ path: '404.md', source: null },
|
||||
{ path: 'zh/404.md', source: null }
|
||||
])
|
||||
expect(s.resolveId('/zh/404.md')).toBe(s.file('zh/404.md'))
|
||||
expect(s.resolveId('/zh/404.md?t=123')).toBe(s.file('zh/404.md'))
|
||||
// the bundler hands entries over as native paths
|
||||
expect(s.resolveId(join(s.siteConfig.srcDir, 'zh', '404.md'))).toBe(
|
||||
s.file('zh/404.md')
|
||||
)
|
||||
expect(s.resolveId('./zh/404.md', s.file('index.md'))).toBe(
|
||||
s.file('zh/404.md')
|
||||
)
|
||||
expect(s.load(s.file('404.md'))).toContain('<NotFound />')
|
||||
expect(s.load(s.file('zh/404.md'))).toContain('<NotFound />')
|
||||
} finally {
|
||||
s.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('a locale without its own page re-exports the root one', async () => {
|
||||
const s = await site({ 'index.md': '# Home', '404.md': '# Lost' })
|
||||
try {
|
||||
expect(s.siteConfig.pages).not.toContain('404.md')
|
||||
expect(s.siteConfig.notFoundPages).toEqual([
|
||||
{ path: '404.md', source: '404.md' },
|
||||
{ path: 'zh/404.md', source: null }
|
||||
])
|
||||
// the authored page loads as a file
|
||||
expect(s.resolveId(s.file('404.md'))).toBeUndefined()
|
||||
expect(s.load(s.file('404.md'))).toBeUndefined()
|
||||
// the inherited one is a virtual js module around it
|
||||
const id = s.resolveId('/zh/404.md')
|
||||
expect(id).toBe('\0' + s.file('zh/404.md'))
|
||||
const code = s.load(id)
|
||||
expect(code).toContain(`from ${JSON.stringify(s.file('404.md'))}`)
|
||||
expect(code).toContain(`relativePath: "zh/404.md"`)
|
||||
expect(code).not.toContain('<NotFound />')
|
||||
} finally {
|
||||
s.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('a rewrite can move a page onto the not-found path', async () => {
|
||||
const s = await site(
|
||||
{ 'index.md': '# Home', 'errors/lost.md': '# Lost' },
|
||||
`rewrites: { 'errors/lost.md': '404.md' }`
|
||||
)
|
||||
try {
|
||||
expect(s.siteConfig.pages).not.toContain('errors/lost.md')
|
||||
expect(s.siteConfig.notFoundPages[0]).toEqual({
|
||||
path: '404.md',
|
||||
source: 'errors/lost.md'
|
||||
})
|
||||
expect(s.load(s.file('zh/404.md'))).toContain(
|
||||
`from ${JSON.stringify(s.file('errors/lost.md'))}`
|
||||
)
|
||||
} finally {
|
||||
s.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('a rewrite moving 404.md away leaves the file a regular page', async () => {
|
||||
const s = await site(
|
||||
{ 'index.md': '# Home', '404.md': '# Moved' },
|
||||
`rewrites: { '404.md': 'moved/404.md' }`
|
||||
)
|
||||
try {
|
||||
expect(s.siteConfig.pages).toContain('404.md')
|
||||
expect(s.siteConfig.notFoundPages).toEqual([
|
||||
{ path: '404.md', source: null },
|
||||
{ path: 'zh/404.md', source: null }
|
||||
])
|
||||
// the file keeps its own content; the not-found page is synthesized
|
||||
// from the theme instead
|
||||
expect(s.resolveId(s.file('404.md'))).toBeUndefined()
|
||||
expect(s.load(s.file('404.md'))).toBeUndefined()
|
||||
expect(s.load(s.file('zh/404.md'))).toContain('<NotFound />')
|
||||
} finally {
|
||||
s.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('leaves sub-requests to the module that owns them', async () => {
|
||||
const s = await site({ 'index.md': '# Home', '404.md': '# Lost' })
|
||||
try {
|
||||
expect(
|
||||
s.resolveId('/zh/404.md?vue&type=style&index=0&lang.css')
|
||||
).toBeUndefined()
|
||||
expect(s.load(s.file('zh/404.md') + '?vue&type=style')).toBeUndefined()
|
||||
} finally {
|
||||
s.cleanup()
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,19 @@
|
||||
import { defineComponent, h } from 'vue'
|
||||
|
||||
import { withBase } from '../utils'
|
||||
|
||||
/**
|
||||
* The not-found page content of a site whose theme provides none. Same
|
||||
* shape as the default theme's, so a theme can style it the same way.
|
||||
*/
|
||||
export const NotFound = defineComponent({
|
||||
name: 'VitePressNotFound',
|
||||
setup() {
|
||||
return () =>
|
||||
h('div', { class: 'vp-not-found' }, [
|
||||
h('p', { class: 'code' }, '404'),
|
||||
h('h1', { class: 'title' }, 'Page not found'),
|
||||
h('a', { class: 'link', href: withBase('/') }, 'Take me home')
|
||||
])
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,129 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { normalizePath, type Plugin } from 'vite'
|
||||
|
||||
import { APP_PATH } from '../alias'
|
||||
import type { SiteConfig } from '../siteConfig'
|
||||
import { isExternal, slash, type SiteData } from '../shared'
|
||||
|
||||
const notFoundRE = /(?:^|\/)404\.md(?:\?|$)/
|
||||
|
||||
// the re-export module is plain js under a `.md` id, which keeps it a page
|
||||
// chunk; the virtual-module marker keeps the markdown and sfc transforms off
|
||||
// it (both skip `\0` ids)
|
||||
const VIRTUAL_PREFIX = '\0'
|
||||
|
||||
/**
|
||||
* The not-found page of every locale, as output-relative paths: `404.md`
|
||||
* for the root plus `<locale>/404.md` for each locale directory.
|
||||
*/
|
||||
export function resolveNotFoundPagePaths(site: SiteData): string[] {
|
||||
const dirs = Object.keys(site.locales ?? {}).filter(
|
||||
(key) => key !== 'root' && !isExternal(key)
|
||||
)
|
||||
return ['404.md', ...dirs.map((dir) => `${dir}/404.md`)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Backs every not-found page with a module. A page the author wrote loads
|
||||
* as-is; the others are synthesized here so the router, the build and the
|
||||
* preview server can treat the not-found page like any page:
|
||||
*
|
||||
* - a locale without its own file re-exports the root `404.md`, keeping the
|
||||
* locale in its page data
|
||||
* - with no file at all, a markdown page renders the theme's `NotFound`
|
||||
* component
|
||||
*/
|
||||
export const notFoundPlugin = (siteConfig: SiteConfig): Plugin => {
|
||||
const { srcDir } = siteConfig
|
||||
|
||||
const splitQuery = (id: string): [file: string, query: string] => {
|
||||
const index = id.indexOf('?')
|
||||
return index === -1 ? [id, ''] : [id.slice(0, index), id.slice(index + 1)]
|
||||
}
|
||||
|
||||
// ids arrive as urls, posix paths or native windows paths (the bundler
|
||||
// entries), with the drive letter in either case
|
||||
const isUnderSrcDir = (file: string) =>
|
||||
file
|
||||
.replace(/^[a-z]:/i, (d) => d.toLowerCase())
|
||||
.startsWith(srcDir.replace(/^[a-z]:/i, (d) => d.toLowerCase()))
|
||||
|
||||
// the synthesized page a would-be file stands for, and the authored root
|
||||
// page it inherits when there is one
|
||||
const virtualPage = (file: string) => {
|
||||
const relativePath = slash(
|
||||
path.relative(srcDir, file.replace(VIRTUAL_PREFIX, ''))
|
||||
)
|
||||
// an authored page that a rewrite moves elsewhere still owns its file
|
||||
if (siteConfig.pages.includes(relativePath)) return
|
||||
const page = siteConfig.notFoundPages.find((p) => p.path === relativePath)
|
||||
if (!page || page.source != null) return
|
||||
const root = siteConfig.notFoundPages.find((p) => p.path === '404.md')
|
||||
const inherits = page.path !== '404.md' ? (root?.source ?? null) : null
|
||||
return { path: page.path, inherits }
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vitepress:not-found',
|
||||
enforce: 'pre',
|
||||
|
||||
resolveId: {
|
||||
filter: { id: notFoundRE },
|
||||
handler(id, importer) {
|
||||
const [rawFile, query] = splitQuery(id)
|
||||
// sub-requests (`?vue&type=…`) belong to the module that owns them
|
||||
if (query && !/^t=\d+$/.test(query)) return
|
||||
// normalizing first would fold `./x` into `x`, so test the raw id
|
||||
const file = normalizePath(rawFile)
|
||||
const resolved = isUnderSrcDir(file)
|
||||
? file
|
||||
: rawFile.startsWith('/')
|
||||
? normalizePath(path.join(srcDir, rawFile))
|
||||
: importer && rawFile.startsWith('.')
|
||||
? normalizePath(path.resolve(path.dirname(importer), rawFile))
|
||||
: undefined
|
||||
const page = resolved && virtualPage(resolved)
|
||||
if (page) return page.inherits ? VIRTUAL_PREFIX + resolved : resolved
|
||||
}
|
||||
},
|
||||
|
||||
load: {
|
||||
filter: { id: notFoundRE },
|
||||
handler(id) {
|
||||
const [file, query] = splitQuery(id)
|
||||
if (query) return
|
||||
const page = virtualPage(file)
|
||||
if (!page) return
|
||||
|
||||
if (page.inherits) {
|
||||
const source = normalizePath(path.resolve(srcDir, page.inherits))
|
||||
return [
|
||||
`import Root, { __pageData as base } from ${JSON.stringify(source)}`,
|
||||
`export * from ${JSON.stringify(source)}`,
|
||||
`export default Root`,
|
||||
`export const __pageData = { ...base, relativePath: ${JSON.stringify(page.path)} }`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const helper = normalizePath(path.join(APP_PATH, 'theme.js'))
|
||||
return [
|
||||
'---',
|
||||
'title: "404"',
|
||||
'description: Not Found',
|
||||
'---',
|
||||
'',
|
||||
'<script setup>',
|
||||
`import RawTheme from '@theme/index'`,
|
||||
`import { resolveNotFound } from ${JSON.stringify(helper)}`,
|
||||
'',
|
||||
'const NotFound = resolveNotFound(RawTheme)',
|
||||
'</script>',
|
||||
'',
|
||||
'<NotFound />',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue