From b4ff6a74f3de3c50cc9823d0d47c1f0d80448a5f Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 00:57:03 +0800
Subject: [PATCH 01/11] refactor: remove temp page files and load page
 component via bundler

---
 .../components/ComponentForMarkdownImport.vue |   5 -
 .../ComponentForMarkdownImportBar.vue         |   5 +
 .../ComponentForMarkdownImportFoo.vue         |   5 +
 .../markdowns/dangling-markdown-file.md       |   5 +
 e2e/docs/README.md                            |   2 +
 e2e/docs/markdown/vue-components.md           |  11 +-
 e2e/tests/markdown/vue-components.spec.ts     |   7 +-
 packages/bundler-vite/src/plugins/index.ts    |   1 +
 .../src/plugins/vuepressMarkdownPlugin.ts     |  53 +++
 .../src/plugins/vuepressVuePlugin.ts          |   1 +
 .../bundler-vite/src/resolveViteConfig.ts     |   2 +
 packages/bundler-webpack/package.json         |   1 +
 .../src/build/createClientConfig.ts           |  12 -
 .../src/build/createServerConfig.ts           |  15 -
 .../src/config/createBaseConfig.ts            |   2 +-
 .../src/config/handleModule.ts                |   5 +-
 .../src/config/handleModuleVue.ts             |  62 +++-
 .../src/loaders/vuepressMarkdownLoader.cts    |   3 +
 .../src/loaders/vuepressMarkdownLoader.ts     |  37 +++
 packages/bundler-webpack/tsup.config.ts       |   1 +
 packages/core/src/app/appInit.ts              |   4 +-
 .../src/app/prepare/preparePageComponent.ts   |  12 +-
 packages/core/src/app/resolveAppPages.ts      |  11 +-
 packages/core/src/page/createPage.ts          |   2 +
 .../core/src/page/resolvePageComponentInfo.ts |  13 +-
 packages/core/src/types/app/app.ts            |   7 +
 .../core/tests/app/resolveAppPages.spec.ts    |   8 +-
 .../page/resolvePageComponentInfo.spec.ts     |  18 +-
 .../src/plugins/assetsPlugin/assetsPlugin.ts  |  17 +-
 .../src/plugins/assetsPlugin/resolveLink.ts   |  24 +-
 .../tests/plugins/assetsPlugin.spec.ts        | 313 ++++--------------
 31 files changed, 319 insertions(+), 345 deletions(-)
 delete mode 100644 e2e/docs/.vuepress/components/ComponentForMarkdownImport.vue
 create mode 100644 e2e/docs/.vuepress/components/ComponentForMarkdownImportBar.vue
 create mode 100644 e2e/docs/.vuepress/components/ComponentForMarkdownImportFoo.vue
 create mode 100644 e2e/docs/.vuepress/markdowns/dangling-markdown-file.md
 create mode 100644 packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
 create mode 100644 packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.cts
 create mode 100644 packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts

diff --git a/e2e/docs/.vuepress/components/ComponentForMarkdownImport.vue b/e2e/docs/.vuepress/components/ComponentForMarkdownImport.vue
deleted file mode 100644
index 7108c4a426..0000000000
--- a/e2e/docs/.vuepress/components/ComponentForMarkdownImport.vue
+++ /dev/null
@@ -1,5 +0,0 @@
-<template>
-  <div class="component-for-markdown-import">
-    <p>component for markdown import</p>
-  </div>
-</template>
diff --git a/e2e/docs/.vuepress/components/ComponentForMarkdownImportBar.vue b/e2e/docs/.vuepress/components/ComponentForMarkdownImportBar.vue
new file mode 100644
index 0000000000..0d9fa2f2f6
--- /dev/null
+++ b/e2e/docs/.vuepress/components/ComponentForMarkdownImportBar.vue
@@ -0,0 +1,5 @@
+<template>
+  <div class="component-for-markdown-import-bar">
+    <p>component for markdown import bar</p>
+  </div>
+</template>
diff --git a/e2e/docs/.vuepress/components/ComponentForMarkdownImportFoo.vue b/e2e/docs/.vuepress/components/ComponentForMarkdownImportFoo.vue
new file mode 100644
index 0000000000..3905e34a83
--- /dev/null
+++ b/e2e/docs/.vuepress/components/ComponentForMarkdownImportFoo.vue
@@ -0,0 +1,5 @@
+<template>
+  <div class="component-for-markdown-import-foo">
+    <p>component for markdown import foo</p>
+  </div>
+</template>
diff --git a/e2e/docs/.vuepress/markdowns/dangling-markdown-file.md b/e2e/docs/.vuepress/markdowns/dangling-markdown-file.md
new file mode 100644
index 0000000000..5bf006ada9
--- /dev/null
+++ b/e2e/docs/.vuepress/markdowns/dangling-markdown-file.md
@@ -0,0 +1,5 @@
+<div class="dangling-markdown-file">
+
+dangling markdown file
+
+</div>
diff --git a/e2e/docs/README.md b/e2e/docs/README.md
index eb63f0ccbf..b3586d97fe 100644
--- a/e2e/docs/README.md
+++ b/e2e/docs/README.md
@@ -1,3 +1,5 @@
 foo
 
 ## Home H2
+
+demo
diff --git a/e2e/docs/markdown/vue-components.md b/e2e/docs/markdown/vue-components.md
index c038c08795..f38b703d0b 100644
--- a/e2e/docs/markdown/vue-components.md
+++ b/e2e/docs/markdown/vue-components.md
@@ -1,8 +1,13 @@
 <ComponentForMarkdownGlobal />
 
-<ComponentForMarkdownImport />
+<ComponentForMarkdownImportFoo />
+
+<ComponentForMarkdownImportBar />
 
 <script setup>
-// TODO: relative path import?
-import ComponentForMarkdownImport from '@source/.vuepress/components/ComponentForMarkdownImport.vue';
+// import via alias
+import ComponentForMarkdownImportFoo from '@source/.vuepress/components/ComponentForMarkdownImportFoo.vue';
+
+// import via relative path
+import ComponentForMarkdownImportBar from '../.vuepress/components/ComponentForMarkdownImportBar.vue';
 </script>
