npm package: @star_work/vite-plugin-virtual-mpa
Description: Enhanced fork of vite-plugin-virtual-mpa with file scan mode, virtual module CORS fix, and improved documentation.
Author: 伽蓝
Latest version: 2.0.3

# @star_work/vite-plugin-virtual-mpa ⚡

[![npm version](https://img.shields.io/npm/v/@star_work/vite-plugin-virtual-mpa)](https://npmjs.com/package/@star_work/vite-plugin-virtual-mpa)
[![awesome-vite](https://awesome.re/badge.svg)](https://github.com/vitejs/awesome-vite)
![license](https://img.shields.io/npm/l/@star_work/vite-plugin-virtual-mpa)
[![install size](https://packagephobia.com/badge?p=@star_work/vite-plugin-virtual-mpa)](https://packagephobia.com/result?p=@star_work/vite-plugin-virtual-mpa)

> **增强版 fork**，基于 [vite-plugin-virtual-mpa](https://github.com/emosheeep/vite-plugin-virtual-mpa) 开发，包含额外功能和改进。

开箱即用的 Vite MPA插件 📦，支持HTML模板引擎和虚拟文件功能，能够使用一份模板生成多个文件。

**原项目：** [vite-plugin-virtual-mpa](https://github.com/emosheeep/vite-plugin-virtual-mpa) by [QinXuYang](https://github.com/emosheeep)

[English](./README.md) | 中文

## ✨ 增强功能（相比原版）

本 fork 版本包含以下增强：

- 🆕 **文件扫描模式**：自动从目录中的 Vue/React 文件生成页面
  - 扫描子目录中的 `.vue`、`.tsx`、`.jsx` 文件
  - 每个文件自动成为独立的 HTML 页面
  - 只扫描直接子目录（可配置深度）
- 🎯 **页面配置扫描功能**：自动提取页面中的 `definePage` 配置
  - 扫描页面目录，提取 `definePage({ ... })` 配置
  - 生成 `virtual:page-configs` 虚拟模块供运行时使用
  - 自动注入 `definePage` 函数，避免运行时错误
  - 支持热更新，文件变化时自动重新扫描
- 🐛 **修复虚拟模块跨域问题**：自动使用 `/@id/` 前缀处理虚拟模块
  - 不再出现加载虚拟入口文件时的 CORS 错误
  - 浏览器兼容的虚拟模块路径
- 🔧 **增强的入口模板**：文件扫描模式下支持 `componentPath` 变量
- 📝 **完善的文档**：完整的使用示例和指南

## 主要功能

- 💡 EJS 模板渲染（支持 HTML 和 Entry 模板）
- 💡 完备的 TypeScript 类型提示支持，是一款小而美的插件
- 🛠️ 自定义模板HTML文件的输出路径，使用一份模板生成多份文件
- 🛠️ **文件扫描模式**：从组件文件自动生成页面
- 🛠️ **虚拟入口模板**：使用一个 entry 模板文件生成多个虚拟入口文件，减少重复代码
- 🎯 **页面配置扫描**：自动提取 `definePage` 配置，生成虚拟模块
- 🛠️ 支持 MPA 多页面应用，为开发和预览服务器提供 History Fallback 支持
- 🐛 修复虚拟模块跨域问题
## 安装

```sh
pnpm add -D @star_work/vite-plugin-virtual-mpa
# 或
npm install -D @star_work/vite-plugin-virtual-mpa
```

## 使用方式

```ts
// vite.config.ts
import { createMpaPlugin, createPages } from '@star_work/vite-plugin-virtual-mpa'

// @see https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    createMpaPlugin({
      pages: [
        // 你可以直接在这里书写页面配置，也可以单独使用 `createPages` 函数并将结果传递到这里。
      ]
    }),
  ],
})

/**
 * 该函数仅仅是将参数转换为一个 pages 数组。
 * 它帮助你在插件之外创建页面配置，主要是为了能够拥有类型提示。
 * 同时在别处统一管理配置的方式可能也能帮助你简化 vite 的配置文件。
 */
const pages = createPages([
  // 你可以传递一个 page 对象或一个 pages 数组。
])

// @see https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    createMpaPlugin({
      pages,
    }),
  ],
})
```

## Options

```ts
type FilterPattern = string | RegExp | (string | RegExp)[]
type RewriteRule = false | Rewrite[]
interface WatchHandler {
  (ctx: {
    server: ViteDevServer,
    file: string,
    type: Event
    /**
     * 可以调用这个方法更新页面配置
     * @params pages MPA 页面核心配置，这将会替换默认的 `pages`
     */
    reloadPages: (pages: Page[]) => void
  }): void
}

interface MpaOptions {
  /**
   * 是否在控制台打印log
   * @default true
   */
  verbose?: boolean,
  /**
   * 默认模板文件
   * @default index.html
   */
  template?: `${string}.html`,
  /**
   * 默认入口模板文件，用于生成虚拟入口文件。
   * 与 `template` 类似，允许使用一个模板文件生成多个入口文件。
   * 如果页面配置中指定了 `entryTemplate`，将优先使用页面级别的配置。
   * @see 关于虚拟入口文件章节
   */
  entryTemplate?: string,
  /**
   * 为开发服务器配置 fallback rewrite rules，只会处理 accept=text/html 的文件请求。
   * @see https://github.com/bripkens/connect-history-api-fallback
   */
  rewrites?: RewriteRule,
  /**
   * 为预览服务器配置重定向规则，配置方式同 rewrites。
   * @see https://github.com/bripkens/connect-history-api-fallback
   */
  previewRewrites?: RewriteRule,
  /**
   * 处理模版 HTML 文件，继承自 `transformIndexHtml`。
   * @see https://vitejs.dev/guide/api-plugin#transformindexhtml
   */
  transformHtml?: (
    html: string,
    ctx: IndexHtmlTransformContext & { page: Page },
  ) => IndexHtmlTransformResult;
  /**
   * 用于扫描相似的目录结构，自动生成 pages 配置。
   * 扫描到的配置会追加到 `pages` 中，具有相同 name 的 page 将被忽略
   */
  scanOptions?: {
    /**
     * 要扫描的目录，子目录名称将作为唯一的 page name。
     */
    scanDirs: string | string[];
    /**
     * 扫描模式：'directory' 扫描子目录（默认），'file' 扫描匹配 filePattern 的文件。
     * 当 scanMode 为 'file' 时，只扫描 scanDirs 的直接子目录下的文件。
     * 更深层的文件和 scanDirs 根目录下的文件将被忽略。
     * @default 'directory'
     */
    scanMode?: 'directory' | 'file';
    /**
     * 当 scanMode 为 'file' 时匹配的文件模式（如 '*.vue', '*.tsx'）。
     * 支持 glob 模式。只有匹配此模式的文件才会被扫描。
     * 文件只在 scanDirs 的直接子目录中扫描，不会扫描更深层。
     * @default '*.vue'
     */
    filePattern?: string;
    /**
     * 相对于扫描目录的入口文件路径
     * 当 scanMode 为 'file' 时，此选项将被忽略，应使用 entryTemplate 代替。
     */
    entryFile?: string;
    /**
     * 文件扫描模式的入口模板文件。
     * 当 scanMode 为 'file' 时，此模板将用于生成虚拟入口文件。
     * 模板可以使用 EJS 语法，支持变量：componentPath（文件路径）、pageName 和 data 字段。
     */
    entryTemplate?: string;
    /**
     * 自定义虚拟文件的名称（输出文件名）
     * @param name 子目录名称（目录模式）或相对于 scanDirs 的文件路径（文件模式）
     * @param filePath 完整文件路径（仅文件模式）
     */
    filename?: (name: string, filePath?: string) => string;
    /**
     * 为每个扫描到的页面注入的数据。
     * 可以是函数，接收文件/目录信息并返回数据对象。
     */
    data?: Record<string, any> | ((info: { name: string; path: string; dir: string }) => Record<string, any>);
  };
  /**
   * 当项目目录下有一些文件触发相应的事件如添加、删除、修改时，你可能想要重新加载 `pages` 配置 或 重启 ViteDevServer。
   * 你可以通过设置 `watchOptions` 来实现这一目的。
   */
  watchOptions?: WatchHandler | {
    /**
     * 指定需要**包含**的文件，基于 `Rollup.createFilter` 过滤
     * @see https://vitejs.dev/guide/api-plugin.html#filtering-include-exclude-pattern
     */
    include?: Exclude<FilterPattern, null>,
    /**
     * 指定需要**排除**的文件，基于 `Rollup.createFilter` 过滤
     * @see https://vitejs.dev/guide/api-plugin.html#filtering-include-exclude-pattern
     */
    excluded?: Exclude<FilterPattern, null>,
    /**
     * 想要监听的文件事件
     * @default ['add', 'unlink', 'change', 'unlinkDir', 'addDir']
     */
    events?: Event[],
    /**
     * 定义的文件事件触发后，执行自定义逻辑
     */
    handler: WatchHandler
  },
  pages: Array<{
    /**
     * 必填。该名称是一个不包含'/'的普通字符串，它用于生成默认的重定向规则。
     * 如果你想自定义生成文件的路径，请使用filename选项，而不是name选项。
     */
    name: string;
    /**
     * 相对于`build.outDir`的路径，应该以html结尾
     * @default `${name}.html`
     */
    filename?: `${string}.html`;
    /**
     * 更高优先级的模板文件，将会覆盖默认模板
     */
    template?: string;
    /**
     * 自动注入入口文件，如果设置了entry，需要移除模板文件中的entry。
     * 入口文件路径必须以 `'/'` 开头（相对于项目根目录），
     * 或者使用 Vite 虚拟模块格式（以 `'virtual:'` 或 `'\0'` 开头）。
     * 如果同时设置了 `entryTemplate`，此选项将被忽略。
     */
    entry?: string;
    /**
     * 入口模板文件，用于生成虚拟入口文件。
     * 与 `template` 类似，允许使用一个模板文件生成多个入口文件。
     * 模板文件支持 EJS 语法，可以注入页面数据（`data`）和页面名称（`pageName`）。
     * 如果页面配置中未指定，将使用全局的 `entryTemplate` 配置。
     *
     * **注意**：不能同时使用 `entry` 和 `entryTemplate`。
     *
     * @example
     * ```js
     * // entry-template.js
     * import App from '/src/pages/<%= pageName %>/App.vue';
     * console.log('Page:', '<%= pageName %>');
     * createApp(App).mount('#root');
     * ```
     */
    entryTemplate?: string;
    /**
     * 注入到模板文件的数据
     */
    data?: Record<string, any>,
  }>,
  /**
   * 是否使用 html-minify-terser 压缩 html 文件
   * @default false
   * @see https://github.com/terser/html-minifier-terser
   */
  htmlMinify?: Options | boolean,
  /**
   * 是否启用页面配置扫描功能。
   * 如果为 true，将自动扫描页面目录并生成 virtual:page-configs 虚拟模块。
   * 如果为对象，可以自定义配置选项。
   * @default false
   */
  pageConfigs?:
    | boolean
    | {
        /**
         * 要扫描的页面目录
         * @default 'src/pages'
         */
        pagesDir?: string | string[];
        /**
         * 文件匹配模式
         * @default '*.vue'
         */
        filePattern?: string;
        /**
         * 虚拟模块 ID
         * @default 'virtual:page-configs'
         */
        virtualModuleId?: string;
      };
}
```
## Examples

> **注意**：CodeSandbox 示例链接到原项目仓库。要体验增强功能（文件扫描模式），请查看下面的示例部分。

点击链接 [codesandbox](https://codesandbox.io/p/sandbox/vite-plugin-virtual-mpa-0djylc) 快速体验原项目功能！

### 基础示例（使用 entry）

```ts
// vite.config.ts
import { normalizePath } from "vite";
import { createMpaPlugin } from "vite-plugin-virtual-mpa"

const base = "/sites/"

// @see https://vitejs.dev/config/
export default defineConfig({
  base,
  plugins: [
    createMpaPlugin({
      htmlMinify: false,
      pages: [
        {
          name: "apple",
          /**
           * 文件名是可选的，默认将会是`${name}.html`，这个路径是相对于`build.outDir`
           */
          filename: "fruits/apple.html", // 将会在编译时输出到sites/fruits/apple.html
          entry: "/src/fruits/apple/index.js",
          data: {
            title: "This is Apple page"
          }
        },
        {
          name: "banana",
          filename: "fruits/banana.html",
          entry: "/src/fruits/banana/index.js",
          data: {
            title: "This is Banana page"
          }
        },
        {
          name: "strawberries",
          filename: "fruits/strawberries.html",
          entry: "/src/fruits/strawberries/index.js",
          data: {
            title: "This is Strawberries page"
          }
        }
      ],
      /**
       * 以下示例的 scanOptions 配置可以替换上面的 pages 配置，除了 data 的注入。
       */
      scanOptions: {
        scanDirs: 'src/fruits',
        entryFile: 'index.js',
        filename: name => `fruits/${name}.html`,
        template: '../../template.html',
      },
      /**
       * 使用入口模板生成虚拟入口文件（可选）
       * 这样可以避免为每个页面创建重复的入口文件
       */
      // entryTemplate: 'src/entry-template.js',
      /**
       * 通过该选项来配置 history fallback rewrite rules
       * 如果你像上面这样配置页面的话，那下面的这份配置将会自动生成。
       * 否则你需要自己编写重定向规则。
       */
      rewrites: [
        {
          from: new RegExp(normalizePath(`/${base}/(apple|banana|strawberries)`)),
          to: (ctx) => normalizePath(`/fruits/${ctx.match[1]}.html`),
        }
      ],
      /**
       * 配置预览服务器的重定向规则，配置方式同 rewrites
       */
      previewRewrites: [
        // 如果产物目录没有 index.html，你需要手动配置规则，以便服务器能正确找到入口文件。
        { from: /.*/, to: '/home.html' },
      ],
      /** 自定义处理模板内容 */
      transformHtml(html, ctx) {
        return {
          html,
          tags: [
            {
              tag: 'div',
              injectTo: 'body-prepend',
              children: `[Auto Injected] Page name: ${ctx.page.name}`,
            },
          ],
        };
      },
    }),
  ],
})
```

### 文件扫描模式示例

当你希望为 `pages` 目录下的每个 Vue 文件生成独立的 HTML 页面时，可以使用文件扫描模式：

**1. 创建入口模板文件**

```js
// src/entry-template-vue.js
import { createApp } from 'vue';
// 根据 componentPath 动态导入 Vue 组件
import Component from '<%= componentPath %>';

// 页面特定的数据可以在这里使用
console.log('组件路径:', '<%= componentPath %>');
console.log('页面标题:', '<%= title %>');

// 挂载 Vue 应用
createApp(Component).mount('#root');
```

**2. 配置插件**

```ts
// vite.config.ts
import { createMpaPlugin } from "vite-plugin-virtual-mpa"

export default defineConfig({
  plugins: [
    createMpaPlugin({
      template: 'src/template.html',
      scanOptions: {
        scanDirs: 'src/pages',
        scanMode: 'file',        // 扫描文件而不是目录
        filePattern: '*.vue',    // 匹配 .vue 文件
        entryTemplate: 'src/entry-template-vue.js', // 用于生成入口文件的模板
        filename: (name) => `${name}.html`, // 输出：apple/About.html, apple/App.html 等
        data: ({ name, dir }) => ({
          title: `${dir} - ${name.split('/').pop()}`,
        }),
      },
    }),
  ],
})
```

**扫描规则：**
- 只扫描 `scanDirs` 的**直接子目录**下的文件
- 忽略更深层嵌套目录中的文件
- 忽略 `scanDirs` 根目录下的文件
- 每个匹配的文件都会成为一个独立的 HTML 页面

**目录结构示例：**
```
src/pages/
  ├── apple/
  │   ├── About.vue     ✅ 扫描 → apple/About.html
  │   ├── App.vue       ✅ 扫描 → apple/App.html
  │   └── deep/
  │       └── Deep.vue  ❌ 忽略（更深层）
  ├── page2/
  │   └── App.vue       ✅ 扫描 → page2/App.html
  └── Root.vue          ❌ 忽略（不在子目录下）
```

**entryTemplate 中可用的变量：**
- `<%= componentPath %>` - Vue 组件文件的完整路径（自动注入）
- `<%= pageName %>` - 页面名称（如 "apple/About"）
- `<%= title %>` - 来自 `data.title`
- `<%= customVar %>` - `data` 配置中的任何自定义变量
- `<%= VITE_XXX %>` - 以 `VITE_` 开头的环境变量

### 使用入口模板（entryTemplate）示例

使用 `entryTemplate` 可以避免为每个页面创建重复的入口文件：

**1. 创建入口模板文件**

```js
// src/entry-template.js
import { createApp } from 'vue';
// 使用 EJS 语法动态导入对应页面的组件
import App from '/src/pages/<%= pageName %>/App.vue';

// 页面数据会自动注入
console.log('🚀 Page name:', '<%= pageName %>');
console.log('📄 Page title:', '<%= title %>');

// 如果有自定义数据，也可以使用
// console.log('Custom data:', '<%= customData %>');

createApp(App).mount('#root');
```

**2. 在配置中使用**

```ts
// vite.config.ts
import { createMpaPlugin } from "vite-plugin-virtual-mpa"

export default defineConfig({
  plugins: [
    createMpaPlugin({
      template: 'src/template.html',
      // 全局入口模板（所有页面共享）
      entryTemplate: 'src/entry-template.js',
      pages: [
        {
          name: "apple",
          filename: "fruits/apple.html",
          // 使用全局 entryTemplate，无需单独指定
          data: {
            title: "This is Apple page",
            customData: "Apple specific data"
          }
        },
        {
          name: "banana",
          filename: "fruits/banana.html",
          // 也可以为特定页面指定不同的入口模板
          // entryTemplate: 'src/banana-entry-template.js',
          data: {
            title: "This is Banana page"
          }
        },
        {
          name: "strawberries",
          filename: "fruits/strawberries.html",
          data: {
            title: "This is Strawberries page"
          }
        }
      ],
      // ... 其他配置
    }),
  ],
})
```

**优势对比：**

- **传统方式**：需要为每个页面创建 `index.js` 文件，代码重复
- **使用 entryTemplate**：只需一个模板文件，通过 EJS 动态生成，减少重复代码

**注意事项：**

- Entry 模板中的 import 路径建议使用绝对路径（以 `/src/` 开头）
- 不能同时使用 `entry` 和 `entryTemplate`
- 页面级别的 `entryTemplate` 会覆盖全局配置

## 本 Fork 版本的新特性

这是基于原版 [vite-plugin-virtual-mpa](https://github.com/emosheeep/vite-plugin-virtual-mpa) 的增强版本，包含以下改进：

### 🆕 文件扫描模式（v1.13.0+）

自动扫描并生成页面，无需手动配置每个页面：

- **自动发现**：扫描子目录中的 `.vue`、`.tsx`、`.jsx` 文件
- **智能过滤**：只扫描直接子目录（可配置）
- **基于模板**：使用入口模板生成虚拟入口文件
- **灵活配置**：支持自定义文件模式和数据注入

### 🐛 虚拟模块跨域修复（v1.13.1+）

修复了浏览器加载虚拟入口模块时的 CORS 问题：

- **自动路径转换**：使用 `/@id/` 前缀确保浏览器兼容性
- **无需手动操作**：插件自动处理所有路径转换
- **开箱即用**：无需额外配置

### 🎯 页面配置扫描功能（v2.0.0+）

自动扫描并提取页面中的 `definePage` 配置，生成虚拟模块供运行时使用：

- **自动扫描**：递归扫描页面目录，提取 `definePage({ ... })` 配置
- **虚拟模块**：生成 `virtual:page-configs` 虚拟模块，提供配置数据
- **运行时支持**：自动注入 `definePage` 函数，避免运行时错误
- **热更新**：文件变化时自动重新扫描并更新配置
- **类型安全**：完整的 TypeScript 类型支持

### 📚 完善的文档

包含所有功能的完整示例和使用指南，包括文件扫描模式的使用方法。

## 关于原项目

本插件基于 [vite-plugin-virtual-mpa](https://github.com/emosheeep/vite-plugin-virtual-mpa)（作者：[QinXuYang](https://github.com/emosheeep)）开发。原项目旨在解决使用 Vite 构建多页面应用（MPA）时的以下需求：

1. 支持模板引擎（如 EJS），能够使用一个模板生成多份文件，且能自定义构建时生成文件的路径
2. 自动配置 `rollupOptions.input`，并提供能力配置开发服务器的代理（主要是 history fallback API）

关于原项目的设计理念和与其他插件的对比，请参考[原项目仓库](https://github.com/emosheeep/vite-plugin-virtual-mpa)。

## 默认重定向规则

正如上面提到的👆🏻，如果你的配置遵循约定，插件将会自动生成一份重定向规则，这份配置会同时应用到开发和预览服务器，如下：
```ts
{
  from: new RegExp(normalizePath(`/${base}/(${Object.keys(inputMap).join('|')})`)),
  to: ctx => normalizePath(`/${base}/${inputMap[ctx.match[1]]}`),
}
```

其中, **inputMap** 是一个`name`到对应虚拟文件的映射，结构如下:

```ts
{
  apple: 'fruits/apple.html',
  banana: 'fruits/banana.html',
  strawberries: 'fruits/strawberries.html',
}
```

请求Url`/sites/apple/xxx`将会被**默认重定向规则**处理并重定向到对应的url，也就是`/fruits/apple.html`(name `'apple'` 对应 `'fruits/apple.html'`, 其他同理)，重定向后的路径将会基于`viteConfig.base(这里是'/sites/')`去寻找目标文件，所以最终的Url会变成`/sites/fruits/apple.html`.

## 关于虚拟入口文件

通常在开发时，我们的文件都是写在本地的，我们通过DevServer的代理能够通过url访问到本地对应的文件。虚拟文件也是如此，只不过对应的文件没有写到文件系统中，而是保存在内存中而已。

该插件通过模板系统生成了对应的虚拟文件，让你可以在开发时**通过代理访问到内存中的虚拟文件**，并在构建时生成到对应的目录下。

你完全可以认为这些虚拟文件是真实存在的，这将有助于你在脑海中构建关于虚拟文件的直觉，以便能够正确地编写代理配置。

#### 虚拟模块路径格式

从 v1.13.1 开始，插件自动使用 `/@id/` 前缀来生成虚拟入口模块的路径，确保浏览器可以正确加载虚拟模块，避免 CORS 错误。

**生成的 HTML 格式：**
```html
<!-- ✅ 正确的格式（v1.13.1+） -->
<script type="module" src="/@id/virtual:entry:pageName"></script>
```

**为什么需要 `/@id/` 前缀？**

- 浏览器无法直接识别 `virtual:entry:xxx` 这种裸模块标识符
- Vite 开发服务器通过 `/@id/` 前缀来识别和处理虚拟模块
- 这样可以确保虚拟模块在浏览器中正确加载，避免 CORS 错误

插件会自动处理路径转换，你无需手动添加 `/@id/` 前缀。

### 虚拟入口模板（entryTemplate）

从 v1.12.2 开始，插件支持**虚拟入口模板**功能，允许你像使用 HTML 模板一样，使用一个 entry 模板文件生成多个虚拟入口文件。

#### 使用场景

当你需要为多个页面生成相似的入口文件时，可以使用 `entryTemplate` 来避免重复代码：

- **传统方式**：每个页面都需要一个独立的 `index.js` 文件
- **使用 entryTemplate**：只需要一个模板文件，通过 EJS 语法动态生成每个页面的入口文件

#### 基本用法

**1. 创建入口模板文件**

```js
// src/entry-template.js
import { createApp } from 'vue';
// 使用 EJS 语法注入页面名称
import App from '/src/pages/<%= pageName %>/App.vue';

// 页面数据会自动注入
console.log('Page name:', '<%= pageName %>');
console.log('Page title:', '<%= title %>');

createApp(App).mount('#root');
```

**2. 在配置中使用**

```ts
// vite.config.ts
export default defineConfig({
  plugins: [
    createMpaPlugin({
      template: 'src/template.html',
      // 全局入口模板（可选）
      entryTemplate: 'src/entry-template.js',
      pages: [
        {
          name: 'apple',
          entryTemplate: 'src/entry-template.js', // 页面级别配置（优先级更高）
          data: {
            title: 'This is Apple page',
            // 自定义数据也会注入到模板中
          }
        },
        {
          name: 'banana',
          // 使用全局 entryTemplate
          data: {
            title: 'This is Banana page',
          }
        }
      ]
    })
  ]
})
```

#### 可用变量

在 `entryTemplate` 中，你可以使用以下变量：

- `<%= pageName %>` - 页面名称（自动注入）
- `<%= componentPath %>` - 组件文件路径（仅在文件扫描模式下可用，自动注入）
- `<%= title %>` - 来自 `data.title` 的数据
- `<%= customVar %>` - 来自 `data` 配置的任何自定义变量
- `<%= VITE_XXX %>` - 以 `VITE_` 开头的环境变量（自动注入）

#### 注意事项

1. **路径问题**：在 entry 模板中使用 import 时，建议使用绝对路径（以 `/src/` 开头），因为虚拟入口文件没有实际的文件系统路径。

2. **不能同时使用**：不能同时设置 `entry` 和 `entryTemplate`，否则会报错。

3. **优先级**：页面级别的 `entryTemplate` 配置会覆盖全局配置。

4. **热更新**：修改 entry 模板文件会自动触发页面重载。

#### 对比示例

**传统方式（使用 entry）：**
```ts
pages: [
  {
    name: 'apple',
    entry: '/src/pages/apple/index.js', // 需要为每个页面创建文件
  },
  {
    name: 'banana',
    entry: '/src/pages/banana/index.js', // 重复的代码结构
  }
]
```

**使用 entryTemplate：**
```ts
pages: [
  {
    name: 'apple',
    entryTemplate: 'src/entry-template.js', // 共享模板
  },
  {
    name: 'banana',
    entryTemplate: 'src/entry-template.js', // 共享模板
  }
]
```

使用 `entryTemplate` 可以大大减少重复代码，特别是在页面结构相似的情况下。

### 页面配置扫描示例

页面配置扫描功能允许你在 Vue 文件中使用 `definePage` 定义页面配置，然后通过虚拟模块在运行时访问这些配置。

**1. 在 Vue 文件中定义页面配置**

```vue
<!-- src/pages/apple/About.vue -->
<script setup>
// 使用 definePage 定义页面配置
definePage({
  name: 'About',
  title: '关于我们',
  platform: ['web', 'mobile'],
  locale: 'zh-CN',
  meta: {
    description: '这是关于页面',
  },
});
</script>

<template>
  <div>This is Apple About</div>
</template>
```

**2. 在 vite.config.ts 中启用页面配置扫描**

```ts
// vite.config.ts
import { createMpaPlugin } from '@star_work/vite-plugin-virtual-mpa'

export default defineConfig({
  plugins: [
    createMpaPlugin({
      template: 'src/template.html',
      scanOptions: {
        scanDirs: 'src/pages',
        scanMode: 'file',
        filePattern: '*.vue',
        entryTemplate: 'src/entry-template-vue.js',
      },
      // 启用页面配置扫描
      pageConfigs: true, // 或 { pagesDir: 'src/pages', filePattern: '*.vue' }
    }),
  ],
})
```

**3. 在代码中使用页面配置**

```js
// 导入页面配置
import { pageConfigs, getPageConfig } from 'virtual:page-configs';

// 获取所有页面配置
console.log('All page configs:', pageConfigs);
// 输出: [{ path: 'apple/About', name: 'About', title: '关于我们', ... }, ...]

// 根据路径获取特定页面配置
const aboutConfig = getPageConfig('apple/About');
console.log('About page config:', aboutConfig);
// 输出: { path: 'apple/About', name: 'About', title: '关于我们', ... }

// 业务逻辑：根据平台筛选配置（自己实现）
function getPageConfigsByPlatform(platform) {
  return pageConfigs.filter((config) => {
    if (!config.platform) return false;
    if (typeof config.platform === 'string') {
      return config.platform === platform;
    }
    return config.platform.includes(platform);
  });
}

const webConfigs = getPageConfigsByPlatform('web');
console.log('Web platform configs:', webConfigs);
```

**虚拟模块提供的功能：**

- `pageConfigs` - 所有页面配置数组（原始数据）
- `getPageConfig(path)` - 根据路径获取配置，如 `getPageConfig('apple/About')`

**注意事项：**

- `definePage` 函数会在编译时自动注入，无需手动导入
- 如果页面没有 `definePage`，插件会创建默认配置（使用页面路径作为名称）
- 业务逻辑的筛选函数（如按平台、语言筛选）需要自己实现
- 文件变化时会自动重新扫描配置并更新虚拟模块

## 关于 EJS 模板引擎

插件使用 ejs 模板引擎进行数据注入，支持两种模板类型：

### HTML 模板（template）

用于生成 HTML 文件，除页面配置中提供的 `data` 外，插件默认会将以 `'VITE_'` 开头的环境变量注入所有的页面配置中。

### 入口模板（entryTemplate）

用于生成虚拟入口文件（如 `index.js`），支持以下变量：

- `<%= pageName %>` - 页面名称（自动注入，无需在 `data` 中配置）
- `<%= title %>` - 来自 `data.title` 的数据
- `<%= customVar %>` - 来自 `data` 配置的任何自定义变量
- `<%= VITE_XXX %>` - 以 `VITE_` 开头的环境变量（自动注入）

**示例：**

```js
// entry-template.js
import App from '/src/pages/<%= pageName %>/App.vue';
console.log('Page:', '<%= pageName %>');
console.log('Title:', '<%= title %>');
console.log('Custom:', '<%= customData %>');
```

更多关于环境变量的信息可以查看官网 —— [envprefix](https://cn.vitejs.dev/config/shared-options.html#envprefix)。