From 6d89a08cb76674f4d92f54218f8af5624bcf4c47 Mon Sep 17 00:00:00 2001 From: arianrhodsandlot Date: Sat, 30 Dec 2023 18:32:31 +0800 Subject: [PATCH 1/5] fix: print errors when importing an invalid dynamic route (#3201) Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> --- src/node/config.ts | 3 +- src/node/plugin.ts | 6 +++- src/node/plugins/dynamicRoutesPlugin.ts | 38 ++++++++++++++++--------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/node/config.ts b/src/node/config.ts index ee0e2e94..d4a76de6 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -104,7 +104,8 @@ export async function resolveConfig( const { pages, dynamicRoutes, rewrites } = await resolvePages( srcDir, - userConfig + userConfig, + logger ) const config: SiteConfig = { diff --git a/src/node/plugin.ts b/src/node/plugin.ts index c079dad6..0c3daddc 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -268,7 +268,11 @@ export async function createVitePressPlugin( if (file.endsWith('.md')) { Object.assign( siteConfig, - await resolvePages(siteConfig.srcDir, siteConfig.userConfig) + await resolvePages( + siteConfig.srcDir, + siteConfig.userConfig, + siteConfig.logger + ) ) } diff --git a/src/node/plugins/dynamicRoutesPlugin.ts b/src/node/plugins/dynamicRoutesPlugin.ts index e42e8c13..cf9576ae 100644 --- a/src/node/plugins/dynamicRoutesPlugin.ts +++ b/src/node/plugins/dynamicRoutesPlugin.ts @@ -1,6 +1,7 @@ import { loadConfigFromFile, normalizePath, + type Logger, type Plugin, type ViteDevServer } from 'vite' @@ -13,7 +14,11 @@ import { resolveRewrites } from './rewritesPlugin' export const dynamicRouteRE = /\[(\w+?)\]/g -export async function resolvePages(srcDir: string, userConfig: UserConfig) { +export async function resolvePages( + srcDir: string, + userConfig: UserConfig, + logger: Logger +) { // Important: fast-glob doesn't guarantee order of the returned files. // We must sort the pages so the input list to rollup is stable across // builds - otherwise different input order could result in different exports @@ -39,7 +44,11 @@ export async function resolvePages(srcDir: string, userConfig: UserConfig) { ;(dynamicRouteRE.test(file) ? dynamicRouteFiles : pages).push(file) }) - const dynamicRoutes = await resolveDynamicRoutes(srcDir, dynamicRouteFiles) + const dynamicRoutes = await resolveDynamicRoutes( + srcDir, + dynamicRouteFiles, + logger + ) pages.push(...dynamicRoutes.routes.map((r) => r.path)) const rewrites = resolveRewrites(pages, userConfig.rewrites) @@ -141,7 +150,7 @@ export const dynamicRoutesPlugin = async ( if (!/\.md$/.test(ctx.file)) { Object.assign( config, - await resolvePages(config.srcDir, config.userConfig) + await resolvePages(config.srcDir, config.userConfig, config.logger) ) } for (const id of mods) { @@ -154,7 +163,8 @@ export const dynamicRoutesPlugin = async ( export async function resolveDynamicRoutes( srcDir: string, - routes: string[] + routes: string[], + logger: Logger ): Promise { const pendingResolveRoutes: Promise[] = [] const routeFileToModulesMap: Record> = {} @@ -170,7 +180,7 @@ export async function resolveDynamicRoutes( const pathsFile = paths.find((p) => fs.existsSync(p)) if (pathsFile == null) { - console.warn( + logger.warn( c.yellow( `Missing paths file for dynamic route ${route}: ` + `a corresponding ${paths[0]} (or .ts/.mjs/.mts) file is needed.` @@ -183,15 +193,15 @@ export async function resolveDynamicRoutes( let mod = routeModuleCache.get(pathsFile) if (!mod) { try { - mod = (await loadConfigFromFile({} as any, pathsFile)) as RouteModule + mod = (await loadConfigFromFile( + {} as any, + pathsFile, + undefined, + 'silent' + )) as RouteModule routeModuleCache.set(pathsFile, mod) - } catch (e) { - console.warn( - c.yellow( - `Invalid paths file export in ${pathsFile}. ` + - `Expects default export of an object with a "paths" property.` - ) - ) + } catch (e: any) { + logger.warn(`${c.yellow(`Failed to load ${pathsFile}:`)}\n${e.stack}`) continue } } @@ -210,7 +220,7 @@ export async function resolveDynamicRoutes( const loader = mod!.config.paths if (!loader) { - console.warn( + logger.warn( c.yellow( `Invalid paths file export in ${pathsFile}. ` + `Missing "paths" property from default export.` From bb64ecba3db9ba8ffa34d17700fdd1d8c28767ab Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sat, 30 Dec 2023 11:41:37 +0100 Subject: [PATCH 2/5] docs: dynamic site config (#3266) Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> --- docs/reference/site-config.md | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/reference/site-config.md b/docs/reference/site-config.md index b5e124d2..1cf9d726 100644 --- a/docs/reference/site-config.md +++ b/docs/reference/site-config.md @@ -24,6 +24,62 @@ export default { } ``` +:::details Dynamic (Async) Config + +If you need to dynamically generate the config, you can also default export a function. For example: + +```ts +import { defineConfig } from 'vitepress' + +export default async () => defineConfig({ + const posts = await (await fetch('https://my-cms.com/blog-posts')).json() + + return { + // app level config options + lang: 'en-US', + title: 'VitePress', + description: 'Vite & Vue powered static site generator.', + + // theme level config options + themeConfig: { + sidebar: [ + ...posts.map((post) => ({ + text: post.name, + link: `/posts/${post.name}` + })) + ] + } + } +}) +``` + +You can also use top-level `await`. For example: + +```ts +import { defineConfig } from 'vitepress' + +const posts = await (await fetch('https://my-cms.com/blog-posts')).json() + +export default defineConfig({ + // app level config options + lang: 'en-US', + title: 'VitePress', + description: 'Vite & Vue powered static site generator.', + + // theme level config options + themeConfig: { + sidebar: [ + ...posts.map((post) => ({ + text: post.name, + link: `/posts/${post.name}` + })) + ] + } +}) +``` + +::: + ### Config Intellisense Using the `defineConfig` helper will provide TypeScript-powered intellisense for config options. Assuming your IDE supports it, this should work in both JavaScript and TypeScript. From 50c9758d3fa1b60aad5399a0db890644ac44a522 Mon Sep 17 00:00:00 2001 From: Cesar Gomez <65884507+mrcego@users.noreply.github.com> Date: Sat, 30 Dec 2023 07:48:07 -0500 Subject: [PATCH 3/5] fix(theme/i18n): support customizing dark mode switch titles (#3311) Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> --- docs/reference/default-theme-config.md | 14 ++++++++++++++ .../components/VPSwitchAppearance.vue | 6 ++++-- types/default-theme.d.ts | 10 ++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/reference/default-theme-config.md b/docs/reference/default-theme-config.md index 8402dc08..fb6d30b5 100644 --- a/docs/reference/default-theme-config.md +++ b/docs/reference/default-theme-config.md @@ -406,6 +406,20 @@ export interface DocFooter { Can be used to customize the dark mode switch label. This label is only displayed in the mobile view. +## lightModeSwitchTitle + +- Type: `string` +- Default: `Switch to light theme` + +Can be used to customize the light mode switch title that appears on hovering. + +## darkModeSwitchTitle + +- Type: `string` +- Default: `Switch to dark theme` + +Can be used to customize the dark mode switch title that appears on hovering. + ## sidebarMenuLabel - Type: `string` diff --git a/src/client/theme-default/components/VPSwitchAppearance.vue b/src/client/theme-default/components/VPSwitchAppearance.vue index 16828e7e..f63d1ce4 100644 --- a/src/client/theme-default/components/VPSwitchAppearance.vue +++ b/src/client/theme-default/components/VPSwitchAppearance.vue @@ -5,14 +5,16 @@ import VPSwitch from './VPSwitch.vue' import VPIconMoon from './icons/VPIconMoon.vue' import VPIconSun from './icons/VPIconSun.vue' -const { isDark } = useData() +const { isDark, theme } = useData() const toggleAppearance = inject('toggle-appearance', () => { isDark.value = !isDark.value }) const switchTitle = computed(() => { - return isDark.value ? 'Switch to light theme' : 'Switch to dark theme' + return isDark.value + ? theme.value.lightModeSwitchTitle || 'Switch to light theme' + : theme.value.darkModeSwitchTitle || 'Switch to dark theme' }) diff --git a/types/default-theme.d.ts b/types/default-theme.d.ts index c89551b0..157faa00 100644 --- a/types/default-theme.d.ts +++ b/types/default-theme.d.ts @@ -96,6 +96,16 @@ export namespace DefaultTheme { */ darkModeSwitchLabel?: string + /** + * @default 'Switch to light theme' + */ + lightModeSwitchTitle?: string + + /** + * @default 'Switch to dark theme' + */ + darkModeSwitchTitle?: string + /** * @default 'Menu' */ From 203446d69193483a46e1082bba5fbad0e35966fb Mon Sep 17 00:00:00 2001 From: Jinjiang Date: Sat, 30 Dec 2023 21:02:22 +0800 Subject: [PATCH 4/5] feat: support dir in frontmatter (#3353) Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> --- src/client/app/data.ts | 2 +- src/client/theme-default/styles/base.css | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/app/data.ts b/src/client/app/data.ts index 77cc80d3..ccca8123 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -89,7 +89,7 @@ export function initData(route: Route): VitePressData { frontmatter: computed(() => route.data.frontmatter), params: computed(() => route.data.params), lang: computed(() => site.value.lang), - dir: computed(() => site.value.dir), + dir: computed(() => route.data.frontmatter.dir || site.value.dir || 'ltr'), localeIndex: computed(() => site.value.localeIndex || 'root'), title: computed(() => { return createTitle(site.value, route.data) diff --git a/src/client/theme-default/styles/base.css b/src/client/theme-default/styles/base.css index 5fb3a3cc..af2ed98a 100644 --- a/src/client/theme-default/styles/base.css +++ b/src/client/theme-default/styles/base.css @@ -39,7 +39,6 @@ body { font-weight: 400; color: var(--vp-c-text-1); background-color: var(--vp-c-bg); - direction: ltr; font-synthesis: style; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; From 55be3f14d79eb578c9aa2e3bc7a90205c910005d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=83=BD=E5=AE=81?= Date: Sat, 30 Dec 2023 21:26:47 +0800 Subject: [PATCH 5/5] feat: support custom image lazy loading (#3346) Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> --- __tests__/e2e/.vitepress/config.ts | 5 +++++ __tests__/e2e/markdown-extensions/index.md | 4 ++++ .../markdown-extensions.test.ts | 9 ++++++++- docs/guide/markdown.md | 15 +++++++++++++++ src/node/markdown/markdown.ts | 5 +++-- src/node/markdown/plugins/image.ts | 13 ++++++++++++- 6 files changed, 47 insertions(+), 4 deletions(-) diff --git a/__tests__/e2e/.vitepress/config.ts b/__tests__/e2e/.vitepress/config.ts index 84fc64f2..e4e503a6 100644 --- a/__tests__/e2e/.vitepress/config.ts +++ b/__tests__/e2e/.vitepress/config.ts @@ -86,6 +86,11 @@ const sidebar: DefaultTheme.Config['sidebar'] = { export default defineConfig({ title: 'Example', description: 'An example app using VitePress.', + markdown: { + image: { + lazyLoading: true + } + }, themeConfig: { sidebar, search: { diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md index 36d6c028..47246c18 100644 --- a/__tests__/e2e/markdown-extensions/index.md +++ b/__tests__/e2e/markdown-extensions/index.md @@ -196,3 +196,7 @@ export default config ## Markdown File Inclusion with Range without End + +## Image Lazy Loading + +![vitepress logo](/vitepress.png) \ No newline at end of file diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 42a89891..23b8b1a0 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -65,7 +65,7 @@ describe('Table of Contents', () => { test('render toc', async () => { const items = page.locator('#table-of-contents + nav ul li') const count = await items.count() - expect(count).toBe(35) + expect(count).toBe(36) }) }) @@ -280,3 +280,10 @@ describe('Markdown File Inclusion', () => { expect(await p.textContent()).not.toContain('title') }) }) + +describe('Image Lazy Loading', () => { + test('render loading="lazy" in the tag', async () => { + const img = page.locator('#image-lazy-loading + p img') + expect(await img.getAttribute('loading')).toBe('lazy') + }) +}) diff --git a/docs/guide/markdown.md b/docs/guide/markdown.md index 8cd9d4ed..ef3cc0e8 100644 --- a/docs/guide/markdown.md +++ b/docs/guide/markdown.md @@ -847,6 +847,21 @@ $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ | $\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} = \vec{\mathbf{0}}$ | curl of $\vec{\mathbf{E}}$ is proportional to the rate of change of $\vec{\mathbf{B}}$ | | $\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} = \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} = 4 \pi \rho$ | _wha?_ | +## Image Lazy Loading + +You can enable lazy loading for each image added via markdown by setting `lazyLoading` to `true` in your config file: + +```js +export default { + markdown: { + image: { + // image lazy loading is disabled by default + lazyLoading: true + } + } +} +``` + ## Advanced Configuration VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`: diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index f93fd837..e64a1bde 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -23,7 +23,7 @@ import type { Logger } from 'vite' import { containerPlugin, type ContainerOptions } from './plugins/containers' import { highlight } from './plugins/highlight' import { highlightLinePlugin } from './plugins/highlightLines' -import { imagePlugin } from './plugins/image' +import { imagePlugin, type Options as ImageOptions } from './plugins/image' import { lineNumberPlugin } from './plugins/lineNumbers' import { linkPlugin } from './plugins/link' import { preWrapperPlugin } from './plugins/preWrapper' @@ -166,6 +166,7 @@ export interface MarkdownOptions extends MarkdownIt.Options { * @see https://vitepress.dev/guide/markdown#math-equations */ math?: boolean | any + image?: ImageOptions } export type MarkdownRenderer = MarkdownIt @@ -198,7 +199,7 @@ export const createMarkdownRenderer = async ( .use(preWrapperPlugin, { hasSingleTheme }) .use(snippetPlugin, srcDir) .use(containerPlugin, { hasSingleTheme }, options.container) - .use(imagePlugin) + .use(imagePlugin, options.image) .use( linkPlugin, { target: '_blank', rel: 'noreferrer', ...options.externalLinks }, diff --git a/src/node/markdown/plugins/image.ts b/src/node/markdown/plugins/image.ts index 06d1a360..d6f7eb5f 100644 --- a/src/node/markdown/plugins/image.ts +++ b/src/node/markdown/plugins/image.ts @@ -3,7 +3,15 @@ import type MarkdownIt from 'markdown-it' import { EXTERNAL_URL_RE } from '../../shared' -export const imagePlugin = (md: MarkdownIt) => { +export interface Options { + /** + * Support native lazy loading for the `` tag. + * @default false + */ + lazyLoading?: boolean +} + +export const imagePlugin = (md: MarkdownIt, { lazyLoading }: Options = {}) => { const imageRule = md.renderer.rules.image! md.renderer.rules.image = (tokens, idx, options, env, self) => { const token = tokens[idx] @@ -12,6 +20,9 @@ export const imagePlugin = (md: MarkdownIt) => { if (!/^\.?\//.test(url)) url = './' + url token.attrSet('src', decodeURIComponent(url)) } + if (lazyLoading) { + token.attrSet('loading', 'lazy') + } return imageRule(tokens, idx, options, env, self) } }