From e95015f598846e318c60929f1ef6466a8cfbb729 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sat, 1 Jul 2023 15:43:29 +0530 Subject: [PATCH] fix(client): bypass client router for links explicitly specifying target (#2563) BREAKING CHANGE: specifying `target="_self"` for internal links will now perform full reload. --- docs/guide/asset-handling.md | 14 +++++++++++ src/client/app/composables/preFetch.ts | 5 ++-- src/client/app/index.ts | 23 ++++++++++--------- src/client/app/router.ts | 14 +++++++---- src/client/app/utils.ts | 5 ++-- .../components/VPLocalSearchBox.vue | 1 + src/client/theme-default/support/utils.ts | 2 +- 7 files changed, 43 insertions(+), 21 deletions(-) diff --git a/docs/guide/asset-handling.md b/docs/guide/asset-handling.md index d8260687..2f62569d 100644 --- a/docs/guide/asset-handling.md +++ b/docs/guide/asset-handling.md @@ -31,6 +31,20 @@ There is one exception to this: if you have an HTML page in `public` and link to - [/pure.html](/pure.html) - +Note that `pathname://` is only supported in Markdown links. Also, `pathname://` will open the link in a new tab by default. You can use `target="_self"` instead to open it in the same tab: + +**Input** + +```md +[Link to pure.html](/pure.html){target="_self"} + + +``` + +**Output** + +[Link to pure.html](/pure.html){target="_self"} + ## Base URL If your site is deployed to a non-root URL, you will need to set the `base` option in `.vitepress/config.js`. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'` (it should always start and end with a slash). diff --git a/src/client/app/composables/preFetch.ts b/src/client/app/composables/preFetch.ts index 04492144..242cd9e5 100644 --- a/src/client/app/composables/preFetch.ts +++ b/src/client/app/composables/preFetch.ts @@ -66,7 +66,7 @@ export function usePrefetch() { if (!hasFetched.has(pathname)) { hasFetched.add(pathname) const pageChunkPath = pathToFile(pathname) - doFetch(pageChunkPath) + if (pageChunkPath) doFetch(pageChunkPath) } } }) @@ -76,7 +76,6 @@ export function usePrefetch() { document .querySelectorAll('#app a') .forEach((link) => { - const { target } = link const { hostname, pathname } = new URL( link.href instanceof SVGAnimatedString ? link.href.animVal @@ -91,7 +90,7 @@ export function usePrefetch() { if ( // only prefetch same tab navigation, since a new tab will load // the lean js chunk instead. - target !== `_blank` && + link.target !== '_blank' && // only prefetch inbound links hostname === location.hostname ) { diff --git a/src/client/app/index.ts b/src/client/app/index.ts index af5e7dee..a766b4da 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -1,23 +1,22 @@ +import RawTheme from '@theme/index' import { - type App, createApp as createClientApp, createSSRApp, defineComponent, h, onMounted, - watchEffect + watchEffect, + type App } from 'vue' -import RawTheme from '@theme/index' -import { inBrowser, pathToFile } from './utils' -import { type Router, RouterSymbol, createRouter, scrollTo } from './router' -import { siteDataRef, useData } from './data' -import { useUpdateHead } from './composables/head' -import { usePrefetch } from './composables/preFetch' -import { dataSymbol, initData } from './data' -import { Content } from './components/Content' import { ClientOnly } from './components/ClientOnly' -import { useCopyCode } from './composables/copyCode' +import { Content } from './components/Content' import { useCodeGroups } from './composables/codeGroups' +import { useCopyCode } from './composables/copyCode' +import { useUpdateHead } from './composables/head' +import { usePrefetch } from './composables/preFetch' +import { dataSymbol, initData, siteDataRef, useData } from './data' +import { RouterSymbol, createRouter, scrollTo, type Router } from './router' +import { inBrowser, pathToFile } from './utils' function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme { if (theme.extends) { @@ -123,6 +122,8 @@ function newRouter(): Router { return createRouter((path) => { let pageFilePath = pathToFile(path) + if (!pageFilePath) return null + if (isInitialPageLoad) { initialPath = pageFilePath } diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 1a790724..d1c82843 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -22,7 +22,7 @@ export const RouterSymbol: InjectionKey = Symbol() // we are just using URL to parse the pathname and hash - the base doesn't // matter and is only passed to support same-host hrefs. -const fakeHost = `http://a.com` +const fakeHost = 'http://a.com' const getDefaultRoute = (): Route => ({ path: '/', @@ -36,7 +36,7 @@ interface PageModule { } export function createRouter( - loadPageModule: (path: string) => Promise, + loadPageModule: (path: string) => Awaitable, fallbackComponent?: Component ): Router { const route = reactive(getDefaultRoute()) @@ -73,6 +73,9 @@ export function createRouter( const pendingPath = (latestPendingPath = targetLoc.pathname) try { let page = await loadPageModule(pendingPath) + if (!page) { + throw new Error(`Page not found: ${pendingPath}`) + } if (latestPendingPath === pendingPath) { latestPendingPath = null @@ -120,7 +123,10 @@ export function createRouter( } } } catch (err: any) { - if (!/fetch/.test(err.message) && !/^\/404(\.html|\/)?$/.test(href)) { + if ( + !/fetch|Page not found/.test(err.message) && + !/^\/404(\.html|\/)?$/.test(href) + ) { console.error(err) } @@ -176,7 +182,7 @@ export function createRouter( !e.shiftKey && !e.altKey && !e.metaKey && - target !== `_blank` && + !target && origin === currentUrl.origin && // don't intercept if non-html extension is present !(extMatch && extMatch[0] !== '.html') diff --git a/src/client/app/utils.ts b/src/client/app/utils.ts index fe103dc1..a399609b 100644 --- a/src/client/app/utils.ts +++ b/src/client/app/utils.ts @@ -18,7 +18,7 @@ export { inBrowser } from '../shared' /** * Join two paths by resolving the slash collision. */ -export function joinPath(base: string, path: string): string { +export function joinPath(base: string, path: string) { return `${base}${path}`.replace(/\/+/g, '/') } @@ -31,7 +31,7 @@ export function withBase(path: string) { /** * Converts a url path to the corresponding js chunk filename. */ -export function pathToFile(path: string): string { +export function pathToFile(path: string) { let pagePath = path.replace(/\.html$/, '') pagePath = decodeURIComponent(pagePath) pagePath = pagePath.replace(/\/$/, '/index') // /foo/ -> /foo/index @@ -57,6 +57,7 @@ export function pathToFile(path: string): string { : pagePath.slice(0, -3) + '_index.md' pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()] } + if (!pageHash) return null pagePath = `${base}assets/${pagePath}.${pageHash}.js` } else { // ssr build uses much simpler name mapping diff --git a/src/client/theme-default/components/VPLocalSearchBox.vue b/src/client/theme-default/components/VPLocalSearchBox.vue index 4ae76ef9..e5b789f3 100644 --- a/src/client/theme-default/components/VPLocalSearchBox.vue +++ b/src/client/theme-default/components/VPLocalSearchBox.vue @@ -230,6 +230,7 @@ debouncedWatch( async function fetchExcerpt(id: string) { const file = pathToFile(id.slice(0, id.indexOf('#'))) try { + if (!file) throw new Error(`Cannot find file for id: ${id}`) return { id, mod: await import(/*@vite-ignore*/ file) } } catch (e) { console.error(e) diff --git a/src/client/theme-default/support/utils.ts b/src/client/theme-default/support/utils.ts index acb8fcb9..e5fa8bca 100644 --- a/src/client/theme-default/support/utils.ts +++ b/src/client/theme-default/support/utils.ts @@ -33,7 +33,7 @@ export function normalizeLink(url: string): string { } const { site } = useData() - const { pathname, search, hash } = new URL(url, 'http://example.com') + const { pathname, search, hash } = new URL(url, 'http://a.com') const normalizedPath = pathname.endsWith('/') || pathname.endsWith('.html')