diff --git a/e2e/tests/markdown/vue-components.spec.ts b/e2e/tests/markdown/vue-components.spec.ts
index f33b1dbc68..1f3f1818d6 100644
--- a/e2e/tests/markdown/vue-components.spec.ts
+++ b/e2e/tests/markdown/vue-components.spec.ts
@@ -6,7 +6,10 @@ test('should render vue components correctly', async ({ page }) => {
   await expect(page.locator('.component-for-markdown-global p')).toHaveText(
     'component for markdown global',
   )
-  await expect(page.locator('.component-for-markdown-import p')).toHaveText(
-    'component for markdown import',
+  await expect(page.locator('.component-for-markdown-import-foo p')).toHaveText(
+    'component for markdown import foo',
+  )
+  await expect(page.locator('.component-for-markdown-import-bar p')).toHaveText(
+    'component for markdown import bar',
   )
 })
diff --git a/packages/bundler-vite/src/plugins/index.ts b/packages/bundler-vite/src/plugins/index.ts
index 0268dc0dba..9ff20c8972 100644
--- a/packages/bundler-vite/src/plugins/index.ts
+++ b/packages/bundler-vite/src/plugins/index.ts
@@ -1,5 +1,6 @@
 export * from './vuepressBuildPlugin.js'
 export * from './vuepressConfigPlugin.js'
 export * from './vuepressDevPlugin.js'
+export * from './vuepressMarkdownPlugin.js'
 export * from './vuepressUserConfigPlugin.js'
 export * from './vuepressVuePlugin.js'
diff --git a/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts b/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
new file mode 100644
index 0000000000..eb072f840c
--- /dev/null
+++ b/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
@@ -0,0 +1,53 @@
+import type { App } from '@vuepress/core'
+import { parsePageContent, renderPageSfcBlocksToVue } from '@vuepress/core'
+import { path } from '@vuepress/utils'
+import type { Plugin } from 'vite'
+
+/**
+ * Handle markdown transformation
+ */
+export const vuepressMarkdownPlugin = ({ app }: { app: App }): Plugin => ({
+  name: 'vuepress:markdown',
+
+  enforce: 'pre',
+
+  transform(code, id) {
+    if (!id.endsWith('.md')) return
+
+    // get the matched page by file path (id)
+    const page = app.pagesMap[id]
+
+    // if the page content is not changed, render it to vue component directly
+    if (page?.content === code) {
+      return renderPageSfcBlocksToVue(page.sfcBlocks)
+    }
+
+    // parse the markdown content to sfc blocks and render it to vue component
+    const { sfcBlocks } = parsePageContent({
+      app,
+      content: code,
+      filePath: id,
+      filePathRelative: path.relative(app.dir.source(), id),
+      options: {},
+    })
+    return renderPageSfcBlocksToVue(sfcBlocks)
+  },
+
+  async handleHotUpdate(ctx) {
+    if (!ctx.file.endsWith('.md')) return
+
+    // read the source code
+    const code = await ctx.read()
+
+    // parse the content to sfc blocks
+    const { sfcBlocks } = parsePageContent({
+      app,
+      content: code,
+      filePath: ctx.file,
+      filePathRelative: path.relative(app.dir.source(), ctx.file),
+      options: {},
+    })
+
+    ctx.read = () => renderPageSfcBlocksToVue(sfcBlocks)
+  },
+})
diff --git a/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts b/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts
index d85656c731..5688e4c2fb 100644
--- a/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts
+++ b/packages/bundler-vite/src/plugins/vuepressVuePlugin.ts
@@ -11,5 +11,6 @@ export const vuepressVuePlugin = ({
   options: ViteBundlerOptions
 }): Plugin =>
   vuePlugin({
+    include: [/\.vue$/, /\.md$/],
     ...options.vuePluginOptions,
   })
diff --git a/packages/bundler-vite/src/resolveViteConfig.ts b/packages/bundler-vite/src/resolveViteConfig.ts
index 6fafcec449..f950836f92 100644
--- a/packages/bundler-vite/src/resolveViteConfig.ts
+++ b/packages/bundler-vite/src/resolveViteConfig.ts
@@ -5,6 +5,7 @@ import {
   vuepressBuildPlugin,
   vuepressConfigPlugin,
   vuepressDevPlugin,
+  vuepressMarkdownPlugin,
   vuepressUserConfigPlugin,
   vuepressVuePlugin,
 } from './plugins/index.js'
@@ -31,6 +32,7 @@ export const resolveViteConfig = ({
       },
       plugins: [
         vuepressConfigPlugin({ app, isBuild, isServer }),
+        vuepressMarkdownPlugin({ app }),
         vuepressDevPlugin({ app }),
         vuepressBuildPlugin({ isServer }),
         vuepressVuePlugin({ options }),
diff --git a/packages/bundler-webpack/package.json b/packages/bundler-webpack/package.json
index 28d32a8f12..893252e963 100644
--- a/packages/bundler-webpack/package.json
+++ b/packages/bundler-webpack/package.json
@@ -20,6 +20,7 @@
   "author": "meteorlxy",
   "type": "module",
   "imports": {
+    "#vuepress-markdown-loader": "./dist/vuepress-markdown-loader.cjs",
     "#vuepress-ssr-loader": "./dist/vuepress-ssr-loader.cjs"
   },
   "exports": {
diff --git a/packages/bundler-webpack/src/build/createClientConfig.ts b/packages/bundler-webpack/src/build/createClientConfig.ts
index 6266b9bda1..7f0ae415de 100644
--- a/packages/bundler-webpack/src/build/createClientConfig.ts
+++ b/packages/bundler-webpack/src/build/createClientConfig.ts
@@ -1,4 +1,3 @@
-import { createRequire } from 'node:module'
 import type { App } from '@vuepress/core'
 import { fs } from '@vuepress/utils'
 import CopyWebpackPlugin from 'copy-webpack-plugin'
@@ -10,8 +9,6 @@ import { createClientBaseConfig } from '../config/index.js'
 import type { WebpackBundlerOptions } from '../types.js'
 import { createClientPlugin } from './createClientPlugin.js'
 
-const require = createRequire(import.meta.url)
-
 /**
  * Filename of the client manifest file that generated by client plugin
  */
@@ -27,15 +24,6 @@ export const createClientConfig = async (
     isBuild: true,
   })
 
-  // use internal vuepress-ssr-loader to handle SSR dependencies
-  config.module
-    .rule('vue')
-    .test(/\.vue$/)
-    .use('vuepress-ssr-loader')
-    .before('vue-loader')
-    .loader(require.resolve('#vuepress-ssr-loader'))
-    .end()
-
   // vuepress client plugin, handle client assets info for ssr
   config
     .plugin('vuepress-client')
diff --git a/packages/bundler-webpack/src/build/createServerConfig.ts b/packages/bundler-webpack/src/build/createServerConfig.ts
index d21b75b034..600141751d 100644
--- a/packages/bundler-webpack/src/build/createServerConfig.ts
+++ b/packages/bundler-webpack/src/build/createServerConfig.ts
@@ -1,11 +1,8 @@
-import { createRequire } from 'node:module'
 import type { App } from '@vuepress/core'
 import type Config from 'webpack-5-chain'
 import { createBaseConfig } from '../config/index.js'
 import type { WebpackBundlerOptions } from '../types.js'
 
-const require = createRequire(import.meta.url)
-
 export const createServerConfig = async (
   app: App,
   options: WebpackBundlerOptions,
@@ -43,17 +40,5 @@ export const createServerConfig = async (
   // do not need to minimize server bundle
   config.optimization.minimize(false)
 
-  // use internal vuepress-ssr-loader to handle SSR dependencies
-  config.module
-    .rule('vue')
-    .test(/\.vue$/)
-    .use('vuepress-ssr-loader')
-    .before('vue-loader')
-    .loader(require.resolve('#vuepress-ssr-loader'))
-    .options({
-      app,
-    })
-    .end()
-
   return config
 }
diff --git a/packages/bundler-webpack/src/config/createBaseConfig.ts b/packages/bundler-webpack/src/config/createBaseConfig.ts
index 0b6e0accc3..b6d8bec3b0 100644
--- a/packages/bundler-webpack/src/config/createBaseConfig.ts
+++ b/packages/bundler-webpack/src/config/createBaseConfig.ts
@@ -52,7 +52,7 @@ export const createBaseConfig = async ({
   /**
    * module
    */
-  handleModule({ options, config, isBuild, isServer })
+  handleModule({ app, options, config, isBuild, isServer })
 
   /**
    * plugins
diff --git a/packages/bundler-webpack/src/config/handleModule.ts b/packages/bundler-webpack/src/config/handleModule.ts
index be33ca6a73..14d8a57b0c 100644
--- a/packages/bundler-webpack/src/config/handleModule.ts
+++ b/packages/bundler-webpack/src/config/handleModule.ts
@@ -1,3 +1,4 @@
+import type { App } from '@vuepress/core'
 import type Config from 'webpack-5-chain'
 import type { WebpackBundlerOptions } from '../types.js'
 import { handleModuleAssets } from './handleModuleAssets.js'
@@ -11,11 +12,13 @@ import { handleModuleVue } from './handleModuleVue.js'
  * Set webpack module
  */
 export const handleModule = ({
+  app,
   options,
   config,
   isBuild,
   isServer,
 }: {
+  app: App
   options: WebpackBundlerOptions
   config: Config
   isBuild: boolean
@@ -27,7 +30,7 @@ export const handleModule = ({
   )
 
   // vue files
-  handleModuleVue({ options, config, isServer })
+  handleModuleVue({ app, options, config, isBuild, isServer })
 
   // pug files, for templates
   handleModulePug({ config })
diff --git a/packages/bundler-webpack/src/config/handleModuleVue.ts b/packages/bundler-webpack/src/config/handleModuleVue.ts
index 335159cc1b..4065ff16a7 100644
--- a/packages/bundler-webpack/src/config/handleModuleVue.ts
+++ b/packages/bundler-webpack/src/config/handleModuleVue.ts
@@ -1,7 +1,9 @@
 import { createRequire } from 'node:module'
+import type { App } from '@vuepress/core'
 import type { VueLoaderOptions } from 'vue-loader'
 import { VueLoaderPlugin } from 'vue-loader'
 import type Config from 'webpack-5-chain'
+import type { VuepressMarkdownLoaderOptions } from '../loaders/vuepressMarkdownLoader'
 import type { WebpackBundlerOptions } from '../types.js'
 
 const require = createRequire(import.meta.url)
@@ -10,26 +12,62 @@ const require = createRequire(import.meta.url)
  * Set webpack module to handle vue files
  */
 export const handleModuleVue = ({
+  app,
   options,
   config,
+  isBuild,
   isServer,
 }: {
+  app: App
   options: WebpackBundlerOptions
   config: Config
+  isBuild: boolean
   isServer: boolean
 }): void => {
-  // .vue files
-  config.module
-    .rule('vue')
-    .test(/\.vue$/)
-    // use vue-loader
-    .use('vue-loader')
-    .loader(require.resolve('vue-loader'))
-    .options({
-      ...options.vue,
-      isServerBuild: isServer,
-    } as VueLoaderOptions)
-    .end()
+  const applyVuePipeline = ({
+    rule,
+    isMd,
+  }: {
+    rule: Config.Rule
+    isMd: boolean
+  }): void => {
+    // use internal vuepress-ssr-loader to handle SSR dependencies
+    if (isBuild) {
+      rule
+        .use('vuepress-ssr-loader')
+        .loader(require.resolve('#vuepress-ssr-loader'))
+        .end()
+    }
+
+    // use official vue-loader
+    rule
+      .use('vue-loader')
+      .loader(require.resolve('vue-loader'))
+      .options({
+        ...options.vue,
+        isServerBuild: isServer,
+      } satisfies VueLoaderOptions)
+      .end()
+
+    // use internal vuepress-markdown-loader to handle markdown files
+    if (isMd) {
+      rule
+        .use('vuepress-markdown-loader')
+        .loader(require.resolve('#vuepress-markdown-loader'))
+        .options({ app } satisfies VuepressMarkdownLoaderOptions)
+        .end()
+    }
+  }
+
+  applyVuePipeline({
+    rule: config.module.rule('md').test(/\.md$/),
+    isMd: true,
+  })
+
+  applyVuePipeline({
+    rule: config.module.rule('vue').test(/\.vue$/),
+    isMd: false,
+  })
 
   // use vue-loader plugin
   config.plugin('vue-loader').use(VueLoaderPlugin)
diff --git a/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.cts b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.cts
new file mode 100644
index 0000000000..f4b3001431
--- /dev/null
+++ b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.cts
@@ -0,0 +1,3 @@
+const loader = require('./vuepressMarkdownLoader.js')
+
+module.exports = loader.vuepressMarkdownLoader
diff --git a/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts
new file mode 100644
index 0000000000..d9ca435d14
--- /dev/null
+++ b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts
@@ -0,0 +1,37 @@
+import type { App } from '@vuepress/core'
+import type { LoaderDefinitionFunction } from 'webpack'
+
+export interface VuepressMarkdownLoaderOptions {
+  app: App
+}
+
+/**
+ * A webpack loader to transform markdown content to vue component
+ */
+export const vuepressMarkdownLoader: LoaderDefinitionFunction<VuepressMarkdownLoaderOptions> =
+  async function vuepressMarkdownLoader(source) {
+    // import esm dependencies
+    const [{ parsePageContent, renderPageSfcBlocksToVue }, { path }] =
+      await Promise.all([import('@vuepress/core'), import('@vuepress/utils')])
+
+    // get app instance from loader options
+    const { app } = this.getOptions()
+
+    // get the matched page by file path
+    const page = app.pagesMap[this.resourcePath]
+
+    // if the page content is not changed, render it to vue component directly
+    if (page?.content === source) {
+      return renderPageSfcBlocksToVue(page.sfcBlocks)
+    }
+
+    // parse the markdown content to sfc blocks and render it to vue component
+    const { sfcBlocks } = parsePageContent({
+      app,
+      content: source,
+      filePath: this.resourcePath,
+      filePathRelative: path.relative(app.dir.source(), this.resourcePath),
+      options: {},
+    })
+    return renderPageSfcBlocksToVue(sfcBlocks)
+  }
diff --git a/packages/bundler-webpack/tsup.config.ts b/packages/bundler-webpack/tsup.config.ts
index 4899bc89aa..9f8379c060 100644
--- a/packages/bundler-webpack/tsup.config.ts
+++ b/packages/bundler-webpack/tsup.config.ts
@@ -18,6 +18,7 @@ export default defineConfig([
   {
     ...shared,
     entry: {
+      'vuepress-markdown-loader': './src/loaders/vuepressMarkdownLoader.cts',
       'vuepress-ssr-loader': './src/loaders/vuepressSsrLoader.cts',
     },
     format: ['cjs'],
diff --git a/packages/core/src/app/appInit.ts b/packages/core/src/app/appInit.ts
index 1db287fd28..ac485cfe65 100644
--- a/packages/core/src/app/appInit.ts
+++ b/packages/core/src/app/appInit.ts
@@ -22,7 +22,9 @@ export const appInit = async (app: App): Promise<void> => {
   app.markdown = await resolveAppMarkdown(app)
 
   // create pages
-  app.pages = await resolveAppPages(app)
+  const { pages, pagesMap } = await resolveAppPages(app)
+  app.pages = pages
+  app.pagesMap = pagesMap
 
   // plugin hook: onInitialized
   await app.pluginApi.hooks.onInitialized.process(app)
diff --git a/packages/core/src/app/prepare/preparePageComponent.ts b/packages/core/src/app/prepare/preparePageComponent.ts
index 569d63697a..e88d6123d5 100644
--- a/packages/core/src/app/prepare/preparePageComponent.ts
+++ b/packages/core/src/app/prepare/preparePageComponent.ts
@@ -2,14 +2,16 @@ import { renderPageSfcBlocksToVue } from '../../page/index.js'
 import type { App, Page } from '../../types/index.js'
 
 /**
- * Generate page component temp file of a single page
+ * Generate page component temp file if the page does not have a source file
  */
 export const preparePageComponent = async (
   app: App,
   page: Page,
 ): Promise<void> => {
-  await app.writeTemp(
-    page.componentFilePathRelative,
-    renderPageSfcBlocksToVue(page.sfcBlocks),
-  )
+  if (page.filePath === null) {
+    await app.writeTemp(
+      page.componentFilePathRelative,
+      renderPageSfcBlocksToVue(page.sfcBlocks),
+    )
+  }
 }
diff --git a/packages/core/src/app/resolveAppPages.ts b/packages/core/src/app/resolveAppPages.ts
index d8cd00bd41..33c280867b 100644
--- a/packages/core/src/app/resolveAppPages.ts
+++ b/packages/core/src/app/resolveAppPages.ts
@@ -7,7 +7,9 @@ const log = debug('vuepress:core/app')
 /**
  * Resolve pages for vuepress app
  */
-export const resolveAppPages = async (app: App): Promise<Page[]> => {
+export const resolveAppPages = async (
+  app: App,
+): Promise<Pick<App, 'pages' | 'pagesMap'>> => {
   log('resolveAppPages start')
 
   // resolve page absolute file paths according to the page patterns
@@ -19,9 +21,14 @@ export const resolveAppPages = async (app: App): Promise<Page[]> => {
   let hasNotFoundPage = false as boolean
 
   // create pages from files
+  const pagesMap: Record<string, Page> = {}
   const pages = await Promise.all(
     pageFilePaths.map(async (filePath) => {
       const page = await createPage(app, { filePath })
+      // if there is a source file for the page, set it to the pages map
+      if (page.filePath) {
+        pagesMap[page.filePath] = page
+      }
       // if there is a 404 page, set the default layout to NotFound
       if (page.path === '/404.html') {
         page.frontmatter.layout ??= 'NotFound'
@@ -44,5 +51,5 @@ export const resolveAppPages = async (app: App): Promise<Page[]> => {
 
   log('resolveAppPages finish')
 
-  return pages
+  return { pages, pagesMap }
 }
diff --git a/packages/core/src/page/createPage.ts b/packages/core/src/page/createPage.ts
index c000873c84..0c48399162 100644
--- a/packages/core/src/page/createPage.ts
+++ b/packages/core/src/page/createPage.ts
@@ -85,6 +85,8 @@ export const createPage = async (
   const { componentFilePath, componentFilePathRelative } =
     resolvePageComponentInfo({
       app,
+      filePath,
+      filePathRelative,
       htmlFilePathRelative,
     })
 
diff --git a/packages/core/src/page/resolvePageComponentInfo.ts b/packages/core/src/page/resolvePageComponentInfo.ts
index b1ee312cc4..2105bdb187 100644
--- a/packages/core/src/page/resolvePageComponentInfo.ts
+++ b/packages/core/src/page/resolvePageComponentInfo.ts
@@ -6,15 +6,26 @@ import type { App } from '../types/index.js'
  */
 export const resolvePageComponentInfo = ({
   app,
+  filePath,
+  filePathRelative,
   htmlFilePathRelative,
 }: {
   app: App
+  filePath: string | null
+  filePathRelative: string | null
   htmlFilePathRelative: string
 }): {
   componentFilePath: string
   componentFilePathRelative: string
 } => {
-  // resolve component file path
+  // if there is a source file for the page, use it as the component file
+  if (filePath && filePathRelative) {
+    return {
+      componentFilePath: filePath,
+      componentFilePathRelative: filePathRelative,
+    }
+  }
+  // otherwise, generate a component file for the page
   const componentFilePathRelative = path.join(
     'pages',
     `${htmlFilePathRelative}.vue`,
diff --git a/packages/core/src/types/app/app.ts b/packages/core/src/types/app/app.ts
index e943950ec9..3bab071811 100644
--- a/packages/core/src/types/app/app.ts
+++ b/packages/core/src/types/app/app.ts
@@ -79,6 +79,13 @@ export interface App {
    * Only available after initialization
    */
   pages: Page[]
+
+  /**
+   * Page source filepath map.
+   *
+   * Only available after initialization
+   */
+  pagesMap: Record<string, Page | undefined>
 }
 
 /**
diff --git a/packages/core/tests/app/resolveAppPages.spec.ts b/packages/core/tests/app/resolveAppPages.spec.ts
index f394888238..a0366c1ab3 100644
--- a/packages/core/tests/app/resolveAppPages.spec.ts
+++ b/packages/core/tests/app/resolveAppPages.spec.ts
@@ -13,7 +13,7 @@ describe('core > app > resolveAppPages', () => {
     })
     app.markdown = createMarkdown()
 
-    const pages = await resolveAppPages(app)
+    const { pages } = await resolveAppPages(app)
     const fooPage = pages.find((page) => page.path === '/foo.html')
     const barPage = pages.find((page) => page.path === '/bar.html')
     const notFoundPage = pages.find((page) => page.path === '/404.html')
@@ -33,7 +33,7 @@ describe('core > app > resolveAppPages', () => {
     })
     app.markdown = createMarkdown()
 
-    const pages = await resolveAppPages(app)
+    const { pages } = await resolveAppPages(app)
     const fooPage = pages.find((page) => page.path === '/foo.html')
     const barPage = pages.find((page) => page.path === '/bar.html')
     const notFoundPage = pages.find((page) => page.path === '/404.html')
@@ -61,7 +61,7 @@ describe('core > app > resolveAppPages', () => {
     app.pluginApi.registerHooks()
     app.markdown = createMarkdown()
 
-    const pages = await resolveAppPages(app)
+    const { pages } = await resolveAppPages(app)
 
     pages.forEach((page) => {
       expect(page.frontmatter.foo).toBe('bar')
@@ -84,7 +84,7 @@ describe('core > app > resolveAppPages', () => {
     app.pluginApi.registerHooks()
     app.markdown = createMarkdown()
 
-    const pages = await resolveAppPages(app)
+    const { pages } = await resolveAppPages(app)
 
     pages.forEach((page) => {
       expect(page.frontmatter.foo).toBe('baz')
diff --git a/packages/core/tests/page/resolvePageComponentInfo.spec.ts b/packages/core/tests/page/resolvePageComponentInfo.spec.ts
index 7feabdd495..c8a009e1ea 100644
--- a/packages/core/tests/page/resolvePageComponentInfo.spec.ts
+++ b/packages/core/tests/page/resolvePageComponentInfo.spec.ts
@@ -9,9 +9,11 @@ const app = createBaseApp({
   bundler: {} as Bundler,
 })
 
-it('should resolve page component info correctly', () => {
+it('should resolve page component info correctly without source file path', () => {
   const resolved = resolvePageComponentInfo({
     app,
+    filePath: null,
+    filePathRelative: null,
     htmlFilePathRelative: 'foo.html',
   })
 
@@ -20,3 +22,17 @@ it('should resolve page component info correctly', () => {
     componentFilePathRelative: 'pages/foo.html.vue',
   })
 })
+
+it('should resolve page component info correctly with source file path', () => {
+  const resolved = resolvePageComponentInfo({
+    app,
+    filePath: app.dir.source('foo.md'),
+    filePathRelative: 'foo.md',
+    htmlFilePathRelative: 'foo.html',
+  })
+
+  expect(resolved).toEqual({
+    componentFilePath: app.dir.source('foo.md'),
+    componentFilePathRelative: 'foo.md',
+  })
+})
diff --git a/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts b/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts
index 1434888055..2524963372 100644
--- a/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts
+++ b/packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts
@@ -8,11 +8,6 @@ export interface AssetsPluginOptions {
    * Whether to prepend base to absolute path
    */
   absolutePathPrependBase?: boolean
-
-  /**
-   * Prefix to add to relative assets links
-   */
-  relativePathPrefix?: string
 }
 
 /**
@@ -20,10 +15,7 @@ export interface AssetsPluginOptions {
  */
 export const assetsPlugin: PluginWithOptions<AssetsPluginOptions> = (
   md,
-  {
-    absolutePathPrependBase = false,
-    relativePathPrefix = '@source',
-  }: AssetsPluginOptions = {},
+  { absolutePathPrependBase = false }: AssetsPluginOptions = {},
 ) => {
   // wrap raw image renderer rule
   const rawImageRule = md.renderer.rules.image!
@@ -35,10 +27,7 @@ export const assetsPlugin: PluginWithOptions<AssetsPluginOptions> = (
 
     if (link) {
       // replace the original link with resolved link
-      token.attrSet(
-        'src',
-        resolveLink(link, { env, absolutePathPrependBase, relativePathPrefix }),
-      )
+      token.attrSet('src', resolveLink(link, { env, absolutePathPrependBase }))
     }
 
     return rawImageRule(tokens, idx, options, env, self)
@@ -57,7 +46,6 @@ export const assetsPlugin: PluginWithOptions<AssetsPluginOptions> = (
             `${prefix}${quote}${resolveLink(src.trim(), {
               env,
               absolutePathPrependBase,
-              relativePathPrefix,
               strict: true,
             })}${quote}`,
         )
@@ -74,7 +62,6 @@ export const assetsPlugin: PluginWithOptions<AssetsPluginOptions> = (
                     `${resolveLink(url.trim(), {
                       env,
                       absolutePathPrependBase,
-                      relativePathPrefix,
                       strict: true,
                     })}${descriptor.replace(/[ \n]+/g, ' ').trimEnd()}`,
                 ),
diff --git a/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts b/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts
index b9277ed855..a1081fe9ed 100644
--- a/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts
+++ b/packages/markdown/src/plugins/assetsPlugin/resolveLink.ts
@@ -5,18 +5,12 @@ import type { MarkdownEnv } from '../../types.js'
 interface ResolveLinkOptions {
   env: MarkdownEnv
   absolutePathPrependBase?: boolean
-  relativePathPrefix: string
   strict?: boolean
 }
 
 export const resolveLink = (
   link: string,
-  {
-    env,
-    absolutePathPrependBase = false,
-    relativePathPrefix,
-    strict = false,
-  }: ResolveLinkOptions,
+  { env, absolutePathPrependBase = false }: ResolveLinkOptions,
 ): string => {
   // do not resolve data uri
   if (link.startsWith('data:')) return link
@@ -24,22 +18,6 @@ export const resolveLink = (
   // decode link to ensure bundler can find the file correctly
   let resolvedLink = decode(link)
 
-  // check if the link is relative path
-  const isRelativePath = strict
-    ? // in strict mode, only link that starts with `./` or `../` is considered as relative path
-      /^\.{1,2}\//.test(link)
-    : // in non-strict mode, link that does not start with `/` and does not have protocol is considered as relative path
-      !link.startsWith('/') && !/[A-z]+:\/\//.test(link)
-
-  // if the link is relative path, and the `env.filePathRelative` exists
-  // add `@source` alias to the link
-  if (isRelativePath && env.filePathRelative) {
-    resolvedLink = `${relativePathPrefix}/${path.join(
-      path.dirname(env.filePathRelative),
-      resolvedLink,
-    )}`
-  }
-
   // prepend base to absolute path if needed
   if (absolutePathPrependBase && env.base && link.startsWith('/')) {
     resolvedLink = path.join(env.base, resolvedLink)
diff --git a/packages/markdown/tests/plugins/assetsPlugin.spec.ts b/packages/markdown/tests/plugins/assetsPlugin.spec.ts
index af149f84d9..34dca2e41d 100644
--- a/packages/markdown/tests/plugins/assetsPlugin.spec.ts
+++ b/packages/markdown/tests/plugins/assetsPlugin.spec.ts
@@ -49,37 +49,36 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
         md: MarkdownIt().use(assetsPlugin),
         env: {
           base: '/base/',
-          filePathRelative: 'sub/foo.md',
         },
         expected: [
           // relative paths
-          '<img src="@source/sub/foo.png" alt="foo">',
-          '<img src="@source/sub/foo.png" alt="foo2">',
-          '<img src="@source/sub/foo/bar.png" alt="foo-bar">',
-          '<img src="@source/sub/foo/bar.png" alt="foo-bar2">',
-          '<img src="@source/baz.png" alt="baz">',
-          '<img src="@source/../out.png" alt="out">',
-          '<img src="@source/sub/汉字.png" alt="汉字">',
-          '<img src="@source/sub/100%.png" alt="100%">',
+          '<img src="./foo.png" alt="foo">',
+          '<img src="../sub/foo.png" alt="foo2">',
+          '<img src="./foo/bar.png" alt="foo-bar">',
+          '<img src="../sub/foo/bar.png" alt="foo-bar2">',
+          '<img src="../baz.png" alt="baz">',
+          '<img src="../../out.png" alt="out">',
+          '<img src="./汉字.png" alt="汉字">',
+          '<img src="./100%.png" alt="100%">',
           // absolute paths
           '<img src="/absolute.png" alt="absolute">',
           '<img src="/foo/absolute.png" alt="absolute-foo">',
           // no-prefix paths
-          '<img src="@source/sub/no-prefix.png" alt="no-prefix">',
-          '<img src="@source/sub/foo/no-prefix.png" alt="no-prefix-foo">',
-          '<img src="@source/sub/@alias/foo.png" alt="alias">',
-          '<img src="@source/sub/@alias/汉字.png" alt="汉字">',
-          '<img src="@source/sub/@alias/100%.png" alt="100%">',
-          '<img src="@source/sub/~@alias/foo.png" alt="~alias">',
-          '<img src="@source/sub/~@alias/汉字.png" alt="~汉字">',
-          '<img src="@source/sub/~@alias/100%.png" alt="~100%">',
+          '<img src="no-prefix.png" alt="no-prefix">',
+          '<img src="foo/no-prefix.png" alt="no-prefix-foo">',
+          '<img src="@alias/foo.png" alt="alias">',
+          '<img src="@alias/汉字.png" alt="汉字">',
+          '<img src="@alias/100%.png" alt="100%">',
+          '<img src="~@alias/foo.png" alt="~alias">',
+          '<img src="~@alias/汉字.png" alt="~汉字">',
+          '<img src="~@alias/100%.png" alt="~100%">',
           // keep as is
           '<img src="http://foobar.com/icon.png" alt="url">',
           '<img src="" alt="empty">',
           // invalid paths
-          '<img src="@source/sub/.../invalid.png" alt="invalid">',
-          '<img src="@source/sub/.../汉字.png" alt="汉字">',
-          '<img src="@source/sub/.../100%.png" alt="100%">',
+          '<img src=".../invalid.png" alt="invalid">',
+          '<img src=".../汉字.png" alt="汉字">',
+          '<img src=".../100%.png" alt="100%">',
           // data uri
           '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wr4H/wAAAABJRU5ErkJggg==" alt="data-uri">',
         ],
@@ -91,87 +90,7 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
         }),
         env: {
           base: '/base/',
-          filePathRelative: 'sub/foo.md',
         },
-        expected: [
-          // relative paths
-          '<img src="@source/sub/foo.png" alt="foo">',
-          '<img src="@source/sub/foo.png" alt="foo2">',
-          '<img src="@source/sub/foo/bar.png" alt="foo-bar">',
-          '<img src="@source/sub/foo/bar.png" alt="foo-bar2">',
-          '<img src="@source/baz.png" alt="baz">',
-          '<img src="@source/../out.png" alt="out">',
-          '<img src="@source/sub/汉字.png" alt="汉字">',
-          '<img src="@source/sub/100%.png" alt="100%">',
-          // absolute paths
-          '<img src="/base/absolute.png" alt="absolute">',
-          '<img src="/base/foo/absolute.png" alt="absolute-foo">',
-          // no-prefix paths
-          '<img src="@source/sub/no-prefix.png" alt="no-prefix">',
-          '<img src="@source/sub/foo/no-prefix.png" alt="no-prefix-foo">',
-          '<img src="@source/sub/@alias/foo.png" alt="alias">',
-          '<img src="@source/sub/@alias/汉字.png" alt="汉字">',
-          '<img src="@source/sub/@alias/100%.png" alt="100%">',
-          '<img src="@source/sub/~@alias/foo.png" alt="~alias">',
-          '<img src="@source/sub/~@alias/汉字.png" alt="~汉字">',
-          '<img src="@source/sub/~@alias/100%.png" alt="~100%">',
-          // keep as is
-          '<img src="http://foobar.com/icon.png" alt="url">',
-          '<img src="" alt="empty">',
-          // invalid paths
-          '<img src="@source/sub/.../invalid.png" alt="invalid">',
-          '<img src="@source/sub/.../汉字.png" alt="汉字">',
-          '<img src="@source/sub/.../100%.png" alt="100%">',
-          // data uri
-          '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wr4H/wAAAABJRU5ErkJggg==" alt="data-uri">',
-        ],
-      },
-      {
-        description: 'should respect `relativePathPrefix` option',
-        md: MarkdownIt().use(assetsPlugin, {
-          relativePathPrefix: '@foo',
-        }),
-        env: {
-          filePathRelative: 'sub/foo.md',
-        },
-        expected: [
-          // relative paths
-          '<img src="@foo/sub/foo.png" alt="foo">',
-          '<img src="@foo/sub/foo.png" alt="foo2">',
-          '<img src="@foo/sub/foo/bar.png" alt="foo-bar">',
-          '<img src="@foo/sub/foo/bar.png" alt="foo-bar2">',
-          '<img src="@foo/baz.png" alt="baz">',
-          '<img src="@foo/../out.png" alt="out">',
-          '<img src="@foo/sub/汉字.png" alt="汉字">',
-          '<img src="@foo/sub/100%.png" alt="100%">',
-          // absolute paths
-          '<img src="/absolute.png" alt="absolute">',
-          '<img src="/foo/absolute.png" alt="absolute-foo">',
-          // no-prefix paths
-          '<img src="@foo/sub/no-prefix.png" alt="no-prefix">',
-          '<img src="@foo/sub/foo/no-prefix.png" alt="no-prefix-foo">',
-          '<img src="@foo/sub/@alias/foo.png" alt="alias">',
-          '<img src="@foo/sub/@alias/汉字.png" alt="汉字">',
-          '<img src="@foo/sub/@alias/100%.png" alt="100%">',
-          '<img src="@foo/sub/~@alias/foo.png" alt="~alias">',
-          '<img src="@foo/sub/~@alias/汉字.png" alt="~汉字">',
-          '<img src="@foo/sub/~@alias/100%.png" alt="~100%">',
-          // keep as is
-          '<img src="http://foobar.com/icon.png" alt="url">',
-          '<img src="" alt="empty">',
-          // invalid paths
-          '<img src="@foo/sub/.../invalid.png" alt="invalid">',
-          '<img src="@foo/sub/.../汉字.png" alt="汉字">',
-          '<img src="@foo/sub/.../100%.png" alt="100%">',
-          // data uri
-          '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wr4H/wAAAABJRU5ErkJggg==" alt="data-uri">',
-        ],
-      },
-      {
-        description:
-          'should not handle relative paths if `env.filePathRelative` is not provided',
-        md: MarkdownIt().use(assetsPlugin),
-        env: {},
         expected: [
           // relative paths
           '<img src="./foo.png" alt="foo">',
@@ -183,8 +102,8 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
           '<img src="./汉字.png" alt="汉字">',
           '<img src="./100%.png" alt="100%">',
           // absolute paths
-          '<img src="/absolute.png" alt="absolute">',
-          '<img src="/foo/absolute.png" alt="absolute-foo">',
+          '<img src="/base/absolute.png" alt="absolute">',
+          '<img src="/base/foo/absolute.png" alt="absolute-foo">',
           // no-prefix paths
           '<img src="no-prefix.png" alt="no-prefix">',
           '<img src="foo/no-prefix.png" alt="no-prefix-foo">',
@@ -302,20 +221,20 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
           description: 'should handle assets link with default options',
           md: MarkdownIt({ html: true }).use(assetsPlugin),
           env: {
-            filePathRelative: 'sub/foo.md',
+            base: '/base/',
           },
           expected: [
             /* src */
             // relative paths
-            '<img src="@source/sub/foo.png">',
-            '<img src="@source/sub/foo.png">',
-            '<img src="@source/sub/foo/bar.png">',
-            '<img src="@source/sub/foo/bar.png">',
-            '<img src="@source/baz.png">',
-            '<img src="@source/../out.png">',
-            '<img src="@source/sub/汉字.png">',
-            '<img src="@source/sub/100%.png">',
-            '<img alt="attrs" src="@source/sub/attrs.png" width="100px">',
+            '<img src="./foo.png">',
+            '<img src="../sub/foo.png">',
+            '<img src="./foo/bar.png">',
+            '<img src="../sub/foo/bar.png">',
+            '<img src="../baz.png">',
+            '<img src="../../out.png">',
+            '<img src="./汉字.png">',
+            '<img src="./100%.png">',
+            '<img alt="attrs" src="./attrs.png" width="100px">',
             // aliases
             '<img src="@alias/foo.png">',
             '<img src="@alias/汉字.png">',
@@ -344,34 +263,35 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
 
             /* srcset */
             // relative paths
-            '<img srcset="@source/sub/foo.png 1x, @source/sub/foo.png 2x, @source/sub/foo/bar.png 1024w, @source/sub/foo/bar.png 2048w, @source/baz.png 4096w, @source/../out.png">',
-            '<img srcset="@source/sub/汉字.png 1x, @source/sub/100%.png">',
-            '<img alt="attrs" srcset="@source/sub/attrs.png" width="100px">',
+            '<img srcset="./foo.png 1x, ../sub/foo.png 2x, ./foo/bar.png 1024w, ../sub/foo/bar.png 2048w, ../baz.png 4096w, ../../out.png">',
+            '<img srcset="./汉字.png 1x, ./100%.png">',
+            '<img alt="attrs" srcset="./attrs.png" width="100px">',
             // aliases
             '<img srcset="@alias/foo.png 1x, @alias/汉字.png 2x, @alias/100%.png 3x">',
             '<img alt="attrs" srcset="@alias/attrs.png 1024w" width="100px">',
             // webpack legacy aliases
             '<img srcset="~@alias/foo.png 1x, ~@alias/汉字.png 2x, ~@alias/100%.png 3x">',
             '<img alt="attrs" srcset="~@alias/attrs.png 1024w" width="100px">',
-            // keep as is
+            // absolute paths and no-prefix paths
             '<img srcset="/absolute.png 1x, no-prefix.png 2x, http://foobar.com/icon.png">',
+            // keep as is
             '<img srcset="">',
             '<img alt="attrs" srcset="attrs.png 1x, default.png" width="100px">',
             // invalid paths
             '<img srcset=".../invalid.png 1x, .../汉字.png 2x, .../100%.png 3x">',
             '<img alt="attrs" srcset=".../attrs.png 1x, .../default.png" width="100px">',
             // invalid srcset
-            '<img srcset="@source/invalid.png, @source/汉字.png, .../100%.png 3x">',
+            '<img srcset="../invalid.png, ../汉字.png, .../100%.png 3x">',
 
             /* both */
             // relative paths
-            '<img srcset="@source/sub/foo.png 1x, @source/sub/foo.png 2x, @source/sub/foo/bar.png 1024w, @source/sub/foo/bar.png 2048w, @source/baz.png 4096w, @source/../out.png 3x" src="@source/sub/default.png">',
-            '<img src="@source/sub/100%.png" srcset="@source/sub/汉字.png 1x">',
-            '<img src="@source/sub/default.png" srcset="@source/sub/attrs1.png 1x, @source/sub/attrs2.png 2x" alt="attrs" width="100px">',
+            '<img srcset="./foo.png 1x, ../sub/foo.png 2x, ./foo/bar.png 1024w, ../sub/foo/bar.png 2048w, ../baz.png 4096w, ../../out.png 3x" src="./default.png">',
+            '<img src="./100%.png" srcset="./汉字.png 1x">',
+            '<img src="./default.png" srcset="./attrs1.png 1x, ./attrs2.png 2x" alt="attrs" width="100px">',
             // aliases
             '<img srcset="@alias/foo.png 1x, @alias/汉字.png 2x, @alias/100%.png 3x" alt="attrs" src="@alias/attrs.png" width="100px">',
             '<img srcset="~@alias/foo.png 1x, ~@alias/汉字.png 2x, ~@alias/100%.png 3x" alt="attrs" src="~@alias/attrs.png" width="100px">',
-            // keep as is
+            // absolute paths and no-prefix paths
             '<img alt="attrs" src="" width="100px" srcset="/absolute.png 1x, no-prefix.png 2x, http://foobar.com/icon.png">',
 
             /* data uri */
@@ -379,92 +299,13 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
           ],
         },
         {
-          description: 'should respect `relativePathPrefix` option',
+          description: 'should respect `absolutePathPrependBase` option',
           md: MarkdownIt({ html: true }).use(assetsPlugin, {
-            relativePathPrefix: '@foo',
+            absolutePathPrependBase: true,
           }),
           env: {
-            filePathRelative: 'sub/foo.md',
+            base: '/base/',
           },
-          expected: [
-            /* src */
-            // relative paths
-            '<img src="@foo/sub/foo.png">',
-            '<img src="@foo/sub/foo.png">',
-            '<img src="@foo/sub/foo/bar.png">',
-            '<img src="@foo/sub/foo/bar.png">',
-            '<img src="@foo/baz.png">',
-            '<img src="@foo/../out.png">',
-            '<img src="@foo/sub/汉字.png">',
-            '<img src="@foo/sub/100%.png">',
-            '<img alt="attrs" src="@foo/sub/attrs.png" width="100px">',
-            // aliases
-            '<img src="@alias/foo.png">',
-            '<img src="@alias/汉字.png">',
-            '<img src="@alias/100%.png">',
-            '<img alt="attrs" src="@alias/attrs.png" width="100px">',
-            // webpack legacy aliases
-            '<img src="~@alias/foo.png">',
-            '<img src="~@alias/汉字.png">',
-            '<img src="~@alias/100%.png">',
-            '<img alt="attrs" src="~@alias/attrs.png" width="100px">',
-            // absolute paths
-            '<img src="/absolute.png">',
-            '<img src="/foo/absolute.png">',
-            // no-prefix paths
-            '<img src="no-prefix.png">',
-            '<img src="foo/no-prefix.png">',
-            '<img alt="attrs" src="attrs.png" width="100px">',
-            // keep as is
-            '<img src="http://foobar.com/icon.png">',
-            '<img src="">',
-            // invalid paths
-            '<img src=".../invalid.png">',
-            '<img src=".../汉字.png">',
-            '<img src=".../100%.png">',
-            '<img alt="attrs" src=".../attrs.png" width="100px">',
-
-            /* srcset */
-            // relative paths
-            '<img srcset="@foo/sub/foo.png 1x, @foo/sub/foo.png 2x, @foo/sub/foo/bar.png 1024w, @foo/sub/foo/bar.png 2048w, @foo/baz.png 4096w, @foo/../out.png">',
-            '<img srcset="@foo/sub/汉字.png 1x, @foo/sub/100%.png">',
-            '<img alt="attrs" srcset="@foo/sub/attrs.png" width="100px">',
-            // aliases
-            '<img srcset="@alias/foo.png 1x, @alias/汉字.png 2x, @alias/100%.png 3x">',
-            '<img alt="attrs" srcset="@alias/attrs.png 1024w" width="100px">',
-            // webpack legacy aliases
-            '<img srcset="~@alias/foo.png 1x, ~@alias/汉字.png 2x, ~@alias/100%.png 3x">',
-            '<img alt="attrs" srcset="~@alias/attrs.png 1024w" width="100px">',
-            // keep as is
-            '<img srcset="/absolute.png 1x, no-prefix.png 2x, http://foobar.com/icon.png">',
-            '<img srcset="">',
-            '<img alt="attrs" srcset="attrs.png 1x, default.png" width="100px">',
-            // invalid paths
-            '<img srcset=".../invalid.png 1x, .../汉字.png 2x, .../100%.png 3x">',
-            '<img alt="attrs" srcset=".../attrs.png 1x, .../default.png" width="100px">',
-            // invalid srcset
-            '<img srcset="@foo/invalid.png, @foo/汉字.png, .../100%.png 3x">',
-
-            /* both */
-            // relative paths
-            '<img srcset="@foo/sub/foo.png 1x, @foo/sub/foo.png 2x, @foo/sub/foo/bar.png 1024w, @foo/sub/foo/bar.png 2048w, @foo/baz.png 4096w, @foo/../out.png 3x" src="@foo/sub/default.png">',
-            '<img src="@foo/sub/100%.png" srcset="@foo/sub/汉字.png 1x">',
-            '<img src="@foo/sub/default.png" srcset="@foo/sub/attrs1.png 1x, @foo/sub/attrs2.png 2x" alt="attrs" width="100px">',
-            // aliases
-            '<img srcset="@alias/foo.png 1x, @alias/汉字.png 2x, @alias/100%.png 3x" alt="attrs" src="@alias/attrs.png" width="100px">',
-            '<img srcset="~@alias/foo.png 1x, ~@alias/汉字.png 2x, ~@alias/100%.png 3x" alt="attrs" src="~@alias/attrs.png" width="100px">',
-            // keep as is
-            '<img alt="attrs" src="" width="100px" srcset="/absolute.png 1x, no-prefix.png 2x, http://foobar.com/icon.png">',
-
-            /* data uri */
-            '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wr4H/wAAAABJRU5ErkJggg==">',
-          ],
-        },
-        {
-          description:
-            'should not handle relative paths if `env.filePathRelative` is not provided',
-          md: MarkdownIt({ html: true }).use(assetsPlugin),
-          env: {},
           expected: [
             /* src */
             // relative paths
@@ -488,8 +329,8 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
             '<img src="~@alias/100%.png">',
             '<img alt="attrs" src="~@alias/attrs.png" width="100px">',
             // absolute paths
-            '<img src="/absolute.png">',
-            '<img src="/foo/absolute.png">',
+            '<img src="/base/absolute.png">',
+            '<img src="/base/foo/absolute.png">',
             // no-prefix paths
             '<img src="no-prefix.png">',
             '<img src="foo/no-prefix.png">',
@@ -514,8 +355,9 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
             // webpack legacy aliases
             '<img srcset="~@alias/foo.png 1x, ~@alias/汉字.png 2x, ~@alias/100%.png 3x">',
             '<img alt="attrs" srcset="~@alias/attrs.png 1024w" width="100px">',
+            // absolute paths and no-prefix paths
+            '<img srcset="/base/absolute.png 1x, no-prefix.png 2x, http://foobar.com/icon.png">',
             // keep as is
-            '<img srcset="/absolute.png 1x, no-prefix.png 2x, http://foobar.com/icon.png">',
             '<img srcset="">',
             '<img alt="attrs" srcset="attrs.png 1x, default.png" width="100px">',
             // invalid paths
@@ -532,8 +374,8 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
             // aliases
             '<img srcset="@alias/foo.png 1x, @alias/汉字.png 2x, @alias/100%.png 3x" alt="attrs" src="@alias/attrs.png" width="100px">',
             '<img srcset="~@alias/foo.png 1x, ~@alias/汉字.png 2x, ~@alias/100%.png 3x" alt="attrs" src="~@alias/attrs.png" width="100px">',
-            // keep as is
-            '<img alt="attrs" src="" width="100px" srcset="/absolute.png 1x, no-prefix.png 2x, http://foobar.com/icon.png">',
+            // absolute paths and no-prefix paths
+            '<img alt="attrs" src="" width="100px" srcset="/base/absolute.png 1x, no-prefix.png 2x, http://foobar.com/icon.png">',
 
             /* data uri */
             '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wr4H/wAAAABJRU5ErkJggg==">',
@@ -630,22 +472,25 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
         `<img alt="attrs" src="
           .../attrs.png
           " width="100px">`,
+        `<img alt="attrs" src="
+          /absolute/attrs.png
+          " width="100px">`,
 
         /* srcset */
         `<img srcset="./foo.png      1x  ,
               ../sub/foo.png  2x,./foo/bar.png
-    1024w ,../../out.png">`,
+    1024w ,../../out.png, /absolute/attrs.png">`,
         `<img alt="attrs"  srcset=" ./attrs.png 1x
-    ,default.png " width="100px">`,
+    ,default.png ,/absolute/attrs.png" width="100px">`,
 
         /** both */
         `<img src="
           ./default.png
     " srcset="./foo.png      1x  ,
               ../sub/foo.png  2x,./foo/bar.png
-    1024w ,../../out.png">`,
+    1024w ,../../out.png, /absolute/attrs.png">`,
         `<img alt="attrs" src="./default.png" srcset=" ./attrs.png 1x
-    ,default.png " width="100px">`,
+    ,default.png ,/absolute/attrs.png" width="100px">`,
       ]
 
       const TEST_CASES: {
@@ -658,58 +503,42 @@ describe('@vuepress/markdown > plugins > assetsPlugin', () => {
           description: 'should handle assets link with default options',
           md: MarkdownIt({ html: true }).use(assetsPlugin),
           env: {
-            filePathRelative: 'sub/foo.md',
+            base: '/base/',
           },
           expected: [
             /* src */
             '<p><img alt="attrs" src=".../attrs.png" width="100px"></p>',
+            '<p><img alt="attrs" src="/absolute/attrs.png" width="100px"></p>',
 
             /* srcset */
-            '<p><img srcset="@source/sub/foo.png 1x, @source/sub/foo.png 2x, @source/sub/foo/bar.png 1024w, @source/../out.png"></p>',
-            '<p><img alt="attrs"  srcset="@source/sub/attrs.png 1x, default.png" width="100px"></p>',
+            '<p><img srcset="./foo.png 1x, ../sub/foo.png 2x, ./foo/bar.png 1024w, ../../out.png, /absolute/attrs.png"></p>',
+            '<p><img alt="attrs"  srcset="./attrs.png 1x, default.png, /absolute/attrs.png" width="100px"></p>',
 
             /* both */
-            '<p><img src="@source/sub/default.png" srcset="@source/sub/foo.png 1x, @source/sub/foo.png 2x, @source/sub/foo/bar.png 1024w, @source/../out.png"></p>',
-            '<p><img alt="attrs" src="@source/sub/default.png" srcset="@source/sub/attrs.png 1x, default.png" width="100px"></p>',
+            '<p><img src="./default.png" srcset="./foo.png 1x, ../sub/foo.png 2x, ./foo/bar.png 1024w, ../../out.png, /absolute/attrs.png"></p>',
+            '<p><img alt="attrs" src="./default.png" srcset="./attrs.png 1x, default.png, /absolute/attrs.png" width="100px"></p>',
           ],
         },
         {
-          description: 'should respect `relativePathPrefix` option',
+          description: 'should respect `absolutePathPrependBase` option',
           md: MarkdownIt({ html: true }).use(assetsPlugin, {
-            relativePathPrefix: '@foo',
+            absolutePathPrependBase: true,
           }),
           env: {
-            filePathRelative: 'sub/foo.md',
+            base: '/base/',
           },
           expected: [
             /* src */
             '<p><img alt="attrs" src=".../attrs.png" width="100px"></p>',
+            '<p><img alt="attrs" src="/base/absolute/attrs.png" width="100px"></p>',
 
             /* srcset */
-            '<p><img srcset="@foo/sub/foo.png 1x, @foo/sub/foo.png 2x, @foo/sub/foo/bar.png 1024w, @foo/../out.png"></p>',
-            '<p><img alt="attrs"  srcset="@foo/sub/attrs.png 1x, default.png" width="100px"></p>',
-
-            /* both */
-            '<p><img src="@foo/sub/default.png" srcset="@foo/sub/foo.png 1x, @foo/sub/foo.png 2x, @foo/sub/foo/bar.png 1024w, @foo/../out.png"></p>',
-            '<p><img alt="attrs" src="@foo/sub/default.png" srcset="@foo/sub/attrs.png 1x, default.png" width="100px"></p>',
-          ],
-        },
-        {
-          description:
-            'should not handle assets link if `filePathRelative` is not provided',
-          md: MarkdownIt({ html: true }).use(assetsPlugin),
-          env: {},
-          expected: [
-            /* src */
-            '<p><img alt="attrs" src=".../attrs.png" width="100px"></p>',
-
-            /* srcset */
-            '<p><img srcset="./foo.png 1x, ../sub/foo.png 2x, ./foo/bar.png 1024w, ../../out.png"></p>',
-            '<p><img alt="attrs"  srcset="./attrs.png 1x, default.png" width="100px"></p>',
+            '<p><img srcset="./foo.png 1x, ../sub/foo.png 2x, ./foo/bar.png 1024w, ../../out.png, /base/absolute/attrs.png"></p>',
+            '<p><img alt="attrs"  srcset="./attrs.png 1x, default.png, /base/absolute/attrs.png" width="100px"></p>',
 
             /* both */
-            '<p><img src="./default.png" srcset="./foo.png 1x, ../sub/foo.png 2x, ./foo/bar.png 1024w, ../../out.png"></p>',
-            '<p><img alt="attrs" src="./default.png" srcset="./attrs.png 1x, default.png" width="100px"></p>',
+            '<p><img src="./default.png" srcset="./foo.png 1x, ../sub/foo.png 2x, ./foo/bar.png 1024w, ../../out.png, /base/absolute/attrs.png"></p>',
+            '<p><img alt="attrs" src="./default.png" srcset="./attrs.png 1x, default.png, /base/absolute/attrs.png" width="100px"></p>',
           ],
         },
       ]

From e681fb3f5a73e93c72fb62961eabe3044e3f0f58 Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 12:33:15 +0800
Subject: [PATCH 02/11] refactor: updates

---
 eslint.config.ts                              |  1 +
 .../src/plugins/vuepressMarkdownPlugin.ts     | 25 +++---
 .../src/loaders/vuepressMarkdownLoader.ts     | 14 ++--
 .../cli/src/commands/dev/handlePageAdd.ts     | 11 +--
 .../cli/src/commands/dev/handlePageChange.ts  |  9 +--
 .../cli/src/commands/dev/handlePageUnlink.ts  |  1 +
 packages/client/src/components/Content.ts     |  2 +-
 packages/client/src/setupGlobalComputed.ts    | 11 ++-
 packages/client/src/types/routes.ts           |  4 +-
 packages/core/src/app/appPrepare.ts           |  7 +-
 packages/core/src/app/prepare/index.ts        |  1 -
 .../core/src/app/prepare/preparePageChunk.ts  | 32 +-------
 .../src/app/prepare/preparePageComponent.ts   | 17 ----
 packages/core/src/page/createPage.ts          | 15 +---
 packages/core/src/page/index.ts               |  5 +-
 .../core/src/page/renderPageSfcBlocksToVue.ts | 17 ----
 packages/core/src/page/renderPageToVue.ts     | 79 +++++++++++++++++++
 .../core/src/page/resolvePageChunkInfo.ts     | 24 +++++-
 .../core/src/page/resolvePageComponentInfo.ts | 39 ---------
 ...geFileContent.ts => resolvePageContent.ts} | 18 +++--
 packages/core/src/types/page.ts               | 17 +---
 ...ent.spec.ts => resolvePageContent.spec.ts} | 30 +++++--
 22 files changed, 181 insertions(+), 198 deletions(-)
 delete mode 100644 packages/core/src/app/prepare/preparePageComponent.ts
 delete mode 100644 packages/core/src/page/renderPageSfcBlocksToVue.ts
 create mode 100644 packages/core/src/page/renderPageToVue.ts
 delete mode 100644 packages/core/src/page/resolvePageComponentInfo.ts
 rename packages/core/src/page/{resolvePageFileContent.ts => resolvePageContent.ts} (51%)
 rename packages/core/tests/page/{resolvePageFileContent.spec.ts => resolvePageContent.spec.ts} (51%)

diff --git a/eslint.config.ts b/eslint.config.ts
index fa0eed31d7..1cb1edfb7b 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -19,6 +19,7 @@ export default vuepress(
             allow: [
               '__dirname',
               '_context',
+              '_pageData',
               '_pageChunk',
               '_registeredComponents',
             ],
diff --git a/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts b/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
index eb072f840c..94da9828c8 100644
--- a/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
+++ b/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
@@ -1,6 +1,5 @@
 import type { App } from '@vuepress/core'
-import { parsePageContent, renderPageSfcBlocksToVue } from '@vuepress/core'
-import { path } from '@vuepress/utils'
+import { createPage, renderPageToVue } from '@vuepress/core'
 import type { Plugin } from 'vite'
 
 /**
@@ -11,7 +10,7 @@ export const vuepressMarkdownPlugin = ({ app }: { app: App }): Plugin => ({
 
   enforce: 'pre',
 
-  transform(code, id) {
+  async transform(code, id) {
     if (!id.endsWith('.md')) return
 
     // get the matched page by file path (id)
@@ -19,18 +18,15 @@ export const vuepressMarkdownPlugin = ({ app }: { app: App }): Plugin => ({
 
     // if the page content is not changed, render it to vue component directly
     if (page?.content === code) {
-      return renderPageSfcBlocksToVue(page.sfcBlocks)
+      return renderPageToVue(page)
     }
 
-    // parse the markdown content to sfc blocks and render it to vue component
-    const { sfcBlocks } = parsePageContent({
-      app,
+    // create a new page with the new content
+    const newPage = await createPage(app, {
       content: code,
       filePath: id,
-      filePathRelative: path.relative(app.dir.source(), id),
-      options: {},
     })
-    return renderPageSfcBlocksToVue(sfcBlocks)
+    return renderPageToVue(newPage)
   },
 
   async handleHotUpdate(ctx) {
@@ -39,15 +35,12 @@ export const vuepressMarkdownPlugin = ({ app }: { app: App }): Plugin => ({
     // read the source code
     const code = await ctx.read()
 
-    // parse the content to sfc blocks
-    const { sfcBlocks } = parsePageContent({
-      app,
+    // create a new page with the new content
+    const newPage = await createPage(app, {
       content: code,
       filePath: ctx.file,
-      filePathRelative: path.relative(app.dir.source(), ctx.file),
-      options: {},
     })
 
-    ctx.read = () => renderPageSfcBlocksToVue(sfcBlocks)
+    ctx.read = () => renderPageToVue(newPage)
   },
 })
diff --git a/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts
index d9ca435d14..819dc29f66 100644
--- a/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts
+++ b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts
@@ -11,8 +11,7 @@ export interface VuepressMarkdownLoaderOptions {
 export const vuepressMarkdownLoader: LoaderDefinitionFunction<VuepressMarkdownLoaderOptions> =
   async function vuepressMarkdownLoader(source) {
     // import esm dependencies
-    const [{ parsePageContent, renderPageSfcBlocksToVue }, { path }] =
-      await Promise.all([import('@vuepress/core'), import('@vuepress/utils')])
+    const { createPage, renderPageToVue } = await import('@vuepress/core')
 
     // get app instance from loader options
     const { app } = this.getOptions()
@@ -22,16 +21,13 @@ export const vuepressMarkdownLoader: LoaderDefinitionFunction<VuepressMarkdownLo
 
     // if the page content is not changed, render it to vue component directly
     if (page?.content === source) {
-      return renderPageSfcBlocksToVue(page.sfcBlocks)
+      return renderPageToVue(page)
     }
 
-    // parse the markdown content to sfc blocks and render it to vue component
-    const { sfcBlocks } = parsePageContent({
-      app,
+    // create a new page with the new content
+    const newPage = await createPage(app, {
       content: source,
       filePath: this.resourcePath,
-      filePathRelative: path.relative(app.dir.source(), this.resourcePath),
-      options: {},
     })
-    return renderPageSfcBlocksToVue(sfcBlocks)
+    return renderPageToVue(newPage)
   }
diff --git a/packages/cli/src/commands/dev/handlePageAdd.ts b/packages/cli/src/commands/dev/handlePageAdd.ts
index bcf5415a97..4c8d7c2512 100644
--- a/packages/cli/src/commands/dev/handlePageAdd.ts
+++ b/packages/cli/src/commands/dev/handlePageAdd.ts
@@ -1,10 +1,5 @@
 import type { App, Page } from '@vuepress/core'
-import {
-  createPage,
-  preparePageChunk,
-  preparePageComponent,
-  prepareRoutes,
-} from '@vuepress/core'
+import { createPage, preparePageChunk, prepareRoutes } from '@vuepress/core'
 
 /**
  * Event handler for page add event
@@ -28,9 +23,9 @@ export const handlePageAdd = async (
 
   // add the new page
   app.pages.push(page)
+  app.pagesMap[filePath] = page
 
-  // prepare page files
-  await preparePageComponent(app, page)
+  // prepare page file
   await preparePageChunk(app, page)
 
   // prepare routes file
diff --git a/packages/cli/src/commands/dev/handlePageChange.ts b/packages/cli/src/commands/dev/handlePageChange.ts
index c2255096f0..c7e29debf6 100644
--- a/packages/cli/src/commands/dev/handlePageChange.ts
+++ b/packages/cli/src/commands/dev/handlePageChange.ts
@@ -1,10 +1,5 @@
 import type { App, Page } from '@vuepress/core'
-import {
-  createPage,
-  preparePageChunk,
-  preparePageComponent,
-  prepareRoutes,
-} from '@vuepress/core'
+import { createPage, preparePageChunk, prepareRoutes } from '@vuepress/core'
 
 /**
  * Event handler for page change event
@@ -31,9 +26,9 @@ export const handlePageChange = async (
 
   // replace the old page with the new page
   app.pages.splice(pageIndex, 1, pageNew)
+  app.pagesMap[filePath] = pageNew
 
   // prepare page files
-  await preparePageComponent(app, pageNew)
   await preparePageChunk(app, pageNew)
 
   const isPathChanged = pageOld.path !== pageNew.path
diff --git a/packages/cli/src/commands/dev/handlePageUnlink.ts b/packages/cli/src/commands/dev/handlePageUnlink.ts
index ad4d01a2c4..40dca36d4e 100644
--- a/packages/cli/src/commands/dev/handlePageUnlink.ts
+++ b/packages/cli/src/commands/dev/handlePageUnlink.ts
@@ -20,6 +20,7 @@ export const handlePageUnlink = async (
 
   // remove the old page
   app.pages.splice(pageIndex, 1)
+  delete app.pagesMap[filePath]
 
   // re-prepare routes file
   await prepareRoutes(app)
diff --git a/packages/client/src/components/Content.ts b/packages/client/src/components/Content.ts
index e58b46214d..c17bc44a3a 100644
--- a/packages/client/src/components/Content.ts
+++ b/packages/client/src/components/Content.ts
@@ -22,7 +22,7 @@ export const Content = defineComponent({
       if (!props.path) return pageComponent.value
       const route = resolveRoute(props.path)
       return defineAsyncComponent(async () =>
-        route.loader().then(({ comp }) => comp),
+        route.loader().then((m) => m.default),
       )
     })
 
diff --git a/packages/client/src/setupGlobalComputed.ts b/packages/client/src/setupGlobalComputed.ts
index e258cb7673..79c8fe459a 100644
--- a/packages/client/src/setupGlobalComputed.ts
+++ b/packages/client/src/setupGlobalComputed.ts
@@ -47,12 +47,15 @@ export const setupGlobalComputed = (
   if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
     __VUE_HMR_RUNTIME__.updatePageData = async (newPageData: PageData) => {
       const oldPageChunk = await routes.value[newPageData.path].loader()
-      const newPageChunk = { comp: oldPageChunk.comp, data: newPageData }
+      const newPageChunk: PageChunk = {
+        default: oldPageChunk.default,
+        _pageData: newPageData,
+      }
       routes.value[newPageData.path].loader = async () =>
         Promise.resolve(newPageChunk)
       if (
         newPageData.path ===
-        router.currentRoute.value.meta._pageChunk?.data.path
+        router.currentRoute.value.meta._pageChunk?._pageData.path
       ) {
         pageChunk.value = newPageChunk
       }
@@ -67,8 +70,8 @@ export const setupGlobalComputed = (
   const siteLocaleData = computed(() =>
     resolvers.resolveSiteLocaleData(siteData.value, routeLocale.value),
   )
-  const pageComponent = computed(() => pageChunk.value.comp)
-  const pageData = computed(() => pageChunk.value.data)
+  const pageComponent = computed(() => pageChunk.value.default)
+  const pageData = computed(() => pageChunk.value._pageData)
   const pageFrontmatter = computed(() => pageData.value.frontmatter)
   const pageHeadTitle = computed(() =>
     resolvers.resolvePageHeadTitle(pageData.value, siteLocaleData.value),
diff --git a/packages/client/src/types/routes.ts b/packages/client/src/types/routes.ts
index 6aa5d10871..c102dee700 100644
--- a/packages/client/src/types/routes.ts
+++ b/packages/client/src/types/routes.ts
@@ -2,8 +2,8 @@ import type { Component } from 'vue'
 import type { PageData } from '../types/index.js'
 
 export interface PageChunk {
-  comp: Component
-  data: PageData
+  default: Component
+  _pageData: PageData
 }
 
 export type RouteMeta = Record<string, unknown>
diff --git a/packages/core/src/app/appPrepare.ts b/packages/core/src/app/appPrepare.ts
index f8d2585f50..a41f8d72be 100644
--- a/packages/core/src/app/appPrepare.ts
+++ b/packages/core/src/app/appPrepare.ts
@@ -3,7 +3,6 @@ import type { App } from '../types/index.js'
 import {
   prepareClientConfigs,
   preparePageChunk,
-  preparePageComponent,
   prepareRoutes,
   prepareSiteData,
 } from './prepare/index.js'
@@ -13,7 +12,6 @@ const log = debug('vuepress:core/app')
 /**
  * Prepare files for development or build
  *
- * - page components
  * - page chunks
  * - routes
  * - site data
@@ -23,10 +21,7 @@ export const appPrepare = async (app: App): Promise<void> => {
   log('prepare start')
 
   await Promise.all([
-    ...app.pages.flatMap((page) => [
-      preparePageComponent(app, page),
-      preparePageChunk(app, page),
-    ]),
+    ...app.pages.map(async (page) => preparePageChunk(app, page)),
     prepareRoutes(app),
     prepareSiteData(app),
     prepareClientConfigs(app),
diff --git a/packages/core/src/app/prepare/index.ts b/packages/core/src/app/prepare/index.ts
index ea49aa7600..460f3b497f 100644
--- a/packages/core/src/app/prepare/index.ts
+++ b/packages/core/src/app/prepare/index.ts
@@ -1,5 +1,4 @@
 export * from './prepareClientConfigs.js'
 export * from './preparePageChunk.js'
-export * from './preparePageComponent.js'
 export * from './prepareRoutes.js'
 export * from './prepareSiteData.js'
diff --git a/packages/core/src/app/prepare/preparePageChunk.ts b/packages/core/src/app/prepare/preparePageChunk.ts
index 353a98eee1..2ade465418 100644
--- a/packages/core/src/app/prepare/preparePageChunk.ts
+++ b/packages/core/src/app/prepare/preparePageChunk.ts
@@ -1,35 +1,11 @@
+import { renderPageToVue } from '../../page/index.js'
 import type { App, Page } from '../../types/index.js'
 
-const HMR_CODE = `
-if (import.meta.webpackHot) {
-  import.meta.webpackHot.accept()
-  if (__VUE_HMR_RUNTIME__.updatePageData) {
-    __VUE_HMR_RUNTIME__.updatePageData(data)
-  }
-}
-
-if (import.meta.hot) {
-  import.meta.hot.accept(({ data }) => {
-    __VUE_HMR_RUNTIME__.updatePageData(data)
-  })
-}
-`
-
 /**
- * Generate page chunk temp file of a single page
+ * Generate temp file the page does not have a source file
  */
 export const preparePageChunk = async (app: App, page: Page): Promise<void> => {
-  // page chunk file content
-  let content = `\
-import comp from ${JSON.stringify(page.componentFilePath)}
-const data = JSON.parse(${JSON.stringify(JSON.stringify(page.data))})
-export { comp, data }
-`
-
-  // inject HMR code
-  if (app.env.isDev) {
-    content += HMR_CODE
+  if (page.filePath === null) {
+    await app.writeTemp(page.chunkFilePathRelative, renderPageToVue(page))
   }
-
-  await app.writeTemp(page.chunkFilePathRelative, content)
 }
diff --git a/packages/core/src/app/prepare/preparePageComponent.ts b/packages/core/src/app/prepare/preparePageComponent.ts
deleted file mode 100644
index e88d6123d5..0000000000
--- a/packages/core/src/app/prepare/preparePageComponent.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { renderPageSfcBlocksToVue } from '../../page/index.js'
-import type { App, Page } from '../../types/index.js'
-
-/**
- * Generate page component temp file if the page does not have a source file
- */
-export const preparePageComponent = async (
-  app: App,
-  page: Page,
-): Promise<void> => {
-  if (page.filePath === null) {
-    await app.writeTemp(
-      page.componentFilePathRelative,
-      renderPageSfcBlocksToVue(page.sfcBlocks),
-    )
-  }
-}
diff --git a/packages/core/src/page/createPage.ts b/packages/core/src/page/createPage.ts
index 0c48399162..9965abf4a4 100644
--- a/packages/core/src/page/createPage.ts
+++ b/packages/core/src/page/createPage.ts
@@ -2,9 +2,8 @@ import type { App, Page, PageOptions } from '../types/index.js'
 import { inferPagePath } from './inferPagePath.js'
 import { parsePageContent } from './parsePageContent.js'
 import { resolvePageChunkInfo } from './resolvePageChunkInfo.js'
-import { resolvePageComponentInfo } from './resolvePageComponentInfo.js'
+import { resolvePageContent } from './resolvePageContent.js'
 import { resolvePageDate } from './resolvePageDate.js'
-import { resolvePageFileContent } from './resolvePageFileContent.js'
 import { resolvePageFilePath } from './resolvePageFilePath.js'
 import { resolvePageHtmlInfo } from './resolvePageHtmlInfo.js'
 import { resolvePageLang } from './resolvePageLang.js'
@@ -27,7 +26,7 @@ export const createPage = async (
   })
 
   // read the raw file content according to the absolute file path
-  const content = await resolvePageFileContent({ filePath, options })
+  const content = await resolvePageContent({ filePath, options })
 
   // render page content and extract information
   const {
@@ -81,18 +80,14 @@ export const createPage = async (
     path,
   })
 
-  // resolve page component and extract headers & links
-  const { componentFilePath, componentFilePathRelative } =
-    resolvePageComponentInfo({
+  const { chunkFilePath, chunkFilePathRelative, chunkName } =
+    resolvePageChunkInfo({
       app,
       filePath,
       filePathRelative,
       htmlFilePathRelative,
     })
 
-  const { chunkFilePath, chunkFilePathRelative, chunkName } =
-    resolvePageChunkInfo({ app, htmlFilePathRelative })
-
   const page: Page = {
     // page data
     data: {
@@ -127,8 +122,6 @@ export const createPage = async (
     // file info
     filePath,
     filePathRelative,
-    componentFilePath,
-    componentFilePathRelative,
     chunkFilePath,
     chunkFilePathRelative,
     chunkName,
diff --git a/packages/core/src/page/index.ts b/packages/core/src/page/index.ts
index 6e1bc3d592..e6bc9edd07 100644
--- a/packages/core/src/page/index.ts
+++ b/packages/core/src/page/index.ts
@@ -1,11 +1,10 @@
 export * from './createPage.js'
 export * from './inferPagePath.js'
 export * from './parsePageContent.js'
-export * from './renderPageSfcBlocksToVue.js'
+export * from './renderPageToVue.js'
 export * from './resolvePageChunkInfo.js'
-export * from './resolvePageComponentInfo.js'
 export * from './resolvePageDate.js'
-export * from './resolvePageFileContent.js'
+export * from './resolvePageContent.js'
 export * from './resolvePageFilePath.js'
 export * from './resolvePageHtmlInfo.js'
 export * from './resolvePageLang.js'
diff --git a/packages/core/src/page/renderPageSfcBlocksToVue.ts b/packages/core/src/page/renderPageSfcBlocksToVue.ts
deleted file mode 100644
index 7428643823..0000000000
--- a/packages/core/src/page/renderPageSfcBlocksToVue.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { MarkdownSfcBlocks } from '@vuepress/markdown'
-
-/**
- * Render page sfc blocks to vue component
- */
-export const renderPageSfcBlocksToVue = (
-  sfcBlocks: MarkdownSfcBlocks,
-): string =>
-  [
-    // #688: wrap the content of `<template>` with a `<div>` to avoid some potential issues of fragment component
-    `${sfcBlocks.template?.tagOpen}<div>${sfcBlocks.template?.contentStripped}</div>${sfcBlocks.template?.tagClose}\n`,
-    // hoist `<script>`, `<style>` and other custom blocks
-    sfcBlocks.script?.content,
-    sfcBlocks.scriptSetup?.content,
-    ...sfcBlocks.styles.map((item) => item.content),
-    ...sfcBlocks.customBlocks.map((item) => item.content),
-  ].join('\n')
diff --git a/packages/core/src/page/renderPageToVue.ts b/packages/core/src/page/renderPageToVue.ts
new file mode 100644
index 0000000000..849e647d4c
--- /dev/null
+++ b/packages/core/src/page/renderPageToVue.ts
@@ -0,0 +1,79 @@
+import { isString } from '@vuepress/shared'
+import type { Page } from '../types/index.js'
+
+const TEMPLATE_WRAPPER_TAG_OPEN = '<div>'
+const TEMPLATE_WRAPPER_TAG_CLOSE = '</div>'
+
+const SCRIPT_TAG_OPEN = '<script>'
+const SCRIPT_TAG_CLOSE = '</script>'
+
+const SCRIPT_TAG_OPEN_LANG_TS_REGEX = /<\s*script[^>]*\blang=['"]ts['"][^>]*/
+const SCRIPT_TAG_OPEN_LANG_TS = '<script lang="ts">'
+
+const HMR_CODE = `
+if (import.meta.webpackHot) {
+  import.meta.webpackHot.accept()
+  if (__VUE_HMR_RUNTIME__.updatePageData) {
+    __VUE_HMR_RUNTIME__.updatePageData(_pageData)
+  }
+}
+
+if (import.meta.hot) {
+  import.meta.hot.accept(({ _pageData }) => {
+    __VUE_HMR_RUNTIME__.updatePageData(_pageData)
+  })
+}
+`
+
+/**
+ * Util to resolve the open tag of script block
+ */
+const resolveScriptTagOpen = (sfcBlocks: Page['sfcBlocks']): string => {
+  // use existing script open tag
+  if (sfcBlocks.script?.tagOpen) {
+    return sfcBlocks.script.tagOpen
+  }
+  // if the setup script block is using typescript, we should use the same language for script block
+  const isUsingTs = sfcBlocks.scriptSetup?.tagOpen.match(
+    SCRIPT_TAG_OPEN_LANG_TS_REGEX,
+  )
+  return isUsingTs ? SCRIPT_TAG_OPEN_LANG_TS : SCRIPT_TAG_OPEN
+}
+
+/**
+ * Render page to vue component
+ */
+export const renderPageToVue = ({ data, sfcBlocks }: Page): string => {
+  // #688: wrap the content of `<template>` with a `<div>` to avoid some potential issues of fragment component
+  const templateContent =
+    sfcBlocks.template &&
+    [
+      sfcBlocks.template.tagOpen,
+      TEMPLATE_WRAPPER_TAG_OPEN,
+      sfcBlocks.template.contentStripped,
+      TEMPLATE_WRAPPER_TAG_CLOSE,
+      sfcBlocks.template.tagClose,
+    ].join('')
+
+  // inject page data code and HMR code into the script content
+  const pageDataCode = `export const _pageData = JSON.parse(${JSON.stringify(JSON.stringify(data))})`
+  const scriptContent = [
+    resolveScriptTagOpen(sfcBlocks),
+    sfcBlocks.script?.contentStripped,
+    pageDataCode,
+    HMR_CODE,
+    sfcBlocks.script?.tagClose ?? SCRIPT_TAG_CLOSE,
+  ]
+    .filter(isString)
+    .join('\n')
+
+  return [
+    templateContent,
+    scriptContent,
+    sfcBlocks.scriptSetup?.content,
+    ...sfcBlocks.styles.map((item) => item.content),
+    ...sfcBlocks.customBlocks.map((item) => item.content),
+  ]
+    .filter(isString)
+    .join('\n')
+}
diff --git a/packages/core/src/page/resolvePageChunkInfo.ts b/packages/core/src/page/resolvePageChunkInfo.ts
index 037018c6cc..b297794c04 100644
--- a/packages/core/src/page/resolvePageChunkInfo.ts
+++ b/packages/core/src/page/resolvePageChunkInfo.ts
@@ -2,23 +2,41 @@ import { path, transformPathToFileName } from '@vuepress/utils'
 import type { App } from '../types/index.js'
 
 /**
- * Resolve page data file path
+ * Resolve page chunk file relative info
  */
 export const resolvePageChunkInfo = ({
   app,
+  filePath,
+  filePathRelative,
   htmlFilePathRelative,
 }: {
   app: App
+  filePath: string | null
+  filePathRelative: string | null
   htmlFilePathRelative: string
 }): {
   chunkFilePath: string
   chunkFilePathRelative: string
   chunkName: string
 } => {
-  const chunkFilePathRelative = path.join('pages', `${htmlFilePathRelative}.js`)
-  const chunkFilePath = app.dir.temp(chunkFilePathRelative)
   const chunkName = transformPathToFileName(htmlFilePathRelative)
 
+  // if there is a source file for the page, use it directly as the page chunk
+  if (filePath && filePathRelative) {
+    return {
+      chunkFilePath: filePath,
+      chunkFilePathRelative: filePathRelative,
+      chunkName,
+    }
+  }
+
+  // otherwise, generate a temp file for the page
+  const chunkFilePathRelative = path.join(
+    'pages',
+    `${htmlFilePathRelative}.vue`,
+  )
+  const chunkFilePath = app.dir.temp(chunkFilePathRelative)
+
   return {
     chunkFilePath,
     chunkFilePathRelative,
diff --git a/packages/core/src/page/resolvePageComponentInfo.ts b/packages/core/src/page/resolvePageComponentInfo.ts
deleted file mode 100644
index 2105bdb187..0000000000
--- a/packages/core/src/page/resolvePageComponentInfo.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { path } from '@vuepress/utils'
-import type { App } from '../types/index.js'
-
-/**
- * Resolve page component and related info
- */
-export const resolvePageComponentInfo = ({
-  app,
-  filePath,
-  filePathRelative,
-  htmlFilePathRelative,
-}: {
-  app: App
-  filePath: string | null
-  filePathRelative: string | null
-  htmlFilePathRelative: string
-}): {
-  componentFilePath: string
-  componentFilePathRelative: string
-} => {
-  // if there is a source file for the page, use it as the component file
-  if (filePath && filePathRelative) {
-    return {
-      componentFilePath: filePath,
-      componentFilePathRelative: filePathRelative,
-    }
-  }
-  // otherwise, generate a component file for the page
-  const componentFilePathRelative = path.join(
-    'pages',
-    `${htmlFilePathRelative}.vue`,
-  )
-  const componentFilePath = app.dir.temp(componentFilePathRelative)
-
-  return {
-    componentFilePath,
-    componentFilePathRelative,
-  }
-}
diff --git a/packages/core/src/page/resolvePageFileContent.ts b/packages/core/src/page/resolvePageContent.ts
similarity index 51%
rename from packages/core/src/page/resolvePageFileContent.ts
rename to packages/core/src/page/resolvePageContent.ts
index e05bf9a501..c4bda0732b 100644
--- a/packages/core/src/page/resolvePageFileContent.ts
+++ b/packages/core/src/page/resolvePageContent.ts
@@ -1,21 +1,30 @@
+import { isString } from '@vuepress/shared'
 import { debug, fs } from '@vuepress/utils'
 import type { PageOptions } from '../types/index.js'
 
 const log = debug('vuepress:core/page')
 
+// fallback to empty string
+const FALLBACK_CONTENT = ''
+
 /**
- * Resolve page file content according to filePath or options content
+ * Resolve page content according to `content` or `filePath`
  */
-export const resolvePageFileContent = async ({
+export const resolvePageContent = async ({
   filePath,
   options,
 }: {
   filePath: string | null
   options: PageOptions
 }): Promise<string> => {
+  // if `content` is provided by options, use it directly
+  if (isString(options.content)) {
+    return options.content
+  }
+
+  // if `filePath` is resolved, read content from file
   if (filePath) {
     try {
-      // read page content from file
       const content = await fs.readFile(filePath, 'utf-8')
       return content
     } catch (e) {
@@ -23,6 +32,5 @@ export const resolvePageFileContent = async ({
     }
   }
 
-  // load raw content from options
-  return options.content ?? ''
+  return FALLBACK_CONTENT
 }
diff --git a/packages/core/src/types/page.ts b/packages/core/src/types/page.ts
index 178fec9782..ade17a2fd8 100644
--- a/packages/core/src/types/page.ts
+++ b/packages/core/src/types/page.ts
@@ -104,16 +104,6 @@ export type Page<
      */
     filePathRelative: string | null
 
-    /**
-     * Component file path
-     */
-    componentFilePath: string
-
-    /**
-     * Component file path relative to temp directory
-     */
-    componentFilePathRelative: string
-
     /**
      * Chunk file path
      */
@@ -147,11 +137,10 @@ export type Page<
  */
 export interface PageOptions {
   /**
-   * If `filePath` is not set, this option will be used as the raw
-   * markdown content of the page.
+   * The raw markdown content of the page.
    *
-   * If `filePath` is set, this option will be ignored, while the
-   * content of the file will be used.
+   * If `content` is not provided, the file content of the `filePath`
+   * will be used.
    */
   content?: string
 
diff --git a/packages/core/tests/page/resolvePageFileContent.spec.ts b/packages/core/tests/page/resolvePageContent.spec.ts
similarity index 51%
rename from packages/core/tests/page/resolvePageFileContent.spec.ts
rename to packages/core/tests/page/resolvePageContent.spec.ts
index 41f92f9132..f7a22b50b4 100644
--- a/packages/core/tests/page/resolvePageFileContent.spec.ts
+++ b/packages/core/tests/page/resolvePageContent.spec.ts
@@ -1,10 +1,10 @@
 import { fs, path } from '@vuepress/utils'
 import { expect, it } from 'vitest'
-import { resolvePageFileContent } from '../../src/index.js'
+import { resolvePageContent } from '../../src/index.js'
 
 it('should resolve file content correctly from file path', async () => {
   const filePath = path.resolve(__dirname, '../__fixtures__/pages/foo.md')
-  const resolved = await resolvePageFileContent({ filePath, options: {} })
+  const resolved = await resolvePageContent({ filePath, options: {} })
 
   const expected = (await fs.readFile(filePath)).toString()
   expect(resolved).toBe(expected)
@@ -12,24 +12,40 @@ it('should resolve file content correctly from file path', async () => {
 
 it('should use content from page options', async () => {
   const content = 'foobar'
-  const resolved = await resolvePageFileContent({
+  const resolved = await resolvePageContent({
     filePath: null,
     options: { content },
   })
-  expect(resolved).toBe(resolved)
+
+  const expected = content
+  expect(resolved).toBe(expected)
 })
 
 it('should return empty string if nothing provided', async () => {
-  const resolved = await resolvePageFileContent({
+  const resolved = await resolvePageContent({
     filePath: null,
     options: {},
   })
-  expect(resolved).toBe('')
+
+  const expected = ''
+  expect(resolved).toBe(expected)
+})
+
+it('should use content from page options and ignore file path', async () => {
+  const filePath = path.resolve(__dirname, '../__fixtures__/pages/foo.md')
+  const content = 'foobar'
+  const resolved = await resolvePageContent({
+    filePath,
+    options: { content },
+  })
+
+  const expected = content
+  expect(resolved).toBe(expected)
 })
 
 it('should throw error if the file does not exist', async () => {
   try {
-    await resolvePageFileContent({
+    await resolvePageContent({
       filePath: '404',
       options: {},
     })

From e7b4e27f9bedb50e70abd8a4e76d7c5ff53f817c Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 12:34:59 +0800
Subject: [PATCH 03/11] chore: tweaks

---
 eslint.config.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/eslint.config.ts b/eslint.config.ts
index 1cb1edfb7b..77ca52bc6c 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -19,8 +19,8 @@ export default vuepress(
             allow: [
               '__dirname',
               '_context',
-              '_pageData',
               '_pageChunk',
+              '_pageData',
               '_registeredComponents',
             ],
           },

From 3bb73dcc3f8148420e127af155abd6a0f773398f Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 12:47:12 +0800
Subject: [PATCH 04/11] refactor: improve hmr code injection

---
 .../core/src/app/prepare/prepareRoutes.ts     | 21 ++++++-----
 .../core/src/app/prepare/prepareSiteData.ts   | 12 +++----
 packages/core/src/page/renderPageToVue.ts     | 35 +++++++++++++------
 3 files changed, 41 insertions(+), 27 deletions(-)

diff --git a/packages/core/src/app/prepare/prepareRoutes.ts b/packages/core/src/app/prepare/prepareRoutes.ts
index b3556e8041..c60d840425 100644
--- a/packages/core/src/app/prepare/prepareRoutes.ts
+++ b/packages/core/src/app/prepare/prepareRoutes.ts
@@ -1,21 +1,20 @@
 import { normalizeRoutePath } from '@vuepress/shared'
 import type { App, Page } from '../../types/index.js'
 
+const ROUTES_VAR_NAME = 'routes'
+const REDIRECTS_VAR_NAME = 'redirects'
+
 const HMR_CODE = `
 if (import.meta.webpackHot) {
   import.meta.webpackHot.accept()
-  if (__VUE_HMR_RUNTIME__.updateRoutes) {
-    __VUE_HMR_RUNTIME__.updateRoutes(routes)
-  }
-  if (__VUE_HMR_RUNTIME__.updateRedirects) {
-    __VUE_HMR_RUNTIME__.updateRedirects(redirects)
-  }
+  __VUE_HMR_RUNTIME__.updateRoutes?.(${ROUTES_VAR_NAME})
+  __VUE_HMR_RUNTIME__.updateRedirects?.(${REDIRECTS_VAR_NAME}})
 }
 
 if (import.meta.hot) {
-  import.meta.hot.accept(({ routes, redirects }) => {
-    __VUE_HMR_RUNTIME__.updateRoutes(routes)
-    __VUE_HMR_RUNTIME__.updateRedirects(redirects)
+  import.meta.hot.accept((m) => {
+    __VUE_HMR_RUNTIME__.updateRoutes?.(m.${ROUTES_VAR_NAME})
+    __VUE_HMR_RUNTIME__.updateRedirects?.(m.${REDIRECTS_VAR_NAME})
   })
 }
 `
@@ -47,7 +46,7 @@ const resolvePageRedirects = ({ path, pathInferred }: Page): string[] => {
 export const prepareRoutes = async (app: App): Promise<void> => {
   // routes file content
   let content = `\
-export const redirects = JSON.parse(${JSON.stringify(
+export const ${REDIRECTS_VAR_NAME} = JSON.parse(${JSON.stringify(
     JSON.stringify(
       Object.fromEntries(
         app.pages.flatMap((page) =>
@@ -57,7 +56,7 @@ export const redirects = JSON.parse(${JSON.stringify(
     ),
   )})
 
-export const routes = Object.fromEntries([
+export const ${ROUTES_VAR_NAME} = Object.fromEntries([
 ${app.pages
   .map(
     ({ chunkFilePath, chunkName, path, routeMeta }) =>
diff --git a/packages/core/src/app/prepare/prepareSiteData.ts b/packages/core/src/app/prepare/prepareSiteData.ts
index c2749c0dc2..af0027fdd9 100644
--- a/packages/core/src/app/prepare/prepareSiteData.ts
+++ b/packages/core/src/app/prepare/prepareSiteData.ts
@@ -1,16 +1,16 @@
 import type { App } from '../../types/index.js'
 
+const SITE_DATA_VAR_NAME = 'siteData'
+
 const HMR_CODE = `
 if (import.meta.webpackHot) {
   import.meta.webpackHot.accept()
-  if (__VUE_HMR_RUNTIME__.updateSiteData) {
-    __VUE_HMR_RUNTIME__.updateSiteData(siteData)
-  }
+  __VUE_HMR_RUNTIME__.updateSiteData?.(${SITE_DATA_VAR_NAME})
 }
 
 if (import.meta.hot) {
-  import.meta.hot.accept(({ siteData }) => {
-    __VUE_HMR_RUNTIME__.updateSiteData(siteData)
+  import.meta.hot.accept((m) => {
+    __VUE_HMR_RUNTIME__.updateSiteData?.(m.${SITE_DATA_VAR_NAME})
   })
 }
 `
@@ -20,7 +20,7 @@ if (import.meta.hot) {
  */
 export const prepareSiteData = async (app: App): Promise<void> => {
   let content = `\
-export const siteData = JSON.parse(${JSON.stringify(
+export const ${SITE_DATA_VAR_NAME} = JSON.parse(${JSON.stringify(
     JSON.stringify(app.siteData),
   )})
 `
diff --git a/packages/core/src/page/renderPageToVue.ts b/packages/core/src/page/renderPageToVue.ts
index 849e647d4c..14f6fb8311 100644
--- a/packages/core/src/page/renderPageToVue.ts
+++ b/packages/core/src/page/renderPageToVue.ts
@@ -1,5 +1,5 @@
 import { isString } from '@vuepress/shared'
-import type { Page } from '../types/index.js'
+import type { App, Page } from '../types/index.js'
 
 const TEMPLATE_WRAPPER_TAG_OPEN = '<div>'
 const TEMPLATE_WRAPPER_TAG_CLOSE = '</div>'
@@ -10,21 +10,32 @@ const SCRIPT_TAG_CLOSE = '</script>'
 const SCRIPT_TAG_OPEN_LANG_TS_REGEX = /<\s*script[^>]*\blang=['"]ts['"][^>]*/
 const SCRIPT_TAG_OPEN_LANG_TS = '<script lang="ts">'
 
+const PAGE_DATA_CODE_VAR_NAME = '_pageData'
+const PAGE_DATA_CODE_TEMPLATE_OUTLET = '__PAGE_DATA__'
+const PAGE_DATA_CODE_TEMPLATE = `export const ${PAGE_DATA_CODE_VAR_NAME} = JSON.parse(${PAGE_DATA_CODE_TEMPLATE_OUTLET})`
+
 const HMR_CODE = `
 if (import.meta.webpackHot) {
   import.meta.webpackHot.accept()
-  if (__VUE_HMR_RUNTIME__.updatePageData) {
-    __VUE_HMR_RUNTIME__.updatePageData(_pageData)
-  }
+  __VUE_HMR_RUNTIME__.updatePageData?.(${PAGE_DATA_CODE_VAR_NAME})
 }
 
 if (import.meta.hot) {
-  import.meta.hot.accept(({ _pageData }) => {
-    __VUE_HMR_RUNTIME__.updatePageData(_pageData)
+  import.meta.hot.accept((m) => {
+    __VUE_HMR_RUNTIME__.updatePageData?.(m.${PAGE_DATA_CODE_VAR_NAME})
   })
 }
 `
 
+/**
+ * Util to resolve the page data code
+ */
+const resolvePageDataCode = (data: Page['data']): string =>
+  PAGE_DATA_CODE_TEMPLATE.replace(
+    PAGE_DATA_CODE_TEMPLATE_OUTLET,
+    JSON.stringify(JSON.stringify(data)),
+  )
+
 /**
  * Util to resolve the open tag of script block
  */
@@ -43,7 +54,10 @@ const resolveScriptTagOpen = (sfcBlocks: Page['sfcBlocks']): string => {
 /**
  * Render page to vue component
  */
-export const renderPageToVue = ({ data, sfcBlocks }: Page): string => {
+export const renderPageToVue = (
+  app: App,
+  { data, sfcBlocks }: Page,
+): string => {
   // #688: wrap the content of `<template>` with a `<div>` to avoid some potential issues of fragment component
   const templateContent =
     sfcBlocks.template &&
@@ -56,12 +70,13 @@ export const renderPageToVue = ({ data, sfcBlocks }: Page): string => {
     ].join('')
 
   // inject page data code and HMR code into the script content
-  const pageDataCode = `export const _pageData = JSON.parse(${JSON.stringify(JSON.stringify(data))})`
+  const scriptTagOpen = resolveScriptTagOpen(sfcBlocks)
+  const pageDataCode = resolvePageDataCode(data)
   const scriptContent = [
-    resolveScriptTagOpen(sfcBlocks),
+    scriptTagOpen,
     sfcBlocks.script?.contentStripped,
     pageDataCode,
-    HMR_CODE,
+    app.env.isDev && HMR_CODE,
     sfcBlocks.script?.tagClose ?? SCRIPT_TAG_CLOSE,
   ]
     .filter(isString)

From cb80a9e637a67165c8f7252c1fcceb7d6947d63f Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 12:48:45 +0800
Subject: [PATCH 05/11] chore: fix params

---
 packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts | 6 +++---
 .../bundler-webpack/src/loaders/vuepressMarkdownLoader.ts   | 4 ++--
 packages/core/src/app/prepare/preparePageChunk.ts           | 2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts b/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
index 94da9828c8..9dc2ec29c9 100644
--- a/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
+++ b/packages/bundler-vite/src/plugins/vuepressMarkdownPlugin.ts
@@ -18,7 +18,7 @@ export const vuepressMarkdownPlugin = ({ app }: { app: App }): Plugin => ({
 
     // if the page content is not changed, render it to vue component directly
     if (page?.content === code) {
-      return renderPageToVue(page)
+      return renderPageToVue(app, page)
     }
 
     // create a new page with the new content
@@ -26,7 +26,7 @@ export const vuepressMarkdownPlugin = ({ app }: { app: App }): Plugin => ({
       content: code,
       filePath: id,
     })
-    return renderPageToVue(newPage)
+    return renderPageToVue(app, newPage)
   },
 
   async handleHotUpdate(ctx) {
@@ -41,6 +41,6 @@ export const vuepressMarkdownPlugin = ({ app }: { app: App }): Plugin => ({
       filePath: ctx.file,
     })
 
-    ctx.read = () => renderPageToVue(newPage)
+    ctx.read = () => renderPageToVue(app, newPage)
   },
 })
diff --git a/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts
index 819dc29f66..df071dfc90 100644
--- a/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts
+++ b/packages/bundler-webpack/src/loaders/vuepressMarkdownLoader.ts
@@ -21,7 +21,7 @@ export const vuepressMarkdownLoader: LoaderDefinitionFunction<VuepressMarkdownLo
 
     // if the page content is not changed, render it to vue component directly
     if (page?.content === source) {
-      return renderPageToVue(page)
+      return renderPageToVue(app, page)
     }
 
     // create a new page with the new content
@@ -29,5 +29,5 @@ export const vuepressMarkdownLoader: LoaderDefinitionFunction<VuepressMarkdownLo
       content: source,
       filePath: this.resourcePath,
     })
-    return renderPageToVue(newPage)
+    return renderPageToVue(app, newPage)
   }
diff --git a/packages/core/src/app/prepare/preparePageChunk.ts b/packages/core/src/app/prepare/preparePageChunk.ts
index 2ade465418..0dd2475008 100644
--- a/packages/core/src/app/prepare/preparePageChunk.ts
+++ b/packages/core/src/app/prepare/preparePageChunk.ts
@@ -6,6 +6,6 @@ import type { App, Page } from '../../types/index.js'
  */
 export const preparePageChunk = async (app: App, page: Page): Promise<void> => {
   if (page.filePath === null) {
-    await app.writeTemp(page.chunkFilePathRelative, renderPageToVue(page))
+    await app.writeTemp(page.chunkFilePathRelative, renderPageToVue(app, page))
   }
 }

From 5f156ae1ef2bb68ff8c004c07551267e7aa05fc4 Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 12:58:17 +0800
Subject: [PATCH 06/11] refactor: normalize module config

---
 .../src/config/handleModuleVue.ts             | 25 ++++++++-----------
 1 file changed, 10 insertions(+), 15 deletions(-)

diff --git a/packages/bundler-webpack/src/config/handleModuleVue.ts b/packages/bundler-webpack/src/config/handleModuleVue.ts
index 4065ff16a7..cca19e8fab 100644
--- a/packages/bundler-webpack/src/config/handleModuleVue.ts
+++ b/packages/bundler-webpack/src/config/handleModuleVue.ts
@@ -24,13 +24,15 @@ export const handleModuleVue = ({
   isBuild: boolean
   isServer: boolean
 }): void => {
-  const applyVuePipeline = ({
-    rule,
-    isMd,
+  const handleVue = ({
+    lang,
+    test,
   }: {
-    rule: Config.Rule
-    isMd: boolean
+    lang: 'md' | 'vue'
+    test: RegExp
   }): void => {
+    const rule = config.module.rule(lang).test(test)
+
     // use internal vuepress-ssr-loader to handle SSR dependencies
     if (isBuild) {
       rule
@@ -50,7 +52,7 @@ export const handleModuleVue = ({
       .end()
 
     // use internal vuepress-markdown-loader to handle markdown files
-    if (isMd) {
+    if (lang === 'md') {
       rule
         .use('vuepress-markdown-loader')
         .loader(require.resolve('#vuepress-markdown-loader'))
@@ -59,15 +61,8 @@ export const handleModuleVue = ({
     }
   }
 
-  applyVuePipeline({
-    rule: config.module.rule('md').test(/\.md$/),
-    isMd: true,
-  })
-
-  applyVuePipeline({
-    rule: config.module.rule('vue').test(/\.vue$/),
-    isMd: false,
-  })
+  handleVue({ lang: 'md', test: /\.md$/ })
+  handleVue({ lang: 'vue', test: /\.vue$/ })
 
   // use vue-loader plugin
   config.plugin('vue-loader').use(VueLoaderPlugin)

From e597cab94f909885639f772183502a1378f9565e Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 13:07:56 +0800
Subject: [PATCH 07/11] test: fix unit tests

---
 .../core/src/app/prepare/prepareRoutes.ts     |   2 +-
 packages/core/src/types/page.ts               | 242 +++++++++---------
 packages/core/tests/page/createPage.spec.ts   |  12 +-
 .../tests/page/resolvePageChunkInfo.spec.ts   |  23 +-
 .../page/resolvePageComponentInfo.spec.ts     |  38 ---
 5 files changed, 144 insertions(+), 173 deletions(-)
 delete mode 100644 packages/core/tests/page/resolvePageComponentInfo.spec.ts

diff --git a/packages/core/src/app/prepare/prepareRoutes.ts b/packages/core/src/app/prepare/prepareRoutes.ts
index c60d840425..d449d237d5 100644
--- a/packages/core/src/app/prepare/prepareRoutes.ts
+++ b/packages/core/src/app/prepare/prepareRoutes.ts
@@ -8,7 +8,7 @@ const HMR_CODE = `
 if (import.meta.webpackHot) {
   import.meta.webpackHot.accept()
   __VUE_HMR_RUNTIME__.updateRoutes?.(${ROUTES_VAR_NAME})
-  __VUE_HMR_RUNTIME__.updateRedirects?.(${REDIRECTS_VAR_NAME}})
+  __VUE_HMR_RUNTIME__.updateRedirects?.(${REDIRECTS_VAR_NAME})
 }
 
 if (import.meta.hot) {
diff --git a/packages/core/src/types/page.ts b/packages/core/src/types/page.ts
index ade17a2fd8..561b283af0 100644
--- a/packages/core/src/types/page.ts
+++ b/packages/core/src/types/page.ts
@@ -4,133 +4,131 @@ import type { PageBase, PageData, PageFrontmatter } from '@vuepress/shared'
 /**
  * Vuepress Page
  */
-export type Page<
+export interface Page<
   ExtraPageData extends Record<string, unknown> = Record<string, unknown>,
   ExtraPageFrontmatter extends Record<string, unknown> = Record<
     string,
     unknown
   >,
-  ExtraPageFields extends Record<string, unknown> = Record<string, unknown>,
-> = ExtraPageFields &
-  PageBase<ExtraPageFrontmatter> & {
-    /**
-     * Data of the page, which will be available in client code
-     */
-    data: PageData<ExtraPageData, ExtraPageFrontmatter>
-
-    /**
-     * Raw Content of the page
-     */
-    content: string
-
-    /**
-     * Rendered content of the page
-     */
-    contentRendered: string
-
-    /**
-     * Date of the page, in 'yyyy-MM-dd' format
-     *
-     * @example '2020-09-09'
-     */
-    date: string
-
-    /**
-     * Dependencies of the page
-     */
-    deps: string[]
-
-    /**
-     * Links of the page
-     */
-    links: MarkdownLink[]
-
-    /**
-     * Markdown env object of the page
-     */
-    markdownEnv: Record<string, unknown>
-
-    /**
-     * Path of the page that inferred from file path
-     *
-     * If the page does not come from a file, it would be `null`
-     *
-     * @example '/guide/index.html'
-     */
-    pathInferred: string | null
-
-    /**
-     * Locale path prefix of the page
-     *
-     * @example '/getting-started.html' -> '/'
-     * @example '/en/getting-started.html' -> '/en/'
-     * @example '/zh/getting-started.html' -> '/zh/'
-     */
-    pathLocale: string
-
-    /**
-     * Permalink of the page
-     *
-     * If the page does not have a permalink, it would be `null`
-     */
-    permalink: string | null
-
-    /**
-     * Custom data to be attached to route record
-     */
-    routeMeta: Record<string, unknown>
-
-    /**
-     * Extracted sfc blocks of the page
-     */
-    sfcBlocks: MarkdownSfcBlocks
-
-    /**
-     * Slug of the page
-     */
-    slug: string
-
-    /**
-     * Source file path
-     *
-     * If the page does not come from a file, it would be `null`
-     */
-    filePath: string | null
-
-    /**
-     * Source file path relative to source directory
-     *
-     * If the page does not come from a file, it would be `null`
-     */
-    filePathRelative: string | null
-
-    /**
-     * Chunk file path
-     */
-    chunkFilePath: string
-
-    /**
-     * Chunk file path relative to temp directory
-     */
-    chunkFilePathRelative: string
-
-    /**
-     * Chunk name
-     *
-     * This will only take effect in webpack
-     */
-    chunkName: string
-
-    /**
-     * Rendered html file path
-     */
-    htmlFilePath: string
-
-    /**
-     * Rendered html file path relative to dest directory
-     */
-    htmlFilePathRelative: string
-  }
+> extends PageBase<ExtraPageFrontmatter> {
+  /**
+   * Data of the page, which will be available in client code
+   */
+  data: PageData<ExtraPageData, ExtraPageFrontmatter>
+
+  /**
+   * Raw Content of the page
+   */
+  content: string
+
+  /**
+   * Rendered content of the page
+   */
+  contentRendered: string
+
+  /**
+   * Date of the page, in 'yyyy-MM-dd' format
+   *
+   * @example '2020-09-09'
+   */
+  date: string
+
+  /**
+   * Dependencies of the page
+   */
+  deps: string[]
+
+  /**
+   * Links of the page
+   */
+  links: MarkdownLink[]
+
+  /**
+   * Markdown env object of the page
+   */
+  markdownEnv: Record<string, unknown>
+
+  /**
+   * Path of the page that inferred from file path
+   *
+   * If the page does not come from a file, it would be `null`
+   *
+   * @example '/guide/index.html'
+   */
+  pathInferred: string | null
+
+  /**
+   * Locale path prefix of the page
+   *
+   * @example '/getting-started.html' -> '/'
+   * @example '/en/getting-started.html' -> '/en/'
+   * @example '/zh/getting-started.html' -> '/zh/'
+   */
+  pathLocale: string
+
+  /**
+   * Permalink of the page
+   *
+   * If the page does not have a permalink, it would be `null`
+   */
+  permalink: string | null
+
+  /**
+   * Custom data to be attached to route record
+   */
+  routeMeta: Record<string, unknown>
+
+  /**
+   * Extracted sfc blocks of the page
+   */
+  sfcBlocks: MarkdownSfcBlocks
+
+  /**
+   * Slug of the page
+   */
+  slug: string
+
+  /**
+   * Source file path
+   *
+   * If the page does not come from a file, it would be `null`
+   */
+  filePath: string | null
+
+  /**
+   * Source file path relative to source directory
+   *
+   * If the page does not come from a file, it would be `null`
+   */
+  filePathRelative: string | null
+
+  /**
+   * Chunk file path
+   */
+  chunkFilePath: string
+
+  /**
+   * Chunk file path relative to temp directory
+   */
+  chunkFilePathRelative: string
+
+  /**
+   * Chunk name
+   *
+   * This will only take effect in webpack
+   */
+  chunkName: string
+
+  /**
+   * Rendered html file path
+   */
+  htmlFilePath: string
+
+  /**
+   * Rendered html file path relative to dest directory
+   */
+  htmlFilePathRelative: string
+}
 
 /**
  * Options to create vuepress page
diff --git a/packages/core/tests/page/createPage.spec.ts b/packages/core/tests/page/createPage.spec.ts
index 313fed3876..f15fe825c6 100644
--- a/packages/core/tests/page/createPage.spec.ts
+++ b/packages/core/tests/page/createPage.spec.ts
@@ -73,19 +73,13 @@ describe('should work without plugins', () => {
     expect(page.filePathRelative).toBeNull()
     expect(page.htmlFilePath).toBe(app.dir.dest(`index.html`))
     expect(page.htmlFilePathRelative).toBe(`index.html`)
-    expect(page.componentFilePath).toBe(
-      app.dir.temp(`pages/${page.htmlFilePathRelative}.vue`),
-    )
-    expect(page.componentFilePathRelative).toBe(
-      `pages/${page.htmlFilePathRelative}.vue`,
-    )
     expect(page.chunkFilePath).toBe(
-      app.dir.temp(`pages/${page.htmlFilePathRelative}.js`),
+      app.dir.temp(`pages/${page.htmlFilePathRelative}.vue`),
     )
     expect(page.chunkFilePathRelative).toBe(
-      `pages/${page.htmlFilePathRelative}.js`,
+      `pages/${page.htmlFilePathRelative}.vue`,
     )
-    expect(page.chunkName).toBeTruthy()
+    expect(page.chunkName).toBe(`index.html`)
   })
 })
 
diff --git a/packages/core/tests/page/resolvePageChunkInfo.spec.ts b/packages/core/tests/page/resolvePageChunkInfo.spec.ts
index 738d3c1f05..bf4cc88274 100644
--- a/packages/core/tests/page/resolvePageChunkInfo.spec.ts
+++ b/packages/core/tests/page/resolvePageChunkInfo.spec.ts
@@ -9,15 +9,32 @@ const app = createBaseApp({
   bundler: {} as Bundler,
 })
 
-it('should resolve page chunk info correctly', () => {
+it('should resolve page chunk info correctly without source file path', () => {
   const resolved = resolvePageChunkInfo({
     app,
+    filePath: null,
+    filePathRelative: null,
     htmlFilePathRelative: 'foo.html',
   })
 
   expect(resolved).toEqual({
-    chunkFilePath: app.dir.temp('pages/foo.html.js'),
-    chunkFilePathRelative: 'pages/foo.html.js',
+    chunkFilePath: app.dir.temp('pages/foo.html.vue'),
+    chunkFilePathRelative: 'pages/foo.html.vue',
+    chunkName: sanitizeFileName('foo.html'),
+  })
+})
+
+it('should resolve page chunk info correctly with source file path', () => {
+  const resolved = resolvePageChunkInfo({
+    app,
+    filePath: app.dir.source('foo.md'),
+    filePathRelative: 'foo.md',
+    htmlFilePathRelative: 'foo.html',
+  })
+
+  expect(resolved).toEqual({
+    chunkFilePath: app.dir.source('foo.md'),
+    chunkFilePathRelative: 'foo.md',
     chunkName: sanitizeFileName('foo.html'),
   })
 })
diff --git a/packages/core/tests/page/resolvePageComponentInfo.spec.ts b/packages/core/tests/page/resolvePageComponentInfo.spec.ts
deleted file mode 100644
index c8a009e1ea..0000000000
--- a/packages/core/tests/page/resolvePageComponentInfo.spec.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { path } from '@vuepress/utils'
-import { expect, it } from 'vitest'
-import type { Bundler } from '../../src/index.js'
-import { createBaseApp, resolvePageComponentInfo } from '../../src/index.js'
-
-const app = createBaseApp({
-  source: path.resolve(__dirname, 'fake-source'),
-  theme: { name: 'test' },
-  bundler: {} as Bundler,
-})
-
-it('should resolve page component info correctly without source file path', () => {
-  const resolved = resolvePageComponentInfo({
-    app,
-    filePath: null,
-    filePathRelative: null,
-    htmlFilePathRelative: 'foo.html',
-  })
-
-  expect(resolved).toEqual({
-    componentFilePath: app.dir.temp('pages/foo.html.vue'),
-    componentFilePathRelative: 'pages/foo.html.vue',
-  })
-})
-
-it('should resolve page component info correctly with source file path', () => {
-  const resolved = resolvePageComponentInfo({
-    app,
-    filePath: app.dir.source('foo.md'),
-    filePathRelative: 'foo.md',
-    htmlFilePathRelative: 'foo.html',
-  })
-
-  expect(resolved).toEqual({
-    componentFilePath: app.dir.source('foo.md'),
-    componentFilePathRelative: 'foo.md',
-  })
-})

From 0e85a0036c88955c4faae7b5b06398eeb4042e7b Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 13:18:20 +0800
Subject: [PATCH 08/11] chore: revert page type update

---
 packages/core/src/types/page.ts | 242 ++++++++++++++++----------------
 1 file changed, 122 insertions(+), 120 deletions(-)

diff --git a/packages/core/src/types/page.ts b/packages/core/src/types/page.ts
index 561b283af0..ade17a2fd8 100644
--- a/packages/core/src/types/page.ts
+++ b/packages/core/src/types/page.ts
@@ -4,131 +4,133 @@ import type { PageBase, PageData, PageFrontmatter } from '@vuepress/shared'
 /**
  * Vuepress Page
  */
-export interface Page<
+export type Page<
   ExtraPageData extends Record<string, unknown> = Record<string, unknown>,
   ExtraPageFrontmatter extends Record<string, unknown> = Record<
     string,
     unknown
   >,
-> extends PageBase<ExtraPageFrontmatter> {
-  /**
-   * Data of the page, which will be available in client code
-   */
-  data: PageData<ExtraPageData, ExtraPageFrontmatter>
-
-  /**
-   * Raw Content of the page
-   */
-  content: string
-
-  /**
-   * Rendered content of the page
-   */
-  contentRendered: string
-
-  /**
-   * Date of the page, in 'yyyy-MM-dd' format
-   *
-   * @example '2020-09-09'
-   */
-  date: string
-
-  /**
-   * Dependencies of the page
-   */
-  deps: string[]
-
-  /**
-   * Links of the page
-   */
-  links: MarkdownLink[]
-
-  /**
-   * Markdown env object of the page
-   */
-  markdownEnv: Record<string, unknown>
-
-  /**
-   * Path of the page that inferred from file path
-   *
-   * If the page does not come from a file, it would be `null`
-   *
-   * @example '/guide/index.html'
-   */
-  pathInferred: string | null
-
-  /**
-   * Locale path prefix of the page
-   *
-   * @example '/getting-started.html' -> '/'
-   * @example '/en/getting-started.html' -> '/en/'
-   * @example '/zh/getting-started.html' -> '/zh/'
-   */
-  pathLocale: string
-
-  /**
-   * Permalink of the page
-   *
-   * If the page does not have a permalink, it would be `null`
-   */
-  permalink: string | null
-
-  /**
-   * Custom data to be attached to route record
-   */
-  routeMeta: Record<string, unknown>
-
-  /**
-   * Extracted sfc blocks of the page
-   */
-  sfcBlocks: MarkdownSfcBlocks
-
-  /**
-   * Slug of the page
-   */
-  slug: string
-
-  /**
-   * Source file path
-   *
-   * If the page does not come from a file, it would be `null`
-   */
-  filePath: string | null
-
-  /**
-   * Source file path relative to source directory
-   *
-   * If the page does not come from a file, it would be `null`
-   */
-  filePathRelative: string | null
-
-  /**
-   * Chunk file path
-   */
-  chunkFilePath: string
-
-  /**
-   * Chunk file path relative to temp directory
-   */
-  chunkFilePathRelative: string
-
-  /**
-   * Chunk name
-   *
-   * This will only take effect in webpack
-   */
-  chunkName: string
-
-  /**
-   * Rendered html file path
-   */
-  htmlFilePath: string
-
-  /**
-   * Rendered html file path relative to dest directory
-   */
-  htmlFilePathRelative: string
-}
+  ExtraPageFields extends Record<string, unknown> = Record<string, unknown>,
+> = ExtraPageFields &
+  PageBase<ExtraPageFrontmatter> & {
+    /**
+     * Data of the page, which will be available in client code
+     */
+    data: PageData<ExtraPageData, ExtraPageFrontmatter>
+
+    /**
+     * Raw Content of the page
+     */
+    content: string
+
+    /**
+     * Rendered content of the page
+     */
+    contentRendered: string
+
+    /**
+     * Date of the page, in 'yyyy-MM-dd' format
+     *
+     * @example '2020-09-09'
+     */
+    date: string
+
+    /**
+     * Dependencies of the page
+     */
+    deps: string[]
+
+    /**
+     * Links of the page
+     */
+    links: MarkdownLink[]
+
+    /**
+     * Markdown env object of the page
+     */
+    markdownEnv: Record<string, unknown>
+
+    /**
+     * Path of the page that inferred from file path
+     *
+     * If the page does not come from a file, it would be `null`
+     *
+     * @example '/guide/index.html'
+     */
+    pathInferred: string | null
+
+    /**
+     * Locale path prefix of the page
+     *
+     * @example '/getting-started.html' -> '/'
+     * @example '/en/getting-started.html' -> '/en/'
+     * @example '/zh/getting-started.html' -> '/zh/'
+     */
+    pathLocale: string
+
+    /**
+     * Permalink of the page
+     *
+     * If the page does not have a permalink, it would be `null`
+     */
+    permalink: string | null
+
+    /**
+     * Custom data to be attached to route record
+     */
+    routeMeta: Record<string, unknown>
+
+    /**
+     * Extracted sfc blocks of the page
+     */
+    sfcBlocks: MarkdownSfcBlocks
+
+    /**
+     * Slug of the page
+     */
+    slug: string
+
+    /**
+     * Source file path
+     *
+     * If the page does not come from a file, it would be `null`
+     */
+    filePath: string | null
+
+    /**
+     * Source file path relative to source directory
+     *
+     * If the page does not come from a file, it would be `null`
+     */
+    filePathRelative: string | null
+
+    /**
+     * Chunk file path
+     */
+    chunkFilePath: string
+
+    /**
+     * Chunk file path relative to temp directory
+     */
+    chunkFilePathRelative: string
+
+    /**
+     * Chunk name
+     *
+     * This will only take effect in webpack
+     */
+    chunkName: string
+
+    /**
+     * Rendered html file path
+     */
+    htmlFilePath: string
+
+    /**
+     * Rendered html file path relative to dest directory
+     */
+    htmlFilePathRelative: string
+  }
 
 /**
  * Options to create vuepress page

From d45c6781da0049a5c48eb277f2ac7eb73609ecbb Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 21:22:56 +0800
Subject: [PATCH 09/11] feat: inject default export

---
 packages/core/src/page/renderPageToVue.ts | 80 ++++++++++++++++-------
 1 file changed, 56 insertions(+), 24 deletions(-)

diff --git a/packages/core/src/page/renderPageToVue.ts b/packages/core/src/page/renderPageToVue.ts
index 14f6fb8311..d6a7c021a7 100644
--- a/packages/core/src/page/renderPageToVue.ts
+++ b/packages/core/src/page/renderPageToVue.ts
@@ -1,4 +1,5 @@
 import { isString } from '@vuepress/shared'
+import { path } from '@vuepress/utils'
 import type { App, Page } from '../types/index.js'
 
 const TEMPLATE_WRAPPER_TAG_OPEN = '<div>'
@@ -10,6 +11,13 @@ const SCRIPT_TAG_CLOSE = '</script>'
 const SCRIPT_TAG_OPEN_LANG_TS_REGEX = /<\s*script[^>]*\blang=['"]ts['"][^>]*/
 const SCRIPT_TAG_OPEN_LANG_TS = '<script lang="ts">'
 
+const SCRIPT_DEFAULT_EXPORT_REGEX = /((?:^|\n|;)\s*)export(\s*)default/
+const SCRIPT_DEFAULT_NAMED_EXPORT_REGEX =
+  /((?:^|\n|;)\s*)export(.+)as(\s*)default/
+
+const SCRIPT_DEFAULT_EXPORT_CODE_TEMPLATE_OUTLET = '__SCRIPT_DEFAULT_EXPORT__'
+const SCRIPT_DEFAULT_EXPORT_CODE_TEMPLATE = `export default { name: ${SCRIPT_DEFAULT_EXPORT_CODE_TEMPLATE_OUTLET} }`
+
 const PAGE_DATA_CODE_VAR_NAME = '_pageData'
 const PAGE_DATA_CODE_TEMPLATE_OUTLET = '__PAGE_DATA__'
 const PAGE_DATA_CODE_TEMPLATE = `export const ${PAGE_DATA_CODE_VAR_NAME} = JSON.parse(${PAGE_DATA_CODE_TEMPLATE_OUTLET})`
@@ -27,37 +35,65 @@ if (import.meta.hot) {
 }
 `
 
-/**
- * Util to resolve the page data code
- */
-const resolvePageDataCode = (data: Page['data']): string =>
-  PAGE_DATA_CODE_TEMPLATE.replace(
-    PAGE_DATA_CODE_TEMPLATE_OUTLET,
-    JSON.stringify(JSON.stringify(data)),
-  )
-
 /**
  * Util to resolve the open tag of script block
  */
-const resolveScriptTagOpen = (sfcBlocks: Page['sfcBlocks']): string => {
+const resolveScriptTagOpen = (page: Page): string => {
   // use existing script open tag
-  if (sfcBlocks.script?.tagOpen) {
-    return sfcBlocks.script.tagOpen
+  if (page.sfcBlocks.script?.tagOpen) {
+    return page.sfcBlocks.script.tagOpen
   }
   // if the setup script block is using typescript, we should use the same language for script block
-  const isUsingTs = sfcBlocks.scriptSetup?.tagOpen.match(
+  const isUsingTs = page.sfcBlocks.scriptSetup?.tagOpen.match(
     SCRIPT_TAG_OPEN_LANG_TS_REGEX,
   )
   return isUsingTs ? SCRIPT_TAG_OPEN_LANG_TS : SCRIPT_TAG_OPEN
 }
 
+/**
+ * Util to resolve the default export code
+ */
+const resolveDefaultExportCode = (page: Page): string =>
+  SCRIPT_DEFAULT_EXPORT_CODE_TEMPLATE.replace(
+    SCRIPT_DEFAULT_EXPORT_CODE_TEMPLATE_OUTLET,
+    JSON.stringify(path.basename(page.chunkFilePath)),
+  )
+
+/**
+ * Util to resolve the page data code
+ */
+const resolvePageDataCode = (page: Page): string =>
+  PAGE_DATA_CODE_TEMPLATE.replace(
+    PAGE_DATA_CODE_TEMPLATE_OUTLET,
+    JSON.stringify(JSON.stringify(page.data)),
+  )
+
+/**
+ * Resolve the stripped content of script block
+ */
+const resolveScriptContentStripped = (app: App, page: Page): string => {
+  const rawContentStripped = page.sfcBlocks.script?.contentStripped
+  const hasDefaultExport = rawContentStripped
+    ? SCRIPT_DEFAULT_EXPORT_REGEX.test(rawContentStripped) ||
+      SCRIPT_DEFAULT_NAMED_EXPORT_REGEX.test(rawContentStripped)
+    : false
+  return [
+    rawContentStripped,
+    resolvePageDataCode(page), // inject page data code
+    !hasDefaultExport && resolveDefaultExportCode(page), // inject default export with component name
+    app.env.isDev && HMR_CODE, // inject HMR code in dev mode
+  ]
+    .filter(isString)
+    .join('\n')
+}
+
 /**
  * Render page to vue component
  */
-export const renderPageToVue = (
-  app: App,
-  { data, sfcBlocks }: Page,
-): string => {
+export const renderPageToVue = (app: App, page: Page): string => {
+  const { sfcBlocks } = page
+
+  // get the content of template block
   // #688: wrap the content of `<template>` with a `<div>` to avoid some potential issues of fragment component
   const templateContent =
     sfcBlocks.template &&
@@ -69,14 +105,10 @@ export const renderPageToVue = (
       sfcBlocks.template.tagClose,
     ].join('')
 
-  // inject page data code and HMR code into the script content
-  const scriptTagOpen = resolveScriptTagOpen(sfcBlocks)
-  const pageDataCode = resolvePageDataCode(data)
+  // get the content of script block
   const scriptContent = [
-    scriptTagOpen,
-    sfcBlocks.script?.contentStripped,
-    pageDataCode,
-    app.env.isDev && HMR_CODE,
+    resolveScriptTagOpen(page),
+    resolveScriptContentStripped(app, page),
     sfcBlocks.script?.tagClose ?? SCRIPT_TAG_CLOSE,
   ]
     .filter(isString)

From e4211ba52a88be122d9601d35fcaea9dac4a8f72 Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Wed, 11 Sep 2024 21:30:38 +0800
Subject: [PATCH 10/11] chore: tweaks

---
 packages/cli/src/commands/dev/handlePageAdd.ts    | 6 ++----
 packages/cli/src/commands/dev/handlePageChange.ts | 4 +---
 2 files changed, 3 insertions(+), 7 deletions(-)

diff --git a/packages/cli/src/commands/dev/handlePageAdd.ts b/packages/cli/src/commands/dev/handlePageAdd.ts
index 4c8d7c2512..0d91181927 100644
--- a/packages/cli/src/commands/dev/handlePageAdd.ts
+++ b/packages/cli/src/commands/dev/handlePageAdd.ts
@@ -17,9 +17,7 @@ export const handlePageAdd = async (
   }
 
   // create page
-  const page = await createPage(app, {
-    filePath,
-  })
+  const page = await createPage(app, { filePath })
 
   // add the new page
   app.pages.push(page)
@@ -28,7 +26,7 @@ export const handlePageAdd = async (
   // prepare page file
   await preparePageChunk(app, page)
 
-  // prepare routes file
+  // re-prepare routes file
   await prepareRoutes(app)
 
   return page
diff --git a/packages/cli/src/commands/dev/handlePageChange.ts b/packages/cli/src/commands/dev/handlePageChange.ts
index c7e29debf6..84df9d5d4c 100644
--- a/packages/cli/src/commands/dev/handlePageChange.ts
+++ b/packages/cli/src/commands/dev/handlePageChange.ts
@@ -20,9 +20,7 @@ export const handlePageChange = async (
   const pageOld = app.pages[pageIndex]
 
   // create a new page from the changed file
-  const pageNew = await createPage(app, {
-    filePath,
-  })
+  const pageNew = await createPage(app, { filePath })
 
   // replace the old page with the new page
   app.pages.splice(pageIndex, 1, pageNew)

From a0da5333d46677a5319a980f16ebde6074a70213 Mon Sep 17 00:00:00 2001
From: meteorlxy <meteor.lxy@foxmail.com>
Date: Fri, 13 Sep 2024 01:53:31 +0800
Subject: [PATCH 11/11] refactor(core): improve comments

---
 e2e/docs/.vuepress/plugins/foo/fooPlugin.ts   |  3 +-
 packages/core/src/app/appInit.ts              |  2 +
 packages/core/src/app/appPrepare.ts           |  2 +
 packages/core/src/app/appUse.ts               |  7 ++
 packages/core/src/app/createBaseApp.ts        |  6 +-
 packages/core/src/app/createBuildApp.ts       |  2 +-
 packages/core/src/app/createDevApp.ts         |  2 +-
 .../src/app/prepare/prepareClientConfigs.ts   |  2 +
 .../core/src/app/prepare/prepareRoutes.ts     |  2 +
 .../core/src/app/prepare/prepareSiteData.ts   |  2 +
 packages/core/src/app/resolveAppDir.ts        |  2 +
 packages/core/src/app/resolveAppEnv.ts        |  2 +
 packages/core/src/app/resolveAppMarkdown.ts   |  2 +
 packages/core/src/app/resolveAppOptions.ts    |  2 +
 packages/core/src/app/resolveAppPages.ts      |  2 +
 packages/core/src/app/resolveAppSiteData.ts   |  2 +
 packages/core/src/app/resolveAppVersion.ts    |  2 +
 packages/core/src/app/resolveAppWriteTemp.ts  |  2 +
 packages/core/src/app/resolvePluginObject.ts  |  2 +
 packages/core/src/app/resolveThemeInfo.ts     |  2 +
 .../core/src/app/setupAppThemeAndPlugins.ts   |  2 +
 packages/core/src/page/createPage.ts          |  3 +
 packages/core/src/page/inferPagePath.ts       |  2 +
 packages/core/src/page/parsePageContent.ts    |  2 +
 .../core/src/page/renderPageSfcBlocksToVue.ts |  2 +
 .../core/src/page/resolvePageChunkInfo.ts     |  2 +
 .../core/src/page/resolvePageComponentInfo.ts |  2 +
 packages/core/src/page/resolvePageContent.ts  |  2 +
 packages/core/src/page/resolvePageDate.ts     |  2 +
 packages/core/src/page/resolvePageFilePath.ts |  2 +
 packages/core/src/page/resolvePageHtmlInfo.ts |  2 +
 packages/core/src/page/resolvePageLang.ts     |  2 +
 packages/core/src/page/resolvePagePath.ts     |  2 +
 .../core/src/page/resolvePagePermalink.ts     |  2 +
 .../core/src/page/resolvePageRouteMeta.ts     |  2 +
 packages/core/src/page/resolvePageSlug.ts     |  2 +
 .../core/src/pluginApi/createHookQueue.ts     |  2 +
 .../core/src/pluginApi/createPluginApi.ts     |  5 ++
 .../src/pluginApi/createPluginApiHooks.ts     |  5 ++
 .../pluginApi/createPluginApiRegisterHooks.ts |  5 ++
 .../src/pluginApi/normalizeAliasDefineHook.ts |  2 +
 .../normalizeClientConfigFileHook.ts          |  2 +
 packages/core/src/types/app/options.ts        | 90 ++++++++++++++++++-
 packages/core/src/types/app/utils.ts          | 23 +++++
 packages/core/src/types/bundler.ts            | 11 +++
 packages/core/src/types/plugin.ts             |  8 +-
 46 files changed, 227 insertions(+), 9 deletions(-)

diff --git a/e2e/docs/.vuepress/plugins/foo/fooPlugin.ts b/e2e/docs/.vuepress/plugins/foo/fooPlugin.ts
index 93ef0c8cff..a6031278ff 100644
--- a/e2e/docs/.vuepress/plugins/foo/fooPlugin.ts
+++ b/e2e/docs/.vuepress/plugins/foo/fooPlugin.ts
@@ -1,8 +1,9 @@
+import type { Plugin } from 'vuepress/core'
 import { getDirname, path } from 'vuepress/utils'
 
 const __dirname = getDirname(import.meta.url)
 
-export const fooPlugin = {
+export const fooPlugin: Plugin = {
   name: 'test-plugin',
   clientConfigFile: path.resolve(
     __dirname,
diff --git a/packages/core/src/app/appInit.ts b/packages/core/src/app/appInit.ts
index 1db287fd28..95ce110178 100644
--- a/packages/core/src/app/appInit.ts
+++ b/packages/core/src/app/appInit.ts
@@ -9,6 +9,8 @@ const log = debug('vuepress:core/app')
  * Initialize a vuepress app
  *
  * Plugins should be used before initialization.
+ *
+ * @internal
  */
 export const appInit = async (app: App): Promise<void> => {
   log('init start')
diff --git a/packages/core/src/app/appPrepare.ts b/packages/core/src/app/appPrepare.ts
index f8d2585f50..15dd6986b7 100644
--- a/packages/core/src/app/appPrepare.ts
+++ b/packages/core/src/app/appPrepare.ts
@@ -18,6 +18,8 @@ const log = debug('vuepress:core/app')
  * - routes
  * - site data
  * - other files that generated by plugins
+ *
+ * @internal
  */
 export const appPrepare = async (app: App): Promise<void> => {
   log('prepare start')
diff --git a/packages/core/src/app/appUse.ts b/packages/core/src/app/appUse.ts
index df53e43cab..9c98443a39 100644
--- a/packages/core/src/app/appUse.ts
+++ b/packages/core/src/app/appUse.ts
@@ -4,6 +4,13 @@ import { resolvePluginObject } from './resolvePluginObject.js'
 
 const log = debug('vuepress:core/app')
 
+/**
+ * Use a plugin in vuepress app.
+ *
+ * Should be called before initialization.
+ *
+ * @internal
+ */
 export const appUse = (app: App, rawPlugin: Plugin): App => {
   const pluginObject = resolvePluginObject(app, rawPlugin)
 
diff --git a/packages/core/src/app/createBaseApp.ts b/packages/core/src/app/createBaseApp.ts
index dc2b2ecaf4..dc8b18750d 100644
--- a/packages/core/src/app/createBaseApp.ts
+++ b/packages/core/src/app/createBaseApp.ts
@@ -17,7 +17,11 @@ import { resolveAppWriteTemp } from './resolveAppWriteTemp.js'
 import { setupAppThemeAndPlugins } from './setupAppThemeAndPlugins.js'
 
 /**
- * Create vuepress app
+ * Create base vuepress app.
+ *
+ * Notice that the base app could not be used for dev nor build.
+ *
+ * It would be used for creating dev app or build app, or for testing.
  */
 export const createBaseApp = (config: AppConfig): App => {
   const options = resolveAppOptions(config)
diff --git a/packages/core/src/app/createBuildApp.ts b/packages/core/src/app/createBuildApp.ts
index 2bc29eddff..13e1651b8e 100644
--- a/packages/core/src/app/createBuildApp.ts
+++ b/packages/core/src/app/createBuildApp.ts
@@ -2,7 +2,7 @@ import type { AppConfig, BuildApp } from '../types/index.js'
 import { createBaseApp } from './createBaseApp.js'
 
 /**
- * Create vuepress build app
+ * Create vuepress build app.
  */
 export const createBuildApp = (config: AppConfig): BuildApp => {
   const app = createBaseApp(config) as BuildApp
diff --git a/packages/core/src/app/createDevApp.ts b/packages/core/src/app/createDevApp.ts
index 3a0f4003e6..3d21ea35ed 100644
--- a/packages/core/src/app/createDevApp.ts
+++ b/packages/core/src/app/createDevApp.ts
@@ -2,7 +2,7 @@ import type { AppConfig, DevApp } from '../types/index.js'
 import { createBaseApp } from './createBaseApp.js'
 
 /**
- * Create vuepress dev app
+ * Create vuepress dev app.
  */
 export const createDevApp = (config: AppConfig): DevApp => {
   const app = createBaseApp(config) as DevApp
diff --git a/packages/core/src/app/prepare/prepareClientConfigs.ts b/packages/core/src/app/prepare/prepareClientConfigs.ts
index 59bb1f5624..4636bd5556 100644
--- a/packages/core/src/app/prepare/prepareClientConfigs.ts
+++ b/packages/core/src/app/prepare/prepareClientConfigs.ts
@@ -2,6 +2,8 @@ import type { App } from '../../types/index.js'
 
 /**
  * Generate client configs temp file
+ *
+ * @internal
  */
 export const prepareClientConfigs = async (app: App): Promise<void> => {
   // plugin hook: clientConfigFile
diff --git a/packages/core/src/app/prepare/prepareRoutes.ts b/packages/core/src/app/prepare/prepareRoutes.ts
index b3556e8041..a4f8d6f549 100644
--- a/packages/core/src/app/prepare/prepareRoutes.ts
+++ b/packages/core/src/app/prepare/prepareRoutes.ts
@@ -22,6 +22,8 @@ if (import.meta.hot) {
 
 /**
  * Resolve page redirects
+ *
+ * @internal
  */
 const resolvePageRedirects = ({ path, pathInferred }: Page): string[] => {
   // paths that should redirect to this page, use set to dedupe
diff --git a/packages/core/src/app/prepare/prepareSiteData.ts b/packages/core/src/app/prepare/prepareSiteData.ts
index c2749c0dc2..7121e72845 100644
--- a/packages/core/src/app/prepare/prepareSiteData.ts
+++ b/packages/core/src/app/prepare/prepareSiteData.ts
@@ -17,6 +17,8 @@ if (import.meta.hot) {
 
 /**
  * Generate site data temp file
+ *
+ * @internal
  */
 export const prepareSiteData = async (app: App): Promise<void> => {
   let content = `\
diff --git a/packages/core/src/app/resolveAppDir.ts b/packages/core/src/app/resolveAppDir.ts
index 5e78b9d344..18819dca9d 100644
--- a/packages/core/src/app/resolveAppDir.ts
+++ b/packages/core/src/app/resolveAppDir.ts
@@ -6,6 +6,8 @@ const require = createRequire(import.meta.url)
 
 /**
  * Create directory util function
+ *
+ * @internal
  */
 export const createAppDirFunction =
   (baseDir: string): AppDirFunction =>
diff --git a/packages/core/src/app/resolveAppEnv.ts b/packages/core/src/app/resolveAppEnv.ts
index 7ec4bf96c0..d0b069c6a4 100644
--- a/packages/core/src/app/resolveAppEnv.ts
+++ b/packages/core/src/app/resolveAppEnv.ts
@@ -2,6 +2,8 @@ import type { AppEnv, AppOptions } from '../types/index.js'
 
 /**
  * Resolve environment flags for vuepress app
+ *
+ * @internal
  */
 export const resolveAppEnv = (options: AppOptions): AppEnv => ({
   isBuild: false,
diff --git a/packages/core/src/app/resolveAppMarkdown.ts b/packages/core/src/app/resolveAppMarkdown.ts
index 84ce7d0a1a..d3b35b4ef2 100644
--- a/packages/core/src/app/resolveAppMarkdown.ts
+++ b/packages/core/src/app/resolveAppMarkdown.ts
@@ -4,6 +4,8 @@ import type { App } from '../types/index.js'
 
 /**
  * Resolve markdown-it instance for vuepress app
+ *
+ * @internal
  */
 export const resolveAppMarkdown = async (app: App): Promise<Markdown> => {
   // plugin hook: extendsMarkdownOptions
diff --git a/packages/core/src/app/resolveAppOptions.ts b/packages/core/src/app/resolveAppOptions.ts
index 63b9379962..756227ffe5 100644
--- a/packages/core/src/app/resolveAppOptions.ts
+++ b/packages/core/src/app/resolveAppOptions.ts
@@ -6,6 +6,8 @@ const require = createRequire(import.meta.url)
 
 /**
  * Create app options with default values
+ *
+ * @internal
  */
 export const resolveAppOptions = ({
   // site config
diff --git a/packages/core/src/app/resolveAppPages.ts b/packages/core/src/app/resolveAppPages.ts
index d8cd00bd41..51ed100308 100644
--- a/packages/core/src/app/resolveAppPages.ts
+++ b/packages/core/src/app/resolveAppPages.ts
@@ -6,6 +6,8 @@ const log = debug('vuepress:core/app')
 
 /**
  * Resolve pages for vuepress app
+ *
+ * @internal
  */
 export const resolveAppPages = async (app: App): Promise<Page[]> => {
   log('resolveAppPages start')
diff --git a/packages/core/src/app/resolveAppSiteData.ts b/packages/core/src/app/resolveAppSiteData.ts
index 9f2b654e3e..bc37720131 100644
--- a/packages/core/src/app/resolveAppSiteData.ts
+++ b/packages/core/src/app/resolveAppSiteData.ts
@@ -4,6 +4,8 @@ import type { AppOptions, SiteData } from '../types/index.js'
  * Resolve site data for vuepress app
  *
  * Site data will also be used in client
+ *
+ * @internal
  */
 export const resolveAppSiteData = (options: AppOptions): SiteData => ({
   base: options.base,
diff --git a/packages/core/src/app/resolveAppVersion.ts b/packages/core/src/app/resolveAppVersion.ts
index 590340ffc4..5f356432a3 100644
--- a/packages/core/src/app/resolveAppVersion.ts
+++ b/packages/core/src/app/resolveAppVersion.ts
@@ -5,6 +5,8 @@ const require = createRequire(import.meta.url)
 
 /**
  * Resolve version of vuepress app
+ *
+ * @internal
  */
 export const resolveAppVersion = (): string => {
   const pkgJson = fs.readJsonSync(
diff --git a/packages/core/src/app/resolveAppWriteTemp.ts b/packages/core/src/app/resolveAppWriteTemp.ts
index f4e3b6c7d5..2a3560f072 100644
--- a/packages/core/src/app/resolveAppWriteTemp.ts
+++ b/packages/core/src/app/resolveAppWriteTemp.ts
@@ -3,6 +3,8 @@ import type { AppDir, AppWriteTemp } from '../types/index.js'
 
 /**
  * Resolve write temp file util for vuepress app
+ *
+ * @internal
  */
 export const resolveAppWriteTemp = (dir: AppDir): AppWriteTemp => {
   const writeTemp: AppWriteTemp = async (file: string, content: string) => {
diff --git a/packages/core/src/app/resolvePluginObject.ts b/packages/core/src/app/resolvePluginObject.ts
index 8ccdd72aaa..4636f48ac0 100644
--- a/packages/core/src/app/resolvePluginObject.ts
+++ b/packages/core/src/app/resolvePluginObject.ts
@@ -3,6 +3,8 @@ import type { App, Plugin, PluginObject } from '../types/index.js'
 
 /**
  * Resolve a plugin object according to name / path / module and config
+ *
+ * @internal
  */
 export const resolvePluginObject = <T extends PluginObject = PluginObject>(
   app: App,
diff --git a/packages/core/src/app/resolveThemeInfo.ts b/packages/core/src/app/resolveThemeInfo.ts
index 34e9ff3b0d..b16c371429 100644
--- a/packages/core/src/app/resolveThemeInfo.ts
+++ b/packages/core/src/app/resolveThemeInfo.ts
@@ -3,6 +3,8 @@ import { resolvePluginObject } from './resolvePluginObject.js'
 
 /**
  * Resolve theme info and its parent theme info
+ *
+ * @internal
  */
 export const resolveThemeInfo = (app: App, theme: Theme): ThemeInfo => {
   // resolve current theme info
diff --git a/packages/core/src/app/setupAppThemeAndPlugins.ts b/packages/core/src/app/setupAppThemeAndPlugins.ts
index f28c9f04e7..39aceda92b 100644
--- a/packages/core/src/app/setupAppThemeAndPlugins.ts
+++ b/packages/core/src/app/setupAppThemeAndPlugins.ts
@@ -3,6 +3,8 @@ import { resolveThemeInfo } from './resolveThemeInfo.js'
 
 /**
  * Setup theme and plugins for vuepress app
+ *
+ * @internal
  */
 export const setupAppThemeAndPlugins = (app: App, config: AppConfig): void => {
   // recursively resolve theme info
diff --git a/packages/core/src/page/createPage.ts b/packages/core/src/page/createPage.ts
index 600d60d63d..2e9946dec2 100644
--- a/packages/core/src/page/createPage.ts
+++ b/packages/core/src/page/createPage.ts
@@ -13,6 +13,9 @@ import { resolvePagePermalink } from './resolvePagePermalink.js'
 import { resolvePageRouteMeta } from './resolvePageRouteMeta.js'
 import { resolvePageSlug } from './resolvePageSlug.js'
 
+/**
+ * Create vuepress page object
+ */
 export const createPage = async (
   app: App,
   options: PageOptions,
diff --git a/packages/core/src/page/inferPagePath.ts b/packages/core/src/page/inferPagePath.ts
index 8d082ea93c..2a406c7b66 100644
--- a/packages/core/src/page/inferPagePath.ts
+++ b/packages/core/src/page/inferPagePath.ts
@@ -7,6 +7,8 @@ import type { App } from '../types/index.js'
 
 /**
  * Infer page path according to file path
+ *
+ * @internal
  */
 export const inferPagePath = ({
   app,
diff --git a/packages/core/src/page/parsePageContent.ts b/packages/core/src/page/parsePageContent.ts
index 8af9d92b96..03e036a87e 100644
--- a/packages/core/src/page/parsePageContent.ts
+++ b/packages/core/src/page/parsePageContent.ts
@@ -9,6 +9,8 @@ import type { App, PageFrontmatter, PageOptions } from '../types/index.js'
 
 /**
  * Render page content and extract related info
+ *
+ * @internal
  */
 export const parsePageContent = ({
   app,
diff --git a/packages/core/src/page/renderPageSfcBlocksToVue.ts b/packages/core/src/page/renderPageSfcBlocksToVue.ts
index 7428643823..79f15e92ca 100644
--- a/packages/core/src/page/renderPageSfcBlocksToVue.ts
+++ b/packages/core/src/page/renderPageSfcBlocksToVue.ts
@@ -2,6 +2,8 @@ import type { MarkdownSfcBlocks } from '@vuepress/markdown'
 
 /**
  * Render page sfc blocks to vue component
+ *
+ * @internal
  */
 export const renderPageSfcBlocksToVue = (
   sfcBlocks: MarkdownSfcBlocks,
diff --git a/packages/core/src/page/resolvePageChunkInfo.ts b/packages/core/src/page/resolvePageChunkInfo.ts
index 037018c6cc..0d5c6b994f 100644
--- a/packages/core/src/page/resolvePageChunkInfo.ts
+++ b/packages/core/src/page/resolvePageChunkInfo.ts
@@ -3,6 +3,8 @@ import type { App } from '../types/index.js'
 
 /**
  * Resolve page data file path
+ *
+ * @internal
  */
 export const resolvePageChunkInfo = ({
   app,
diff --git a/packages/core/src/page/resolvePageComponentInfo.ts b/packages/core/src/page/resolvePageComponentInfo.ts
index b1ee312cc4..1e5d4d6156 100644
--- a/packages/core/src/page/resolvePageComponentInfo.ts
+++ b/packages/core/src/page/resolvePageComponentInfo.ts
@@ -3,6 +3,8 @@ import type { App } from '../types/index.js'
 
 /**
  * Resolve page component and related info
+ *
+ * @internal
  */
 export const resolvePageComponentInfo = ({
   app,
diff --git a/packages/core/src/page/resolvePageContent.ts b/packages/core/src/page/resolvePageContent.ts
index c4bda0732b..51a1aa3ab2 100644
--- a/packages/core/src/page/resolvePageContent.ts
+++ b/packages/core/src/page/resolvePageContent.ts
@@ -9,6 +9,8 @@ const FALLBACK_CONTENT = ''
 
 /**
  * Resolve page content according to `content` or `filePath`
+ *
+ * @internal
  */
 export const resolvePageContent = async ({
   filePath,
diff --git a/packages/core/src/page/resolvePageDate.ts b/packages/core/src/page/resolvePageDate.ts
index d371062f68..f9b2d5312c 100644
--- a/packages/core/src/page/resolvePageDate.ts
+++ b/packages/core/src/page/resolvePageDate.ts
@@ -10,6 +10,8 @@ const DEFAULT_DATE = '0000-00-00'
  * Resolve page date according to frontmatter or file path
  *
  * It will be resolved as 'yyyy-MM-dd' format
+ *
+ * @internal
  */
 export const resolvePageDate = ({
   frontmatter,
diff --git a/packages/core/src/page/resolvePageFilePath.ts b/packages/core/src/page/resolvePageFilePath.ts
index ee5a1b01ed..32d34e23a5 100644
--- a/packages/core/src/page/resolvePageFilePath.ts
+++ b/packages/core/src/page/resolvePageFilePath.ts
@@ -3,6 +3,8 @@ import type { App, PageOptions } from '../types/index.js'
 
 /**
  * Resolve absolute and relative path of page file
+ *
+ * @internal
  */
 export const resolvePageFilePath = ({
   app,
diff --git a/packages/core/src/page/resolvePageHtmlInfo.ts b/packages/core/src/page/resolvePageHtmlInfo.ts
index 677921ed04..0417e4edbe 100644
--- a/packages/core/src/page/resolvePageHtmlInfo.ts
+++ b/packages/core/src/page/resolvePageHtmlInfo.ts
@@ -3,6 +3,8 @@ import type { App } from '../types/index.js'
 
 /**
  * Resolve page rendered html file path
+ *
+ * @internal
  */
 export const resolvePageHtmlInfo = ({
   app,
diff --git a/packages/core/src/page/resolvePageLang.ts b/packages/core/src/page/resolvePageLang.ts
index 3f2f243256..d0774e871c 100644
--- a/packages/core/src/page/resolvePageLang.ts
+++ b/packages/core/src/page/resolvePageLang.ts
@@ -3,6 +3,8 @@ import type { App, PageFrontmatter } from '../types/index.js'
 
 /**
  * Resolve language of page
+ *
+ * @internal
  */
 export const resolvePageLang = ({
   app,
diff --git a/packages/core/src/page/resolvePagePath.ts b/packages/core/src/page/resolvePagePath.ts
index c40e366455..0edac439de 100644
--- a/packages/core/src/page/resolvePagePath.ts
+++ b/packages/core/src/page/resolvePagePath.ts
@@ -3,6 +3,8 @@ import type { PageOptions } from '../types/index.js'
 
 /**
  * Resolve the final route path of a page
+ *
+ * @internal
  */
 export const resolvePagePath = ({
   permalink,
diff --git a/packages/core/src/page/resolvePagePermalink.ts b/packages/core/src/page/resolvePagePermalink.ts
index 15a421dec1..4b62c069f7 100644
--- a/packages/core/src/page/resolvePagePermalink.ts
+++ b/packages/core/src/page/resolvePagePermalink.ts
@@ -4,6 +4,8 @@ import type { App, PageFrontmatter } from '../types/index.js'
 
 /**
  * Resolve page permalink from frontmatter / options / pattern
+ *
+ * @internal
  */
 export const resolvePagePermalink = ({
   app,
diff --git a/packages/core/src/page/resolvePageRouteMeta.ts b/packages/core/src/page/resolvePageRouteMeta.ts
index 76e5030a91..fa66e39d36 100644
--- a/packages/core/src/page/resolvePageRouteMeta.ts
+++ b/packages/core/src/page/resolvePageRouteMeta.ts
@@ -2,6 +2,8 @@ import type { PageFrontmatter } from '../types/index.js'
 
 /**
  * Resolve page route meta
+ *
+ * @internal
  */
 export const resolvePageRouteMeta = ({
   frontmatter,
diff --git a/packages/core/src/page/resolvePageSlug.ts b/packages/core/src/page/resolvePageSlug.ts
index a7329ffbaa..f9c3704571 100644
--- a/packages/core/src/page/resolvePageSlug.ts
+++ b/packages/core/src/page/resolvePageSlug.ts
@@ -4,6 +4,8 @@ const DATE_RE = /(\d{4}-\d{1,2}(-\d{1,2})?)-(.*)/
 
 /**
  * Resolve page slug from filename
+ *
+ * @internal
  */
 export const resolvePageSlug = ({
   filePathRelative,
diff --git a/packages/core/src/pluginApi/createHookQueue.ts b/packages/core/src/pluginApi/createHookQueue.ts
index 09331eee3e..9fb70cc9ad 100644
--- a/packages/core/src/pluginApi/createHookQueue.ts
+++ b/packages/core/src/pluginApi/createHookQueue.ts
@@ -10,6 +10,8 @@ const log = debug('vuepress:core/plugin-api')
 
 /**
  * Create hook queue for plugin system
+ *
+ * @internal
  */
 export const createHookQueue = <T extends HooksName>(name: T): HookQueue<T> => {
   const items: HookItem<T>[] = []
diff --git a/packages/core/src/pluginApi/createPluginApi.ts b/packages/core/src/pluginApi/createPluginApi.ts
index 4f90cda435..18aa270ed7 100644
--- a/packages/core/src/pluginApi/createPluginApi.ts
+++ b/packages/core/src/pluginApi/createPluginApi.ts
@@ -2,6 +2,11 @@ import type { PluginApi } from '../types/index.js'
 import { createPluginApiHooks } from './createPluginApiHooks.js'
 import { createPluginApiRegisterHooks } from './createPluginApiRegisterHooks.js'
 
+/**
+ * Create vuepress plugin api
+ *
+ * @internal
+ */
 export const createPluginApi = (): PluginApi => {
   const plugins: PluginApi['plugins'] = []
   const hooks = createPluginApiHooks()
diff --git a/packages/core/src/pluginApi/createPluginApiHooks.ts b/packages/core/src/pluginApi/createPluginApiHooks.ts
index 0cefc33067..d8369985bb 100644
--- a/packages/core/src/pluginApi/createPluginApiHooks.ts
+++ b/packages/core/src/pluginApi/createPluginApiHooks.ts
@@ -1,6 +1,11 @@
 import type { PluginApi } from '../types/index.js'
 import { createHookQueue } from './createHookQueue.js'
 
+/**
+ * Create hooks for plugin api
+ *
+ * @internal
+ */
 export const createPluginApiHooks = (): PluginApi['hooks'] => ({
   // life cycle hooks
   onInitialized: createHookQueue('onInitialized'),
diff --git a/packages/core/src/pluginApi/createPluginApiRegisterHooks.ts b/packages/core/src/pluginApi/createPluginApiRegisterHooks.ts
index 79ee344016..22c94c4a01 100644
--- a/packages/core/src/pluginApi/createPluginApiRegisterHooks.ts
+++ b/packages/core/src/pluginApi/createPluginApiRegisterHooks.ts
@@ -2,6 +2,11 @@ import type { PluginApi } from '../types/index.js'
 import { normalizeAliasDefineHook } from './normalizeAliasDefineHook.js'
 import { normalizeClientConfigFileHook } from './normalizeClientConfigFileHook.js'
 
+/**
+ * Create registerHooks method for plugin api
+ *
+ * @internal
+ */
 export const createPluginApiRegisterHooks =
   (
     plugins: PluginApi['plugins'],
diff --git a/packages/core/src/pluginApi/normalizeAliasDefineHook.ts b/packages/core/src/pluginApi/normalizeAliasDefineHook.ts
index 6c3da1a21b..1b12ac361b 100644
--- a/packages/core/src/pluginApi/normalizeAliasDefineHook.ts
+++ b/packages/core/src/pluginApi/normalizeAliasDefineHook.ts
@@ -3,6 +3,8 @@ import type { AliasDefineHook } from '../types/index.js'
 
 /**
  * Normalize alias and define hook
+ *
+ * @internal
  */
 export const normalizeAliasDefineHook =
   (hook: AliasDefineHook['exposed']): AliasDefineHook['normalized'] =>
diff --git a/packages/core/src/pluginApi/normalizeClientConfigFileHook.ts b/packages/core/src/pluginApi/normalizeClientConfigFileHook.ts
index bba349702c..a6475852e4 100644
--- a/packages/core/src/pluginApi/normalizeClientConfigFileHook.ts
+++ b/packages/core/src/pluginApi/normalizeClientConfigFileHook.ts
@@ -4,6 +4,8 @@ import type { ClientConfigFileHook } from '../types/index.js'
 
 /**
  * Normalize hook for client config file
+ *
+ * @internal
  */
 export const normalizeClientConfigFileHook =
   (hook: ClientConfigFileHook['exposed']): ClientConfigFileHook['normalized'] =>
diff --git a/packages/core/src/types/app/options.ts b/packages/core/src/types/app/options.ts
index 4cbcabbc37..74a11478f6 100644
--- a/packages/core/src/types/app/options.ts
+++ b/packages/core/src/types/app/options.ts
@@ -9,18 +9,98 @@ import type { Theme } from '../theme.js'
  * Vuepress app common config that shared between dev and build
  */
 export interface AppConfigCommon extends Partial<SiteData> {
+  /**
+   * Source directory of the markdown files.
+   *
+   * Vuepress will load markdown files from this directory.
+   *
+   * @required
+   */
   source: string
+
+  /**
+   * Destination directory of the output files.
+   *
+   * Vuepress will output the static site files to this directory.
+   *
+   * @default `${source}/.vuepress/dist`
+   */
   dest?: string
+
+  /**
+   * Temp files directory.
+   *
+   * Vuepress will write temp files to this directory.
+   *
+   * @default `${source}/.vuepress/.temp`
+   */
   temp?: string
+
+  /**
+   * Cache files directory.
+   *
+   * Vuepress will write cache files to this directory.
+   *
+   * @default `${source}/.vuepress/.cache`
+   */
   cache?: string
+
+  /**
+   * Public files directory.
+   *
+   * Vuepress will copy the files from public directory to the output directory.
+   *
+   * @default `${source}/.vuepress/public`
+   */
   public?: string
 
+  /**
+   * Whether to enable debug mode
+   *
+   * @default false
+   */
   debug?: boolean
+
+  /**
+   * Markdown options
+   *
+   * @default {}
+   */
   markdown?: MarkdownOptions
+
+  /**
+   * Patterns to match the markdown files as pages
+   *
+   * @default ['**\/*.md', '!.vuepress', '!node_modules']
+   */
   pagePatterns?: string[]
+
+  /**
+   * Pattern to generate permalink for pages
+   *
+   * @default null
+   */
   permalinkPattern?: string | null
+
+  /**
+   * Vuepress bundler
+   *
+   * @required
+   */
   bundler: Bundler
+
+  /**
+   * Vuepress theme
+   *
+   * @required
+   */
   theme: Theme
+
+  /**
+   * Vuepress plugins
+   *
+   * @default []
+   */
   plugins?: PluginConfig
 }
 
@@ -87,17 +167,21 @@ export interface AppConfigBuild {
   /**
    * Specify the HTML template renderer to be used for build
    *
-   * @default templateRenderer from '@vuepress/utils'
+   * @default `import { templateRenderer } from '@vuepress/utils'`
    */
   templateBuildRenderer?: TemplateRenderer
 }
 
 /**
- * Vuepress app config
+ * Vuepress app user config.
+ *
+ * It would be provided by user, typically via a config file.
  */
 export type AppConfig = AppConfigBuild & AppConfigCommon & AppConfigDev
 
 /**
- * Vuepress app options
+ * Vuepress app options that resolved from user config.
+ *
+ * It fills all optional fields with a default value.
  */
 export type AppOptions = Required<AppConfig>
diff --git a/packages/core/src/types/app/utils.ts b/packages/core/src/types/app/utils.ts
index 0e70eb5508..fdfb3fd4a7 100644
--- a/packages/core/src/types/app/utils.ts
+++ b/packages/core/src/types/app/utils.ts
@@ -7,11 +7,34 @@ export type AppDirFunction = (...args: string[]) => string
  * Directory utils
  */
 export interface AppDir {
+  /**
+   * Resolve file path in cache directory
+   */
   cache: AppDirFunction
+
+  /**
+   * Resolve file path in temp directory
+   */
   temp: AppDirFunction
+
+  /**
+   * Resolve file path in source directory
+   */
   source: AppDirFunction
+
+  /**
+   * Resolve file path in dest directory
+   */
   dest: AppDirFunction
+
+  /**
+   * Resolve file path in public directory
+   */
   public: AppDirFunction
+
+  /**
+   * Resolve file path in client directory
+   */
   client: AppDirFunction
 }
 
diff --git a/packages/core/src/types/bundler.ts b/packages/core/src/types/bundler.ts
index cd3b8dc2a7..0f8e64fa78 100644
--- a/packages/core/src/types/bundler.ts
+++ b/packages/core/src/types/bundler.ts
@@ -8,8 +8,19 @@ import type { App } from './app/index.js'
  * - build: bundle assets for deployment
  */
 export interface Bundler {
+  /**
+   * Name of the bundler
+   */
   name: string
+
+  /**
+   * Method to run vuepress app in dev mode, starting dev server
+   */
   dev: (app: App) => Promise<() => Promise<void>>
+
+  /**
+   * Method to run vuepress app in build mode, generating static pages and assets
+   */
   build: (app: App) => Promise<void>
 }
 
diff --git a/packages/core/src/types/plugin.ts b/packages/core/src/types/plugin.ts
index ae52a6b334..99e65f7517 100644
--- a/packages/core/src/types/plugin.ts
+++ b/packages/core/src/types/plugin.ts
@@ -27,10 +27,14 @@ export type PluginFunction<T extends PluginObject = PluginObject> = (
  * Vuepress plugin object
  */
 export interface PluginObject extends Partial<HooksExposed> {
-  // plugin name
+  /**
+   * Name of the plugin
+   */
   name: string
 
-  // allow use a plugin multiple times or not
+  /**
+   * Allow the plugin to be used multiple times or not
+   */
   multiple?: boolean
 }