@uni-helper/vite-plugin-uni-pages 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +550 -54
  2. package/dist/index.d.mts +621 -282
  3. package/dist/index.mjs +2690 -468
  4. package/package.json +13 -8
package/README.md CHANGED
@@ -7,11 +7,13 @@
7
7
  </p>
8
8
  <p style="text-align: center">
9
9
  <a href="https://github.com/kejunmao"><img src="https://img.shields.io/badge/Author-KeJun-blue?style=for-the-badge" alt="Author"></a>
10
- <a href="https://github.com/ModyQyW"><img src="https://img.shields.io/badge/Maintainer-ModyQyW-blue?style=for-the-badge" alt="Author"></a>
10
+ <a href="https://github.com/ModyQyW"><img src="https://img.shields.io/badge/Maintainer-ModyQyW-blue?style=for-the-badge" alt="Maintainer"></a>
11
11
  </p>
12
12
 
13
13
  使用 TypeScript 编写 `uni-app` 的 `pages.json`。支持约定式路由。
14
14
 
15
+ 完整文档见 [uni-helper.js.org](https://uni-helper.js.org/vite-plugin-uni-pages),另有 [Netlify](https://vite-plugin-uni-pages.netlify.app) 和 [Cloudflare Pages](https://vite-plugin-uni-pages.pages.dev) 镜像。
16
+
15
17
  不想看文档?直接问 AI 🤖 <a href="https://deepwiki.com/uni-helper/vite-plugin-uni-pages"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
16
18
 
17
19
  > **请考虑持续[赞助](https://github.com/ModyQyW/sponsors)以维持该项目的持续健康发展,非常感谢!🙏**
@@ -41,7 +43,7 @@ export default defineConfig({
41
43
  })
42
44
  ```
43
45
 
44
- 创建 `pages.config.(ts|mts|cts|js|cjs|mjs|json)`,然后用 TypeScript 编写你的 `pages.json`。你可以在创建的文件里使用 `#ifdef H5` 等条件编译语句。[👉 pages.config.ts 示例](../../playground/pages.config.ts)
46
+ 创建 `pages.config.(ts|mts|cts|js|cjs|mjs|json)`,然后用 TypeScript 编写你的 `pages.json`。配置文件在构建期执行,需要按平台区分时读取 `process.env.UNI_PLATFORM`。不要依赖写在配置对象上的 `#ifdef` 注释,条件编译只会在 uni-app 处理 `pages.json` 时生效。[👉 pages.config.ts 示例](../../playground/pages.config.ts)
45
47
 
46
48
  ```ts
47
49
  // pages.config.ts
@@ -62,6 +64,166 @@ export default defineUniPages({
62
64
  })
63
65
  ```
64
66
 
67
+ ### 提前生成 pages.json
68
+
69
+ 部分插件在工厂函数执行时读取 `pages.json`。这些场景可以在创建下游插件前等待 `prepare()`,确保它们拿到完整配置:
70
+
71
+ ```ts
72
+ // vite.config.mts
73
+ import Uni from '@uni-helper/plugin-uni'
74
+ import UniPages from '@uni-helper/vite-plugin-uni-pages'
75
+ import Optimization from '@uni-ku/bundle-optimizer'
76
+ import { defineConfig } from 'vite'
77
+
78
+ export default defineConfig(async () => ({
79
+ plugins: [
80
+ await UniPages().prepare({ platformSuffix: false }),
81
+ Uni(),
82
+ Optimization(),
83
+ ],
84
+ }))
85
+ ```
86
+
87
+ 数组按顺序求值,`Optimization()` 会在准备完成后才执行。也可以分步调用:
88
+
89
+ ```ts
90
+ const pages = UniPages(options)
91
+ await pages.prepare({ platformSuffix: false })
92
+ ```
93
+
94
+ `prepare()` 等待用户配置加载、页面扫描、合并、`pages.json` 和已启用的声明文件写入,成功后返回同一个插件实例。Vite 初始化会复用这份上下文。
95
+
96
+ | 参数 | 说明 |
97
+ | --- | --- |
98
+ | `root?: string` | 提前指定 Vite 项目根目录,默认使用 `VITE_ROOT_DIR` 或当前工作目录;相对路径以当前工作目录为基准 |
99
+ | `platformSuffix: boolean` | 必填,声明是否使用 UniPlatform 文件名后缀规则;启用 `@uni-helper/vite-plugin-uni-platform` 时传 `true`,其余场景传 `false` |
100
+
101
+ 通过 uni-app 的开发或构建命令调用该配置。编译平台读取调用时的 `process.env.UNI_PLATFORM`;独立脚本需要提前设置它。准备阶段 Vite 尚未自动加载 `.env`,配置若依赖其中的变量,应先用 Vite `loadEnv()` 加载并提供所需环境。自定义 Vite `root` 时,向 `prepare()` 传入相同的根目录。
102
+
103
+ - 同一次准备期间,相同根目录、编译平台和后缀规则的并发或重复调用只初始化一次,首次 Vite 接管复用该结果。Vite 会话结束后再次调用会重新读取配置和页面。
104
+ - Vite 接管时会核对根目录、编译平台和 UniPlatform 状态,不一致会报错;接管后再次调用也必须保持一致。成功调用过 `prepare()` 后,这个核对一直有效。
105
+ - 配置加载、文件写入或锁获取失败会拒绝 Promise,修复后可再次调用 `prepare()`。
106
+ - 准备前应完成源码与配置的创建;准备完成到文件监听建立之间的修改不保证被自动发现。
107
+
108
+ 普通同步用法保持不变:可推导的输出目录已存在时,工厂会为缺失的 `pages.json` 创建占位文件,再在 Vite 初始化时完整生成。
109
+
110
+ ### 实例复用与关闭
111
+
112
+ 同一实例可以顺序复用。普通 `build()` 完成、开发服务器 `server.close()` 完成或构建监听器 `watcher.close()` 完成后,下一次使用会创建新的页面上下文,重新读取配置和页面,生成 `pages.json`、虚拟模块及已启用的声明文件。例如,第一次构建结束后新增 `detail.vue`,第二次构建会包含新页面:
113
+
114
+ ```ts
115
+ import { writeFile } from 'node:fs/promises'
116
+ import UniPages from '@uni-helper/vite-plugin-uni-pages'
117
+ import { build } from 'vite'
118
+
119
+ const pages = UniPages()
120
+ const config = { plugins: [pages] }
121
+ await build(config)
122
+ await writeFile('src/pages/detail.vue', '<template><view>详情</view></template>')
123
+ await build(config)
124
+ ```
125
+
126
+ 未成功调用过 `prepare()` 的实例在会话结束后也可以换 `root`、`UNI_PLATFORM` 或后缀规则。成功调用过 `prepare()` 后,参数约束持续有效,包括先启动 Vite、后调用 `prepare()` 的情况。
127
+
128
+ 从 `configResolved` 开始初始化到会话关闭完成,实例只接受一个 Vite 会话。活跃的开发服务器、尚未完成的构建、整个 `build --watch` 期间再次用同一实例启动 Vite 会报错;嵌套构建或并行任务应分别调用 `UniPages()` 创建实例。`build --watch` 每轮构建结束时仍保留当前会话和监听,直到整个 watcher 关闭。
129
+
130
+ 关闭时立即停止接受监听注册和新更新,取消尚未开始的更新,等待已开始的生成与资源释放完成。释放失败只记录错误,不覆盖构建结果或初始化错误。本插件初始化失败后可以修复输入并重新使用实例。已关闭 watcher 的旧构建若仍有钩子继续执行,其虚拟模块读取会拒绝,清理不会影响新会话。
131
+
132
+ 通过 Vite JavaScript API 使用构建监听时,应在首轮构建已启动后关闭 watcher,例如等待首个 `END` 事件。Rollup 在首个构建图创建前立即关闭时可能不调用 `closeWatcher`;插件收不到释放通知,因此这种提前关闭不保证释放资源,也不能作为同实例顺序复用的信号。
133
+
134
+ Vite 在创建 Rollup 构建图或开发服务器之前就退出时(例如其他插件的 `configResolved` / `options` 抛错),或只调用了 `resolveConfig()` 而没有启动构建、服务器,本插件同样收不到关闭通知,实例会保持占用。这类情况应创建新的 `UniPages()` 实例;不要把该实例再交给下一次构建。程序化调用 `server.restart()` 会先创建新服务器,复用同一实例也会按重叠会话拒绝;请让配置文件重新求值或换用新实例。
135
+
136
+ 占位文件只在能推导出的输出目录已经存在时创建。目录不存在时不在那个位置新建目录:`root` 优先取 `VITE_ROOT_DIR`,没设时取当前工作目录,而这个工作目录很可能不是真正的项目根,在它下面创建 `src` 会把目录建错地方。这时直接跑完整生成;如果 Vite 传入的 `root` 又是错的,生成会以 `ENOENT` 失败并指明路径。
137
+
138
+ `prepare()` 不会刷新下游插件已经缓存的配置,所以需要读取完整配置的插件应在等待完成后创建。
139
+
140
+ ### 配置依赖热更新
141
+
142
+ 默认的 `pages.config.ts` 或通过 `configSource` 指定的配置入口可以导入本地模块。使用内置 parser 时,开发服务器和 `vite build --watch` 会监听入口及打包器收集到的直接、间接依赖;修改这些文件后自动重新生成 `pages.json` 和已启用的类型声明。
143
+
144
+ ```ts
145
+ // pages.config.ts
146
+ import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
147
+ import { globalStyle } from './config/global-style'
148
+
149
+ export default defineUniPages({ globalStyle })
150
+ ```
151
+
152
+ ```ts
153
+ // config/global-style.ts
154
+ export const globalStyle = {
155
+ navigationBarTitleText: '首页',
156
+ }
157
+ ```
158
+
159
+ 支持本地 TypeScript、JavaScript 和 JSON,以及静态 `import`、`export ... from`、字面量 `import()` / `require()` 形成的依赖链。新增导入后会更新监听集合,移除导入后该文件不再触发配置生成。纯类型导入不参与运行时依赖监听。
160
+
161
+ 通过包名导入 workspace 包时,真实路径位于 `node_modules` 之外的源码也会参与加载、监听和重新求值,包括 Vite 根目录外的文件。已安装包的 TypeScript/JSON 入口及其相对依赖可以加载,但真实路径位于 `node_modules` 内的文件不参与自动监听;普通 JavaScript 包按 `import`/`require` 的条件导出解析。
162
+
163
+ 配置或依赖暂时出现语法错误、缺失文件时,watch 模式保留上次成功的产物并报告错误;修复后继续更新。已加载的配置入口删除后会使用空配置,重建入口后恢复加载。首次加载失败会终止初始化。
164
+
165
+ #### 源位置与运行时加载
166
+
167
+ 配置辅助模块可以在运行时加载其他文件。例如,配置入口导入 `config/global-style.ts`,该模块再通过变量加载同目录的标题文件:
168
+
169
+ ```text
170
+ app/
171
+ ├── pages.config.ts # 插件读取的配置入口
172
+ └── config/
173
+ ├── global-style.ts # 入口导入的辅助模块
174
+ └── title.mjs # 辅助模块在运行时加载的文件
175
+ ```
176
+
177
+ ```ts
178
+ // pages.config.ts
179
+ import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
180
+ import { globalStyle } from './config/global-style'
181
+
182
+ export default defineUniPages({ globalStyle })
183
+ ```
184
+
185
+ ```ts
186
+ // config/global-style.ts
187
+ const target = './title.mjs'
188
+ const { title } = await import(target)
189
+
190
+ export const globalStyle = { navigationBarTitleText: title }
191
+ ```
192
+
193
+ ```js
194
+ // config/title.mjs
195
+ export const title = '首页'
196
+ ```
197
+
198
+ 加载关系是 `pages.config.ts → config/global-style.ts → config/title.mjs`。其中入口到辅助模块是静态导入,最后一步的 `import(target)` 在运行时解析。相对路径以发起调用的 `config/global-style.ts` 为基准,所以读取的是 `app/config/title.mjs`。入口或其他嵌套模块执行同样的调用时,分别使用各自的源文件位置。
199
+
200
+ 导入的写法决定依赖如何加载和更新:
201
+
202
+ | 写法 | 加载与更新行为 |
203
+ | --- | --- |
204
+ | `import { title } from './title.mjs'`、`import('./title.mjs')`、`require('./title.cjs')` | 路径明确,由打包器收集;符合上述本地依赖范围的文件会被监听,并随配置重新打包、求值 |
205
+ | esbuild 可枚举的相对路径表达式,例如 ``import(`./titles/${locale}.mjs`)`` | 由打包器展开候选文件,收集到的本地文件参与监听;一个候选文件都不存在时打包失败、配置不生效,创建文件后需要修改配置入口或重启才能恢复 |
206
+ | 未被打包器展开的 `import(target)`、`require(target)` | 以调用模块的源位置交给 Node 加载,沿用 Node 缓存;动态目标不自动加入监听集合 |
207
+
208
+ 同一张表也写在 [docs/advanced/config-dependencies.md](https://github.com/uni-helper/vite-plugin-uni-pages/blob/main/docs/advanced/config-dependencies.md),两处一起维护。
209
+
210
+ 上例中的 `title.mjs` 属于最后一种情况:仅修改它不会自动触发配置更新,重新加载入口也可能继续读到缓存值;仅补建或修复缺失的动态目标也不保证触发恢复。若标题文件需要随编辑自动更新,可以把 `global-style.ts` 中的加载改为 `import { title } from './title.mjs'`。
211
+
212
+ 对配置入口及打包进配置的脚本模块,`__dirname`、`__filename`、`import.meta.url`、`import.meta.dirname`、`import.meta.filename` 同样对应各自源文件。`require.resolve()` 和 `import.meta.resolve()` 从调用模块的位置解析目标,只返回路径或 URL,不加载目标模块。运行时由 Node 加载的目标不经过这层转换,遵循目标模块自身的 ESM 或 CommonJS 语义。
213
+
214
+ 运行时解析区分包的 `import` / `require` 条件导出,支持模块所属包的 `#imports`。Node 自定义 loader、`--conditions` 等额外命令行解析选项不在此解析约定内。`fs.readFile()` 等自行读取、`node_modules` 变更和应用的 `resolve.alias` 不在自动依赖监听范围内。
215
+
216
+ #### transform 与自定义 parser
217
+
218
+ 需要在解析前转换入口文本时,可以设置 `configSource.transform`;需要自行读取和解析入口时,可以提供自定义 `parser`。对实际加载的入口,已提供的回调按 `transform → parser → rewrite` 的顺序执行,成功加载时各调用一次;前一步失败会终止后续回调。回调接收原配置入口路径。
219
+
220
+ | parser | 读取内容 | 依赖跟踪 |
221
+ | --- | --- | --- |
222
+ | 内置 `auto`、`json`、`import` | 使用内存中的 transform 结果;未返回有效字符串时使用原内容 | 脚本打包收集到的本地输入参与监听 |
223
+ | 自定义函数 | 收到原入口,读取该文件时得到原内容;transform 会执行,但其返回文本不传给自定义 parser | parser 自行读取的其他文件不自动加入依赖集合 |
224
+
225
+ transform 的文本转换在内存中完成,加载器不会把结果写回配置源文件或相邻文件。脚本执行需要的临时文件放在 `node_modules/.cache/uni-pages/` 的独占子目录里,每次加载结束都会尝试清理。清理失败不影响这次加载的结果,也不会覆盖原始错误;残留文件不会被当作配置输入,可以安全删除。这个目录专供加载器使用,不要放项目配置。内部文件的来源、用途和清理过程见[脚本配置的加载流程](#脚本配置的加载流程)。
226
+
65
227
  ### 页面级配置 definePage
66
228
 
67
229
  在页面文件的 `<script setup>` 中使用 `definePage` 宏声明页面元数据:
@@ -152,7 +314,7 @@ definePage({
152
314
  </script>
153
315
  ```
154
316
 
155
- > 导入路径要写全扩展名(`./title.mjs`、`./title.ts` 都可以)。TS 里习惯的省略扩展名写法(`./title`)在这里不行——宏代码在构建期用 Node 的 `require` 执行,它不做 TS 的扩展名补全。
317
+ > 导入路径要写全扩展名(`./title.mjs`、`./title.ts` 都可以)。TS 里习惯的省略扩展名写法(`./title`)在这里不行:宏代码在构建期用 Node 的 `require` 执行,它不做 TS 的扩展名补全。
156
318
 
157
319
  ### 平台条件页面配置
158
320
 
@@ -211,7 +373,8 @@ interface UserOptions {
211
373
 
212
374
  /**
213
375
  * 页面配置文件的加载源
214
- * 基于 unconfig,支持多配置源合并
376
+ *
377
+ * @description 基于 unconfig,按来源顺序加载首个有效配置
215
378
  * @default 'pages.config'
216
379
  * @since 0.2.7
217
380
  */
@@ -246,12 +409,12 @@ interface UserOptions {
246
409
  /**
247
410
  * 分包页面目录的根目录列表
248
411
  * 用于 uni-app 的分包加载功能
249
- * 支持字符串格式(目录路径)或对象格式(自定义 pages.json 中的 root)
412
+ * 字符串和对象的 dir 均支持 glob,root 可通过同步回调计算
250
413
  * 更多上下文参考 <https://github.com/uni-helper/vite-plugin-uni-pages/issues/271>
251
414
  * @default []
252
415
  * @since 0.1.8
253
416
  */
254
- subPackages?: (string | { dir: string, root: string })[]
417
+ subPackages?: (string | { dir: string, root: string | ((dir: string) => string) })[]
255
418
 
256
419
  /**
257
420
  * pages.json 所在目录
@@ -262,9 +425,15 @@ interface UserOptions {
262
425
  outDir?: string
263
426
 
264
427
  /**
265
- * 排除的文件/目录模式
266
- * 基于 tinyglobby 的 ignore 选项
267
- * @default ['node_modules', '.git', '**\/__*__/**']
428
+ * 页面排除的文件或目录模式
429
+ *
430
+ * @description
431
+ * - 相对模式基于 Vite root,绝对模式直接匹配对应路径;命中目录时排除整个子树
432
+ * - 主包、分包的扫描与页面事件使用相同规则
433
+ * - 自定义数组替换默认值,空数组禁用默认页面排除,空字符串忽略
434
+ * - 不接受 `!foo` 否定规则,`!(keep).vue` 等 extglob 有效
435
+ *
436
+ * @default ['**\/node_modules/**', '**\/.git/**', '**\/__*__/**']
268
437
  * @since 0.0.4
269
438
  */
270
439
  exclude?: string[]
@@ -353,7 +522,7 @@ export default defineConfig({
353
522
  dts: 'src/typings/uni-pages.d.ts', // 自定义类型声明路径
354
523
  dir: 'src/{pages,views}', // 多目录扫描
355
524
  subPackages: ['src/sub-package-a', 'src/sub-package-b'],
356
- exclude: ['node_modules', '.git', '**/__*__/**', '**/components/**'],
525
+ exclude: ['**/node_modules/**', '**/.git/**', '**/__*__/**', '**/components/**'],
357
526
  minify: true,
358
527
  debug: true, // 或 debug: 'pages'
359
528
  onAfterScanPages(pages) {
@@ -364,6 +533,59 @@ export default defineConfig({
364
533
  })
365
534
  ```
366
535
 
536
+ ### 页面排除规则
537
+
538
+ `exclude` 的相对模式统一基于 Vite `root`,绝对模式直接匹配对应路径。任一规则命中文件、目录或其祖先目录时,都会排除对应页面及整个目录子树。主包、字面分包、glob 分包以及固定或回调 `root` 的分包使用相同规则,首次扫描和后续页面事件的判断也一致。实际扫描目录、`outDir` 和分包 `root` 不改变排除基准。
539
+
540
+ 例如 Vite `root` 为 `/workspace/app`:
541
+
542
+ | `exclude` 项 | 排除范围 |
543
+ | --- | --- |
544
+ | `index.vue` | `/workspace/app/index.vue` |
545
+ | `src/pages/index.vue` | 指定的项目内页面 |
546
+ | `**/index.vue` | 项目根子树中各层的同名页面 |
547
+ | `src/packages/internal` | 该目录及整个子树 |
548
+ | `../shared/pages/**/index.vue` | 根外 `shared/pages` 中各层的同名页面 |
549
+ | `/workspace/shared/pages/internal` | 指定的绝对路径及其子树 |
550
+
551
+ `**` 不跨过前导 `../`,因此 `**/index.vue` 不会排除 `../shared/pages/index.vue`。根外目录需要显式写出前缀。默认值为 `['**/node_modules/**', '**/.git/**', '**/__*__/**']`,同样只覆盖项目根子树;需要排除根外的依赖目录时,应补充 `../shared/pages/**/node_modules/**` 等规则。
552
+
553
+ 显式提供 `exclude` 会替换默认值。`exclude: []` 禁用默认页面排除,底层 watcher 的固有忽略仍然生效。完全为空的字符串会被忽略,路径中的空格会保留;`!foo` 这类否定规则会报错,`!(keep).vue` 这类 extglob 仍可使用。
554
+
555
+ 页面排除控制自动发现的页面及其变更处理。被排除目录中的合法配置入口或配置依赖仍会参与配置加载和更新。插件收到事件后的排除结果保持一致,实际能否收到文件系统事件仍受 watcher 后端及其配置限制。
556
+
557
+ #### 迁移已有规则
558
+
559
+ 旧版本的文件扫描把 `exclude` 解释为相对每个实际扫描目录。统一到 Vite `root` 后,需要为这类旧规则补齐扫描来源的前缀。例如扫描 `src/pages`、`src/packages/*/pages` 和 `../shared/pages` 时,旧的 `exclude: ['index.vue']` 应迁移为:
560
+
561
+ ```ts
562
+ exclude: [
563
+ 'src/pages/index.vue',
564
+ 'src/packages/*/pages/index.vue',
565
+ '../shared/pages/index.vue',
566
+ ]
567
+ ```
568
+
569
+ 这保留了仅排除各扫描目录第一层 `index.vue` 的范围。直接改为 `**/index.vue` 会额外排除嵌套的同名页面,且仍不覆盖根外来源,不能作为等价迁移。
570
+
571
+ | 原写法 | 保留原文件扫描范围的迁移方式 |
572
+ | --- | --- |
573
+ | `internal/**` | 分别写 `src/pages/internal/**`、`src/packages/*/pages/internal/**`、`../shared/pages/internal/**` |
574
+ | `**/components/**` | 项目内保留原写法,根外补 `../shared/pages/**/components/**` |
575
+ | `node_modules` | 分别写 `src/pages/node_modules`、`src/packages/*/pages/node_modules`、`../shared/pages/node_modules` |
576
+
577
+ 默认值也从 `node_modules`、`.git` 改为项目根子树中各层的目录模式,扩大了项目内的默认排除范围。目录排除现在对所有扫描形式生效,静态分包也会排除命中的目录。
578
+
579
+ 默认值也会命中扫描目录本身。把页面写在依赖里的来源(例如 pnpm link 或 workspace 包中的 `dir: 'node_modules/@scope/ui/pages'`,以及指向同类路径的 `subPackages`)会被默认排除,一条页面都扫不到。保留这类来源需要显式传入 `exclude`,比如关闭默认页面排除的 `exclude: []`,或只留下还需要的那部分规则(如 `exclude: ['**/__*__/**']`)。
580
+
581
+ ### 路径基准
582
+
583
+ 页面相关的路径基准在同一次调整中统一,升级前请确认以下三点:
584
+
585
+ - Vite `root` 在解析阶段固定为绝对路径(相对 `process.cwd()` 传入时也会解析),之后切换 `process.cwd()` 不再影响扫描和排除结果。
586
+ - `dts: true` 的默认输出路径相对解析后的 `root` 计算;此前相对传入的 Vite root,未传时是 `process.cwd()`。
587
+ - `getPageFiles(directory, options)` 的 `directory` 相对 `options.root` 解析(此前相对 `process.cwd()`),返回值仍相对 `directory`。
588
+
367
589
  ### 完整类型定义
368
590
 
369
591
  请查看 [types.ts](./src/types.ts) 获取完整定义。
@@ -405,7 +627,7 @@ export default defineConfig({
405
627
 
406
628
  #### 方案二:自行编写脚本,在 uni 命令前生成
407
629
 
408
- 如果不想引入 `unh`,可以自行编写脚本在 `uni` 命令前生成 `pages.json`,再用 `&&` 串联或 npm `predev`/`prebuild` 钩子触发。脚本需要复刻本插件的完整流水线(加载 `pages.config.ts`、扫描页面文件、解析 `definePage` 宏、合并元数据),维护成本较高,故优先推荐方案一。
630
+ 如果不想引入 `unh`,可以自行编写脚本在 `uni` 命令前生成 `pages.json`,再用 `&&` 串联或 npm `predev`/`prebuild` 钩子触发。脚本需要复刻本插件的完整流水线(加载 `pages.config.ts`、扫描页面文件、解析 `definePage` 宏、合并元数据),维护成本较高,建议优先用方案一。
409
631
 
410
632
  ### 支持 JSX/TSX 吗?
411
633
 
@@ -415,6 +637,164 @@ export default defineConfig({
415
637
 
416
638
  文件名内不能带有额外的 `.` 分隔符,如 `index.v1.vue` 不合法。这是小程序的限制,并非本插件的限制。
417
639
 
640
+ ### 如何自动发现分包?
641
+
642
+ `subPackages` 的字符串和对象 `dir` 都支持目录 glob。例如,`subPackages: ['src/packages/*/pages']` 会把每个匹配的 `pages` 目录作为分包目录,生成的默认 `root` 相对 `outDir` 计算。每条规则的匹配结果按路径排序,规则之间保留配置顺序。
643
+
644
+ `dir` 和 `subPackages` 的值一律按 glob 模式解析,字面目录名里的 glob 元字符也按模式解释。目录名为 `app[prod]` 时写成 `app\[prod\]/src/pages`;不转义的 `[prod]` 是字符类,会匹配到 `appp` 这类目录名,写成绝对路径时还可能一个目录都匹配不到。
645
+
646
+ 如需让分包 `root` 指向扫描目录的上级,可以使用同步回调:
647
+
648
+ ```ts
649
+ import { posix } from 'node:path'
650
+ import UniPages from '@uni-helper/vite-plugin-uni-pages'
651
+
652
+ UniPages({
653
+ subPackages: [{
654
+ dir: 'src/packages/*/pages',
655
+ root: dir => posix.relative('src', posix.dirname(dir)),
656
+ }],
657
+ })
658
+ ```
659
+
660
+ 对于 `src/packages/account/pages/profile.vue`,回调收到 `src/packages/account/pages`,生成:
661
+
662
+ ```json
663
+ {
664
+ "subPackages": [{
665
+ "root": "packages/account",
666
+ "pages": [{ "path": "pages/profile" }]
667
+ }]
668
+ }
669
+ ```
670
+
671
+ 回调参数始终相对 Vite 项目 `root`,使用 `/` 分隔且不带末尾斜杠;传入绝对 glob 时也遵守这一规则。返回值必须是非空相对路径,不能是 `.`、绝对路径或包含 `..`。每次重新发现目录时都会调用回调,不能返回 Promise。
672
+
673
+ 目录与路径规则:
674
+
675
+ - 同一物理目录的固定值和回调结果都是显式 `root`,优先于未填写 `root` 的默认值;多个显式值相同可以合并,不同则报错
676
+ - 多个目录得到同一 `root` 时统一聚合页面,静态目录、glob、回调和混合写法遵守相同规则
677
+ - 排除目录或文件使用 `exclude`;`subPackages` 不接受以 `!` 开头的否定规则
678
+ - 字符串形式的自定义 `root` 保留既有路径计算规则;设置 `root` 不会移动页面文件
679
+
680
+ H5 开发服务器和 `vite build --watch` 都会重新发现主包 `dir` 与分包目录,支持启动时目录尚不存在,以及运行中的新增、删除、重命名和重建。空目录会被发现,在包含页面之前不生成空分包。目录可以位于 Vite 项目根目录之外,路径仍相对 Vite `root` 解析,不受命令执行目录影响。`mergePages: false` 时不扫描或监听页面目录;H5 的监听范围仍受 Vite `server.watch` 配置约束。
681
+
682
+ Windows 下,原生监听会持有子目录句柄,重命名其父目录可能报 `EPERM`,详见 [Chokidar #1380](https://github.com/paulmillr/chokidar/issues/1380)。需要在运行期间重命名这类目录时,可在启动命令前设置环境变量 `CHOKIDAR_USEPOLLING=true`,为 H5 和 `build --watch` 启用 [Chokidar 轮询后端](https://github.com/paulmillr/chokidar/blob/5.0.0/README.md#performance)。页面内容与恢复入口使用相同的轮询设置,`CHOKIDAR_INTERVAL` 可指定轮询间隔(毫秒);H5 也支持 Vite 的 `server.watch.usePolling` 和 `server.watch.interval`。轮询会增加 CPU 和文件系统访问开销,插件不会默认启用。
683
+
684
+ Vite 的 `server.watch.ignored` 同样约束 H5 的页面监听。相对字符串规则按 watcher 的 `cwd` 解析;未设置 `cwd` 时与 Chokidar 一致按 `process.cwd()` 展开,`**/` 开头的规则仍匹配任意绝对路径,`!` 否定规则在解析后保留否定语义。`server.watch.awaitWriteFinish` 和 `server.watch.atomic` 会转发给页面内容 watcher,与 Vite 自身的监听行为保持一致。
685
+
686
+ 动态发现会更新 `pages.json`。如果下游插件只在创建时缓存分包信息,它仍需要重启才能使用新增分包;自动重新发现不会改变其他插件的缓存策略。
687
+
688
+ ### 扫描目录与页面归属
689
+
690
+ 多个扫描声明可以指向同一目录,也可以通过符号链接指向同一页面。插件使用已存在路径的 `realpath` 判断目录和页面身份,不合并硬链接。身份判断不改变路径基准:`exclude` 仍匹配各来源的词法路径,`root` 回调仍收到相对 Vite `root` 的匹配路径,输出路径仍从选中的来源路径计算。
691
+
692
+ 同一物理目录先确定分包 `root`,但保留全部来源用于文件排除。例如 alias 和真实目录都被扫描,只有 alias 下的 `foo.vue` 被排除时,真实目录下未被排除的同一文件仍可贡献页面。目录级排除会移除该目录来源;仅排除其中的文件不会撤销该目录的显式 `root` 意图。
693
+
694
+ 页面归属遵守以下规则:
695
+
696
+ - 同一页面归属同一分包 `root` 时只保留一份;主包和分包同时命中该页面时由有效分包来源优先
697
+ - 同一有效页面被不同分包 `root` 认领时报错,错误中列出相关来源和声明
698
+ - `definePage(null)` 及现有平台规则跳过的页面不参与页面归属和生成路由冲突;目录声明的显式 `root` 冲突仍会报错
699
+ - 代表页面优先从未被排除、当前平台有效的显式来源中选择,其余按声明顺序选择;每条 glob 内按匹配路径排序。没有显式 `root` 时,默认值也按此稳定顺序选择
700
+
701
+ 交换声明顺序可能改变代表路径、默认 `root`、页面顺序及最终路由,但不会因为提前丢弃其他别名来源而漏掉有效页面。不存在的目录不创建物理身份;原始声明和稳定祖先监听仍保留,目录出现后重新解析。空目录或零匹配的 glob 不会改变其他目录按 `root` 聚合的方式。
702
+
703
+ 不同生成来源占用同一完整路由时会报错,包括主包与分包碰撞,以及通过 `.`、`..`、分隔符或末尾斜杠表达的同一路由。比较时会规范化路径,输出仍沿用原有路径换算。已有 `.vue`、`.nvue`、`.uvue` 格式覆盖和主包平台变体规则在原适用范围内保留;不会将主包平台后缀规则推广到分包,也不会因文件族相同而放过不同分包之间的路由冲突。
704
+
705
+ `pages.config` 中的同路由条目是元数据覆盖:先按 `root` 聚合全部扫描页面,再合并配置,未覆盖的扫描字段、手工页面、`plugins` 和仅有配置的分包均保留。扫描来源冲突在配置覆盖前检查,不能用配置中的同名条目掩盖;冲突发生时不会把冲突结果写入已有 `pages.json`。
706
+
707
+ #### 迁移重复扫描声明
708
+
709
+ 以下变化以统一扫描归属前的主分支为比较基准;分包 glob 尚未进入正式版 `0.5.0`。
710
+
711
+ - 静态目录共用同一 `root` 时,过去被后一个目录覆盖的页面现在会重新出现;若这些页面不需要参与生成,应删除相应扫描声明,或用 `exclude` 排除文件、目录
712
+ - 同一目录同时配置默认和显式 `root` 时,改为显式值优先;多个不同显式值仍需统一或移除重复声明
713
+ - 同一物理页面经多个别名生成的多条路由会收敛为一个代表来源,可通过调整有效显式来源和声明顺序确定保留的词法路径
714
+ - 同一页面跨分包认领、不同生成文件占用同一完整路由时,改为明确报错;需要调整扫描范围、分包归属或 `definePage.path`,仅增加配置覆盖不能消除来源冲突
715
+
716
+ 扫描入口会递归收录页面。同时声明 `src/pkg-a` 与 `src/pkg-a/pages`,或一条 `src/pkg-a/**` 同时匹配二者时,子目录页面可能被不同 `root` 重复认领;错误会列出原始规则、匹配目录及分包 `root`。整棵目录属于一个分包时,只声明 `subPackages: ['src/pkg-a']` 即可保留父、子目录的页面;确需多个入口且属于同一分包时,可以统一 `root`,例如 `{ dir: 'src/pkg-a/**', root: 'pkg-a' }`。
717
+
718
+ `exclude` 按词法路径过滤,对所有扫描入口生效。例如,`exclude: ['src/pkg-a/pages']` 也会排除父目录递归扫描到的 `pages/index.vue`,不能只撤销子目录那条扫描声明。需要保留页面、只消除重复认领时,应调整扫描声明或统一分包 `root`;不需要页面时再排除文件、目录。仅删除一条声明,其他入口仍可能收录该页面。
719
+
720
+ 格式覆盖例外按真实文件族判断。例如,真实 `shared.vue` 与指向无扩展名文件的 `shared.nvue` 别名同占 `shared` 路由,此前主分支可能只留下一个结果,现在因不同物理来源不满足真实文件族条件而报冲突,需调整扫描范围或页面路由。单独扫描这个 `shared.nvue` 别名仍可收录页面,不会仅因真实目标没有扩展名而被拒绝。
721
+
722
+ ### 页面内容与符号链接监听
723
+
724
+ 页面文件、目录的新增和删除,以及页面内容 watcher 首次 `ready` 后的补扫,都会重新读取当前页面内容。同路径快速删除重建也会刷新标题、路由和 `definePage(null)` 状态。普通 `change` 只重读该物理文件关联的缓存候选,其他页面继续复用内容缓存,包括归属决议中未被选为代表的来源。普通 `change` 下页面元数据未变时,跳过配置加载与产物生成。元数据变化时仍会扫描页面清单、校验归属与路由冲突,再合并产物。读取失败时保留上次成功配置,后续变更或整轮补扫可以重试。
725
+
726
+ 已收录的符号链接页面可以通过词法路径或真实目标路径的事件更新。事件按当前有效的词法来源检查 `exclude` 和扫描规则,再重新计算页面归属;物理目标的位置不改变排除或输出路径的基准。
727
+
728
+ 页面来源目录内部的嵌套符号链接也参与目标发现和恢复。链接指向空目录、尚未创建的目录或另一层链接时,插件仍保留当前入口关系;目标创建后会发现页面并继续监听内容。嵌套链接只补充当前来源的监听依赖,不改变主包、分包的声明和输出路径。
729
+
730
+ 例如 `src/pages/feature` 指向外部 `shared/pages`。启动时目标不存在,稍后创建 `shared/pages/profile.vue` 会自动加入页面。运行中将 `feature` 改指另一目录后,新目录的后续页面修改也会更新产物。两个有效入口共用同一目标时,移除其中一个入口仍保留另一入口所需的订阅;最后一个有效入口退出后才释放目标。多层链接按各自的词法路径应用排除规则,同一遍历链路遇到已访问的物理目录时停止展开循环,其他独立别名继续参与发现。
731
+
732
+ 监听目标统一换算为真实路径后再用于订阅和事件身份比较;词法路径仍用于 `exclude`、输出和诊断。这样同一个目标的 Unicode 规范化或大小写拼写差异不会导致事件无法映射。目标路径中暂时不可读的部分按“暂不可用来源”处理:本轮跳过该来源的监听,但页面扫描和产物生成继续;权限恢复后,后续完成刷新或重新扫描时再重新发现。链接自身在解析期间被删除或改指时也按悬空目标处理,下一轮刷新会重新解析。页面监听初始化失败只记录错误,不阻断页面生成,并会在后续生成时重试。
733
+
734
+ #### Vite 配置与模块热更新
735
+
736
+ 普通非循环页面链接无需额外配置。UniPages 自行发现链接、解析真实目标并建立页面监听;Vite 的共享监听负责模块更新与 HMR,两条更新路径需要分别判断。
737
+
738
+ 以下两项均为 Vite 配置,分别控制文件监听和模块解析,按项目需要设置:
739
+
740
+ | 配置 | 默认值 | 职责 |
741
+ | --- | --- | --- |
742
+ | `server.watch.followSymlinks` | `true` | 控制监听器是否沿符号链接覆盖目标 |
743
+ | `resolve.preserveSymlinks` | `false` | 控制模块身份是否保留链接路径;默认使用解析后的真实路径 |
744
+
745
+ #### 循环链接的可选处理
746
+
747
+ 仅当循环链接导致 Vite 共享 watcher 报 `ELOOP` 时,才考虑在 H5 开发中关闭链接递归。该问题已在未启用 UniPages 的对照中复现,下面是 Vite 监听层的可选规避措施:
748
+
749
+ ```ts
750
+ export default defineConfig({
751
+ server: {
752
+ watch: { followSymlinks: false },
753
+ },
754
+ plugins: [UniPages()],
755
+ })
756
+ ```
757
+
758
+ `followSymlinks` 作用于整个共享 watcher,可能影响链接模块的 HMR 和依赖它的其他插件。实际影响取决于模块解析路径及真实目标是否另行受到监听,调整后应验证项目中的模块热更新。
759
+
760
+ 项目采用 `preserveSymlinks: true` 保留链接路径时,关闭 `followSymlinks` 后应特别验证链接模块的 HMR。页面配置恢复成功不能证明模块热更新正常。
761
+
762
+ <details>
763
+ <summary>HMR 实测结果与验证范围</summary>
764
+
765
+ 验证环境为 Vite 5.4.21、macOS,使用 Vue 插件加载项目外链接的 Vue 页面和独立 JS 模块。Node watcher 与 FSEvents 两种后端得到相同结果:
766
+
767
+ | `followSymlinks` | `preserveSymlinks` | Vue 模板与 JS 模块更新 | `pages.json` 元数据 |
768
+ | --- | --- | --- | --- |
769
+ | `false` | `true` | 无 HMR 更新消息,再次请求模块转换代码仍是旧内容 | 正常更新 |
770
+ | `false` | `false`(默认值) | HMR 更新消息与转换代码正常更新 | 正常更新 |
771
+
772
+ 这些结果只描述该验证场景,不构成其他配置组合或默认配置的 HMR 保证。核验范围为服务端消息和转换代码,未覆盖浏览器渲染、样式 HMR、Windows、Linux、polling、其他 Vite 版本或其他插件行为。
773
+
774
+ 页面监听回归(含循环链接场景)验证页面发现、配置生成、监听订阅与恢复。私有页面 watcher 未向 Vite 转发全部页面事件;普通 `change` 下元数据未变时,生成逻辑会提前返回。
775
+
776
+ </details>
777
+
778
+ #### 来源恢复与订阅
779
+
780
+ 当前配置中的词法来源决定页面范围,符号链接的当前指向决定内容目标。来源暂时不可用时,插件观察恢复所需的目录入口;仍指向已删除目标的链接可以在目标重建后恢复。确认链接改指另一个目标时,旧目标会退出该来源的关联与订阅,新目标尚不存在时也按新指向等待恢复。链接自身删除后,恢复时重新解析其当前指向。
781
+
782
+ 页面来源可以直接位于 `node_modules` 内,也可以链接到其中。这类路径是否参与监听由 `exclude` 和 Vite 忽略规则按词法来源决定,插件不会额外按 `node_modules`、`.git` 路径片段跳过。默认 `exclude` 仍会排除依赖目录,保留这类来源时需要显式放开,例如 `exclude: []`。
783
+
784
+ 页面文件事件统一由插件独立持有的页面监听器处理。内容订阅使用解析后的物理路径,不递归跟随链接。所有有效词法来源共同决定订阅集合:共享目标只订阅一次,父目录已经覆盖的子目录和文件目标不重复注册,目录之外的文件别名单独订阅实际文件。恢复观察采用非递归的目录入口监听,不会把公共祖先下的兄弟项目加入递归监听。恢复或切换目标后会收回不再需要的订阅。H5 开发服务器和 `build --watch` 使用相同的页面监听机制:共享 watcher 提供配置文件事件和有效页面目录的结构通知。Vite 的共享 watcher 仍由 Vite 管理,构建模式的配置 watcher 按解析后的依赖路径注册,不递归跟随页面链接。恢复以能够从来源路径解析出的链接指向为依据。
785
+
786
+ 页面事件在生成期间到达时合并到下一轮。同一待处理批次中的多个普通内容变更只重读各自关联的缓存候选;批次中包含新增、删除、目标切换或恢复等结构变化时,执行完整刷新。
787
+
788
+ 编辑器通过临时文件重命名覆盖页面时,文件目标的内容监听负责接续替换后的文件。被识别为普通内容变更的事件只重读关联页面;原子保存产生的新增、删除事件或后端目录通知,也可能触发来源发现或结构刷新。
789
+
790
+ 监听来源发现会遍历声明的页面目录,并在同一轮内复用物理目录读取。普通 `change` 下元数据未变时,生成逻辑提前返回;元数据变化后继续生成,以及新增、删除、改指、恢复和 `ready` 补扫等结构刷新时,都会重新发现监听来源。这些是分别可能触发的场景,不能将所有保存都视为普通 `change`。
791
+
792
+ 每次来源发现创建独立的词法路径排除查询,复用本轮路径及共同祖先的 `exclude` 匹配结果,遍历结束后释放。该复用减少重复规则匹配,不缓存文件系统状态或 Vite `ignored` 的判断结果,也不减少目录读取或省略扫描。
793
+
794
+ 页面清单通过 glob 遍历目录,监听依赖发现另有一次目录遍历。当两次遍历覆盖同一目录树时,目录读取约多一遍;这不表示每次保存都会增加遍历,也不表示整条生成链的耗时翻倍。评估性能时需要同时观察页面清单扫描、监听来源发现和完整生成链路。
795
+
796
+ 单轮来源发现最多登记 1024 个链接来源。别名层叠会让同一物理目录经多条词法路径重复展开;达到上限后本轮停止继续展开链接并告警,页面扫描和 `pages.json` 生成不受影响,但超出上限的来源本轮不会建立内容监听。需要完整覆盖时,收窄 `dir`/`subPackages` 或通过 `exclude` 移除不再使用的链接;后续刷新会按调整后的范围重新发现。
797
+
418
798
  ### 支持 monorepo 吗?
419
799
 
420
800
  在 monorepo 项目中,如果页面分布在多个 package 中,可以使用 `subPackages` 配置的对象格式来自定义生成的 `root` 路径。
@@ -449,6 +829,25 @@ export default defineConfig({
449
829
 
450
830
  更多上下文参考 <https://github.com/uni-helper/vite-plugin-uni-pages/issues/271>。
451
831
 
832
+ ## 0.6.0 破坏性变更
833
+
834
+ 升级到 0.6.0 前请阅读以下变更,完整的迁移步骤见[迁移指南](https://uni-helper.js.org/vite-plugin-uni-pages/other/migration):
835
+
836
+ - **`exclude` 基准**:相对模式改为基于 Vite `root`,命中文件、目录或任一祖先目录时排除整个子树,主包、分包、首次扫描和页面事件共用同一套规则。旧规则需要补上扫描来源的前缀,默认值也改为 `['**/node_modules/**', '**/.git/**', '**/__*__/**']`,`!foo` 这类否定规则直接报错(见上方「页面排除规则」)。
837
+ - **扫描来源与页面归属**:同一 `root` 的重复扫描声明不再互相覆盖,显式 `root` 优先于默认值,同一物理页面的多个别名收敛为一个代表来源。跨分包认领同一页面、不同来源占用同一完整路由时报错,冲突结果不会写入 `pages.json`(见上方「扫描目录与页面归属」)。
838
+ - **路径基准**:Vite `root` 在解析阶段固定为绝对路径;`dts` 传字符串时相对 `root` 解析;`getPageFiles(directory, options)` 的 `directory` 改为相对 `options.root`(见上方「路径基准」)。
839
+ - **实例生命周期**:一个实例同时只服务一个 Vite 会话。开发服务器未关闭时启动构建、构建未完成时再启动、`build --watch` 期间再次启动,以及 `server.restart()` 都会报错,这些场景应各自创建 `UniPages()` 实例。顺序复用仍然可用(见上方「实例复用与关闭」)。
840
+ - **`UNI_PLATFORM` 必填**:初始化时读不到 `process.env.UNI_PLATFORM` 会直接报错。0.5.x 会以空平台继续生成,条件编译分支全部被跳过。
841
+
842
+ 0.6.0 同时带来以下新能力:
843
+
844
+ - `prepare()` 在 Vite 启动前完成一次生成,供工厂函数阶段读取 `pages.json` 的下游插件使用(见上方「提前生成 pages.json」)
845
+ - `pages.config` 本地依赖热更新(见上方「配置依赖热更新」)
846
+ - `subPackages` 目录 glob 自动发现,`root` 支持同步回调(见上方「如何自动发现分包」)
847
+ - 页面内容变化刷新标题、路由和 `definePage(null)` 状态,符号链接页面支持嵌套链接、删除重建和恢复(见上方「页面内容与符号链接监听」)
848
+ - 多平台 tabBar 条目按配置顺序摆放
849
+ - `@uni-helper/uni-pages-types` 补齐 uni-app x 字段,Schema 同步重新生成,改动均为新增
850
+
452
851
  ## 0.5.0 破坏性变更
453
852
 
454
853
  升级到 0.5.0 前请阅读以下变更:
@@ -484,46 +883,56 @@ export default defineConfig({
484
883
 
485
884
  - `definePage(null)` 按平台跳过页面注册(见上方「页面级配置 definePage」)
486
885
  - 函数式 `definePage` 与 `define().ifdef()/.ifndef()` 条件 DSL(见上方「平台条件页面配置」)
487
- - 插件生成的过期子包自动清理(见下方「多终端并发开发」)
886
+ - 插件生成的过期分包自动清理(见下方「多终端并发开发」)
488
887
  - tabBar 顶层属性按平台分别记录,多平台取值互不覆盖(见下方「多终端并发开发」)
489
888
  - 新增 `indent`、`eol`、`insertFinalNewline` 格式化选项
490
889
 
491
890
  ## 多终端并发开发
492
891
 
493
- 同一项目里同时运行到多个平台时(例如,两个终端分别运行 `pnpm run dev:mp-weixin` 和 `pnpm run dev:mp-alipay`),多个进程会读写同一个 `pages.json`。
892
+ 同一项目同时运行多个平台时(例如两个终端分别运行 `pnpm run dev:mp-weixin` 和 `pnpm run dev:mp-alipay`),多个进程会读写同一个 `pages.json`。
494
893
 
495
- 本插件默认会:
894
+ 插件默认会:
496
895
 
497
- - 通过文件锁串行化对 `pages.json` 的「读取已有内容 → 合并当前平台配置 → 写回」整个流程,避免并发写入互相覆盖;
498
- - 保留其他平台已经写入的 `#ifdef` 条件编译块,只更新当前平台对应的条目;
499
- - 对同一 `path`(或 tabBar 的 `pagePath`)的条目按内容去重:内容相同的条目会合并为一条,并把两个平台合在一起写(如 `H5 || MP-WEIXIN`),不会因为先后跑过多个平台而产生重复路由;
500
- - tabBar 的顶层属性(`color`、`selectedColor`、`custom`、`position`、`midButton` 等)同样按平台分别记录:各平台取值不同时,每个平台的取值保存在自己的 `#ifdef` 块里(同一属性名会出现多个按平台互斥的副本,uni-app 条件编译后每个平台只剩一份);取值相同时合并为一条不带条件的属性。只有声明了非空 `list` 的运行才会写属性,`list` 为空或未配置 tabBar 的运行不会覆盖其他平台已写入的属性。注意 `minify: true` 的输出无法携带注释,属性会变成最后写入的说了算,请对同一项目所有平台的构建保持一致的格式化设置;
501
- - 手写的 `#ifndef` 块(插件自身只会写 `#ifdef`)表达不了「按平台列表」的规则,会原样保留、不参与合并,tabBar 属性上的 `#ifndef` 同样处理。页面与 tabBar 条目按「只包裹紧随其后的那一个条目」解释;tabBar 的外观属性支持一个条件块包住多个属性——块里每个属性会各自带上完整的 `#ifndef`/`#endif` 包裹重新输出(属性之间互相独立,各平台看到的显示/隐藏结果不变),`#ifdef` 多属性块同理按注释里列出的平台归属每个属性。扫描结果与某个 `#ifndef` 条目内容相同时,会并进该条目,避免当前平台出现重复路由。注意两个边界:当前平台恰好是被排除的平台时,内容相等的配置按 `#ifndef` 语义同样被隐藏;同一 tabBar 属性同时存在 `#ifndef` 副本与各平台不同取值的变体时,被多份副本覆盖的平台会出现重复键,按 JSON 规则后面的键覆盖前面的(当前平台自己的取值写在最后、优先生效)。
896
+ - 通过文件锁串行化对 `pages.json` 的「读取已有内容 → 合并当前平台配置 → 写回」整个流程,避免并发写入互相覆盖。
897
+ - 保留其他平台已经写入的 `#ifdef` 条件编译块,只更新当前平台对应的条目。
898
+ - 对同一 `path`(或 tabBar 的 `pagePath`)的条目按内容去重。内容相同的条目会合并成一条,平台列表叠在一起(如 `H5 || MP-WEIXIN`),不会因为先后跑过多个平台而产生重复路由。
899
+ - tabBar 的 `list` 按配置顺序摆放,分两种情况:
900
+ - 配置覆盖全部既有条目(全量超集)时,整个列表按配置顺序输出,其他平台新增的条目也会落在声明位置。
901
+ - 配置没有覆盖全部条目时,既有条目保持磁盘顺序,新增条目插到它在配置里最近的前驱之后,两个平台交替构建时列表不会来回翻转。
502
902
 
503
- 这样每个终端都能保留各自的平台配置。条件编译注释基于 `comment-json` 写入,最终 `pages.json` 同时包含所有平台的分支。
903
+ 两个平台的配置都覆盖全部条目但顺序不同时,共享文件跟随最后写入者。内容相同而合并成一条的条目没有按平台区分的副本,另一个正在运行的平台可能在下一次生成前读到对方顺序。并发开发时,各平台最好保持一致的完整 tab 顺序。`pages` 数组不参与重排:首页条目由「首页提到最前」的规则单独管理,各平台声明的首页不同时,首页变体的相对顺序按首次出现保持稳定。
904
+ - tabBar 的顶层属性(`color`、`selectedColor`、`custom`、`position`、`midButton` 等)按平台分别记录。取值不同时,每个平台的取值放在自己的 `#ifdef` 块里,同一个属性名会出现多个按平台互斥的副本,uni-app 条件编译后每个平台只剩一份;取值相同时合并成一条不带条件的属性。
905
+ - 只有声明了非空 `list` 的运行才会写属性。`list` 为空或没有 tabBar 的运行不会覆盖其他平台已写入的属性。`minify: true` 的输出带不了注释,属性会变成最后写入者说了算,同一项目所有平台的格式化设置要保持一致。
906
+ - 手写的 `#ifndef` 块原样保留,不参与合并,插件自身只写 `#ifdef`。页面和 tabBar 条目按「只包裹紧随其后的那一个条目」解释;tabBar 外观属性支持一个条件块包住多个属性,块里每个属性都会带上完整的 `#ifndef`/`#endif` 包裹重新输出,属性之间互相独立,各平台看到的显示和隐藏结果不变。
907
+ - `#ifdef` 多属性块按注释里列出的平台归属每个属性。扫描结果与某个 `#ifndef` 条目内容相同时,会并进该条目,避免当前平台出现重复路由。
908
+ - 两个边界:当前平台恰好是被排除的平台时,内容相等的配置按 `#ifndef` 语义同样会被隐藏;同一个 tabBar 属性同时存在 `#ifndef` 副本和按平台不同取值的变体时,被多份副本覆盖的平台会出现重复键,按 JSON 规则后面的键覆盖前面的,当前平台自己的取值写在最后,优先生效。
504
909
 
505
- ### 生成标记与过期子包清理
910
+ 条件编译注释由 `comment-json` 写入,最终 `pages.json` 同时包含所有平台的分支。
911
+
912
+ ### 生成标记与过期分包清理
506
913
 
507
914
  自 0.5.0 起,插件会在 `pages.json` 的 `pages` 与每个 `subPackages` 条目的头部写入生成标记注释(如 `// GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN`),记录写入过该数组的平台全集。
508
915
 
509
- 平台全集**始终包含当前运行的平台**,即使本次运行没有向该数组贡献任何条目(例如所有页面都在当前平台通过 `definePage(null)` 跳过)。这样其他平台专属的条目会保持 `#ifdef` 包裹、不会不加包裹地输出:uni-app 条件编译下,没被包裹的条目所有平台都看得见,不加包裹会把它们漏进当前平台的构建产物。平台全集还会把上一次运行记在标记里的平台算进来,并且只增不减:某平台的条目被它自己的扫描重写后,它也不会从全集中消失,避免多终端交替写入时,残留的条目被误判成盖住全平台、不加包裹地输出。
916
+ 平台全集**始终包含当前运行的平台**,即使这一轮没有向该数组贡献任何条目(例如所有页面都在当前平台通过 `definePage(null)` 跳过)。这样其他平台专属的条目会一直保持 `#ifdef` 包裹,不会不加包裹地输出到所有平台。uni-app 条件编译下,没被包裹的条目所有平台都看得见,不加包裹会把它们漏进当前平台的构建产物。
917
+
918
+ 平台全集还会把上一次运行记在标记里的平台算进来,并且只增不减。某平台的条目被自己的扫描重写后,它也不会从全集里消失,避免多终端交替写入时,残留条目被误判成覆盖了全平台、不加包裹地输出。
510
919
 
511
920
  基于该标记,插件会自动清理由它生成、但在当前平台扫描中已不存在的条目:
512
921
 
513
- - **页面条目**:主包 `pages` 中带生成标记但本次扫描没找到的条目,会把当前平台从它的平台列表里去掉;其他平台的 `#ifdef` 条目保留。
514
- - **子包条目**:带生成标记且本次扫描没找到的子包(所有页面都被 `definePage(null)` 跳过,或目录被删除),当前平台已经看不到它的任何页面,整个子包条目会从 `pages.json` 中删除。这里不会保留其他平台条目的空壳:当前平台的构建产物里没有该分包目录,app.json 里留空壳会指向一个不存在的 root。删除不会丢状态——每个平台每次运行都会从自己的扫描结果重写条目,其他平台下次写入时会把子包重新写回。
515
- - **子包 `plugins`**:`subPackages` 配置里的 `plugins` 跟着配置走——配置里有就随子包写进 `pages.json`,配置里删掉后就从插件生成的子包(带生成标记)上自动清掉,不会一直留在产物里。手写子包(没有生成标记)的 `plugins` 不受影响。
516
- - **用户手写内容**:没有生成标记的条目视为用户手写,插件永远不会修改或删除它们。如果你在 `pages.config.ts` 的 `subPackages` 里手动维护子包,或直接在 `pages.json` 里手写子包条目,它们不受清理影响。在 `pages.config.ts` 的子包里写 `pages` 时,`path` 按 `pages.json` 的惯例**相对 `root` 书写**(如 `root` 为 `pkg` 时写 `detail`):同一个页面文件扫描到时,两条会按页合并成一条,你写的属性(如 `style`)覆盖扫描结果。
922
+ - **页面条目**:主包 `pages` 里带生成标记但这一轮扫描没找到的条目,会把当前平台从平台列表里去掉;其他平台的 `#ifdef` 条目保留。
923
+ - **分包条目**:带生成标记但这一轮扫描没找到的分包(所有页面都被 `definePage(null)` 跳过,或目录被删除),当前平台已经看不到它的任何页面,整个分包条目会从 `pages.json` 里删除。不保留其他平台的空壳:当前平台的构建产物里没有这个分包目录,`app.json` 里留空壳会指向不存在的 root。删除不会丢状态,每个平台每次运行都会按自己的扫描结果重写条目,其他平台下次写入时会把它加回来。
924
+ - **分包 `plugins`**:`subPackages` 配置里的 `plugins` 跟着配置走。配置里有就随分包写进 `pages.json`,配置里删掉就从插件生成的分包上清掉,不会一直留在产物里。手写分包没有生成标记,它的 `plugins` 不受影响。
925
+ - **用户手写内容**:没有生成标记的条目视为用户手写,插件不会修改或删除。在 `pages.config.ts` 的 `subPackages` 里手动维护分包,或直接在 `pages.json` 里手写分包条目,都不受清理影响。在配置的分包里写 `pages` 时,`path` 按 `pages.json` 的惯例**相对 `root` 书写**(如 `root` 为 `pkg` 时写 `detail`);同一个页面文件扫描到时,两条会按页合并,你写的属性(如 `style`)覆盖扫描结果。
517
926
 
518
927
  > 注意:这个约定依赖生成标记的存在。不要删除插件写入的 `// GENERATED BY UNI-PAGES, PLATFORM: ...` 注释行,否则对应条目会被视为用户手写,过期后不再被清理。
519
928
 
520
929
  ### 平台全集只增不减
521
930
 
522
- 平台全集只增不减:某平台写入过一次,它就会一直留在标记里(即使该平台的开发终端之后再也不运行)。多算的平台唯一的影响,是让各平台不相同的条目一直包着 `#ifdef`——多余但无害,不会漏东西。
931
+ 某平台写入过一次,它就会一直留在标记里(即使该平台的开发终端之后再也不运行)。多算的平台唯一的影响,是让各平台不相同的条目一直包着 `#ifdef`:多余但无害,不会漏东西。
523
932
 
524
933
  如果确实彻底停用了某个平台、想让 `pages.json` 回到干净状态:**不要手动编辑标记行**(改错会导致条目被误判为用户手写,或不加包裹地漏出去),直接删除生成的 `pages.json`,下次运行会从零重新生成。
525
934
 
526
- 另外,「用户手写内容永不修改或删除」的保护范围仅指上述清理逻辑覆盖的场景(带生成标记的子包条目)。`pages` 数组中不带生成标记、也不在本次扫描结果里的手写页面条目,会在合并时因平台列表为空而被丢弃——这是历史行为;需要长期保留的手写页面建议放进 `pages.config.ts` 的 `pages` 数组,插件每次都会原样并入。
935
+ 另外,「用户手写内容永不修改或删除」只覆盖清理逻辑涉及的场景(带生成标记的分包条目)。`pages` 数组里不带生成标记、也不在这一轮扫描结果里的手写页面条目,会在合并时因为平台列表为空被丢弃,这是历史行为。需要长期保留的手写页面,建议放进 `pages.config.ts` 的 `pages` 数组,插件每次都会原样并入。
527
936
 
528
937
  ## 开发
529
938
 
@@ -534,20 +943,38 @@ export default defineConfig({
534
943
  插件围绕 `PageContext` 组织核心流程,流水线阶段顺序固定(加载用户配置 → 扫描页面 → 合并页面配置 → 写入 pages.json),各专项能力放在独立的模块里:
535
944
 
536
945
  ```
537
- index.ts Vite 插件入口 — configResolved / transform / configureServer / resolveId / load
538
- context.ts PageContext 编排核心 — 配置加载、扫描、合并、监听、虚拟模块与 HMR
539
- pipeline.ts 纯流水线入口 — createPages / generateAll,root 和 platform 从外部传入,测试直接走这里
540
- pages-json.ts pages.json 读-改-写 — 多平台 #ifdef 合并、首页排前、
541
- 生成标记与过期子包清理、序列化格式化、文件锁与原子写入
542
- files.ts 文件工具 — 页面扫描、路径解析、文件锁(withFileLock)、pages.json 合法性检查
543
- macro.ts definePage 宏解析 — SFC 解析、单个 script 块失败不影响别的块、
946
+ index.ts Vite 插件入口:configResolved / transform / configureServer / resolveId / load
947
+ context.ts PageContext 编排核心:消费配置监听计划与扫描计划,串行生成、写入快照、监听与 HMR
948
+ pipeline.ts 纯流水线入口:createPages / generateAll,root 和 platform 从外部传入,测试直接走这里
949
+ config-loader.ts 配置加载:unconfig 发现与回调、内存转换、隔离产物、本地依赖与恢复输入
950
+ config-watch.ts 配置监听计划:来源、依赖与恢复候选归一化为文件、恢复目录与注册路径,
951
+ 并负责配置事件判定
952
+ config-source.ts 源位置适配:每模块运行时解析、静态调用保留、源码映射
953
+ directories.ts 目录发现:glob 展开、subPackages 来源发现与扫描目录投影
954
+ scan-plan.ts 扫描计划:一次发现主包目录、分包来源、最终 root 与规则,作为不可变视图
955
+ page-rules.ts 页面规则:基于 Vite root 编译排除模式,统一扫描与事件判定
956
+ page-sources.ts 扫描来源与页面归属:来源身份、代表选择、生成路由冲突与文件族校验
957
+ page-watch.ts 页面监听:当前词法来源、内容目标订阅与非递归恢复入口,消费扫描计划
958
+ page-watch-sources.ts 监听依赖发现:顶层与嵌套链接解析、悬空目标保留及循环剪枝
959
+ page-watch-filter.ts 监听过滤:将 Vite ignored 规则应用于词法来源路径
960
+ pages-json.ts pages.json 编排:读-改-写、分包收敛、tabBar 占位、序列化格式化、
961
+ 文件锁与原子写入
962
+ pages-json-items.ts 数组适配器:多平台条目合并、#ifdef / #ifndef 包裹与输出顺序
963
+ pages-json-structure.ts 结构操作:首页前置与过期分包删除,注释随条目移动
964
+ pages-json-tokens.ts 注释令牌:行注释、条件指令查找与 #ifndef 包裹层清理
965
+ pages-snapshot.ts 生成快照:pages / subPackages / tabBar / globConfig / homePath 的值视图
966
+ platform-variants.ts 平台变体表:跨平台合并的共享规则,数组侧与对象侧两类适配器共用
967
+ tab-bar.ts tabBar 外观属性的跨平台合并与渲染(对象侧适配器)
968
+ jsonc-scan.ts 从 pages.json 原始文本里捞回 comment-json 解析时丢掉的重复键
969
+ files.ts 文件工具:页面扫描、路径解析、文件锁(withFileLock)、pages.json 合法性检查
970
+ macro.ts definePage 宏解析:SFC 解析、单个 script 块失败不影响别的块、
544
971
  对象 / 函数 / 异步函数三种形态求值
545
- condition.ts 按平台写配置 — define().ifdef()/.ifndef(),扫描时就算成当前平台的普通对象
546
- page.ts 页面实体 — 文件读取、宏求值、变更检测、跳过状态
547
- options.ts 选项解析 — 默认值合并、glob 目录解析、subPackages root 映射
972
+ condition.ts 按平台写配置:define().ifdef()/.ifndef(),扫描时就算成当前平台的普通对象
973
+ page.ts 页面实体:文件读取、宏求值、内容快照、变更检测与跳过状态
974
+ options.ts 选项解析:默认值合并与扫描规则保留
548
975
  declaration.ts uni-pages.d.ts 生成,为导航 API 提供路径类型检查
549
976
  config.ts defineUniPages 辅助函数 + 类型重导出
550
- constant.ts 常量 — 虚拟模块 ID、页面文件扩展名(vue / nvue / uvue)
977
+ constant.ts 常量:虚拟模块 ID、页面文件扩展名(vue / nvue / uvue)
551
978
  logger.ts 分类 debug 日志
552
979
  types.ts 公共类型定义(Options / UserOptions / ResolvedOptions)
553
980
  ```
@@ -556,33 +983,102 @@ types.ts 公共类型定义(Options / UserOptions / ResolvedOptions
556
983
 
557
984
  ```shell
558
985
  index.ts
559
- └─ context.ts(编排核心)
560
- ├─ options.ts 选项解析
561
- ├─ files.ts 扫描与文件锁
562
- ├─ page.ts ── macro.ts ── condition.ts
563
- ├─ pages-json.ts read-modify-write(内含文件锁)
564
- └─ declaration.ts
565
- pipeline.ts ── context.ts 测试与外部调用的流水线入口
986
+ ├─ context.ts(编排核心)
987
+ │ ├─ config-loader.ts 配置装载、产物身份与恢复输入
988
+ │ │ ├─ config-source.ts 源位置适配与运行时解析
989
+ │ │ └─ config-watch.ts 监听计划与配置事件判定(反向读取加载器路径工具)
990
+ │ ├─ options.ts ── scan-plan.ts 选项解析、目录发现与不可变扫描计划
991
+ │ │ ├─ directories.ts 目录来源、root 决议与扫描投影
992
+ │ │ └─ page-rules.ts 词法范围与排除规则
993
+ │ ├─ files.ts 扫描与文件锁
994
+ │ ├─ page.ts ── macro.ts ── condition.ts
995
+ │ ├─ page-sources.ts 来源身份、代表选择与生成路由冲突
996
+ │ ├─ page-watch.ts 页面内容订阅与恢复入口,消费扫描计划
997
+ │ │ ├─ page-watch-sources.ts 词法来源到物理目标的依赖发现
998
+ │ │ ├─ scan-plan.ts / page-rules.ts 扫描目录与词法范围
999
+ │ │ └─ page-watch-filter.ts Vite ignored 规则匹配
1000
+ │ ├─ pages-snapshot.ts 一次生成的只读结果视图
1001
+ │ ├─ pages-json.ts 文档编排,内含文件锁
1002
+ │ │ ├─ pages-json-items.ts ── pages-json-tokens.ts 数组适配器与注释令牌
1003
+ │ │ ├─ pages-json-structure.ts 首页前置与过期分包删除
1004
+ │ │ ├─ platform-variants.ts 平台变体表(两者按类型互相引用)
1005
+ │ │ └─ tab-bar.ts ── jsonc-scan.ts tabBar 属性合并与原文重复键扫描
1006
+ │ └─ declaration.ts
1007
+ ├─ config-loader.ts 插件工厂预检的路径归一化
1008
+ └─ files.ts pages.json 预检与路径解析
1009
+ pipeline.ts ── context.ts 测试与外部调用的流水线入口
1010
+ ```
1011
+
1012
+ ### 计划与快照
1013
+
1014
+ 生成流程里三处「先发现、后消费」的状态都是显式的值,不再靠 WeakMap、原地改写或调用方分步驱动:
1015
+
1016
+ - **配置监听计划**:`ConfigWatchIndex` 持有最近一次加载的文件、恢复目录与注册路径。`PagesConfigLoader` 只提供来源、依赖和恢复输入,`PageContext` 调 `buildConfigWatchPlan()` 后只消费 `match()` 与 `watchPaths()`,不再复制恢复状态。
1017
+ - **扫描计划**:每轮 `createScanPlan()` 产出一份 `ScanPlan`,包含主包目录、分包来源、最终 root 与编译规则。页面扫描、监听和目录事件判定读同一份值;`applyScanPlan()` 只把 `dirs`、`subPackages`、`subPackageRootMap` 同步回 `ResolvedOptions`,供既有调用方兼容读取。
1018
+ - **生成快照**:`generatePagesJSON()` 在写入前创建 `PagesSnapshot`,虚拟模块、路由字符串与声明文件都从快照取值;创建时复制数组与 tabBar 列表,后续工作字段的增删不会改变这次的结果。`Page.load()` 返回内容快照,`Page.getPageMeta()` 返回副本,合并阶段写入的首页标记不会留在读取缓存里。
1019
+
1020
+ 计划与快照都是模块内部的值,公开入口 `mergePagesJson()`、`writePagesJson()`、`updatePagesJSON()` 与虚拟模块形状保持不变。测试可以直接构造或断言这些值,不必通过 `PageContext` 的私有字段推断时序。
1021
+
1022
+ ### 脚本配置的加载流程
1023
+
1024
+ `PageContext` 通过 `PagesConfigLoader.load()` 取得配置对象及依赖路径,再扫描页面、合并元数据并写入 `pages.json`。加载器使用 unconfig 按来源顺序发现并加载首个有效配置:内置 parser 直接解析 JSON 内容,需要执行 JavaScript / TypeScript 时调用 `loadScript()`;自定义 parser 则由调用方负责读取和解析。
1025
+
1026
+ `loadScript()` 接收原入口路径和经过可选 transform 处理的入口文本。它通过 `bundle-require` 调用 esbuild 打包并执行脚本,过程如下:
1027
+
1028
+ 1. 在 `node_modules/.cache/uni-pages/` 下创建这一轮加载独占的 `load-*` 目录,并写入公共辅助模块 `runtime.mjs`。
1029
+ 2. esbuild 解析入口和静态依赖。`onResolve` 按导入位置解析目标,并记录缺失导入的恢复候选;`onLoad` 读取模块,将需要转换的脚本文本与原文件路径交给 `transformConfigSource()`。
1030
+ 3. `transformConfigSource()` 在内存中为每个模块注入绑定原文件位置的 `require`、`import.meta` 和动态导入函数,保留可由 esbuild 打包的调用,并生成源码映射。
1031
+ 4. esbuild 将转换后的配置模块和所需辅助代码打包为 `config.mjs`,`bundle-require` 加载执行它;加载器提取配置对象,并记录参与打包的本地依赖。
1032
+ 5. 成功或失败都由加载器统一尝试删除本轮 `load-*` 目录,包括打包执行文件;删除失败只记录调试日志,不影响配置结果或原始错误。加载失败时保留已尝试的源路径及适用的缺失导入候选,供后续文件事件触发重试。
1033
+
1034
+ 临时目录中的文件由加载器管理:
1035
+
1036
+ ```text
1037
+ node_modules/.cache/uni-pages/
1038
+ └── load-<随机后缀>/
1039
+ ├── runtime.mjs
1040
+ └── config.mjs
1041
+ ```
1042
+
1043
+ | 文件 | 内容来源 | 用途 |
1044
+ | --- | --- | --- |
1045
+ | `runtime.mjs` | `configRuntimeSource()` 返回的 JavaScript 源码 | 提供 `createRequire`、`createMeta`、`importFrom` 等公共助手;各源模块通过注入的调用参数提供自己的路径 |
1046
+ | `config.mjs` | esbuild 汇总入口文本、打包的依赖及辅助代码生成 | 由 Node 执行并导出配置对象;外部依赖保留为导入,运行时动态目标由助手解析后加载 |
1047
+
1048
+ 例如,`config/global-style.ts` 中的 `import(target)` 会被转换成效果相当于 `importFrom(target, 'file:///app/config/global-style.ts')` 的调用。`importFrom` 使用 `import-meta-resolve` 将目标解析成 URL,再调用 Node 的 `import()`。`require(target)` 则使用 Node 的 `createRequire(原文件路径)`。每个模块分别携带自己的源位置,执行文件所在的临时目录不会改变这些调用的解析基准。
1049
+
1050
+ ```text
1051
+ 配置入口文本、静态依赖 ── 内存转换 ──┐
1052
+ ├── esbuild ── config.mjs ── 配置对象
1053
+ 插件生成的 runtime.mjs ────────────┘ │
1054
+ └── PageContext 合并、生成 pages.json
566
1055
  ```
567
1056
 
1057
+ 这里的 `.cache/uni-pages` 用作临时工作目录,每次加载都会生成新的产物;本轮子目录清理后,父目录可以保留。清理失败时残留的 `load-*` 目录不会被输入索引或事件过滤当作配置输入,可以安全删除。源码转换不修改项目文件,独占目录也使并发加载的产物互不覆盖。配置输入索引和事件过滤按这个目录排除内部产物,避免临时文件的写入和删除触发配置重载。
1058
+
1059
+ 这些辅助文件用于 Node 侧的配置执行。业务代码导入的 `virtual:uni-pages` 由 Vite 的 `resolveId` / `load` 钩子提供,内容来自 `PageContext` 汇总的页面元数据。
1060
+
568
1061
  ### 插件生命周期
569
1062
 
570
1063
  插件通过 Vite 的生命周期钩子驱动,顺序如下:
571
1064
 
572
- 1. **插件工厂调用**(同步)— 预检 `pages.json` 是否存在且合法(此时 `config.root` 未知,回退到 `VITE_ROOT_DIR` / `process.cwd()` 解析路径)
573
- 2. **`configResolved`**(异步)— 创建 `PageContext`,检测是否与 `vite-plugin-uni-platform` 协同,执行首次 `updatePagesJSON()` 生成 pages.json;`build --watch` 模式下另建 chokidar 监听页面目录
574
- 3. **`configureServer`**(dev)— 复用 Vite 的 `server.watcher`,把配置文件源(`pages.config.ts` 等)加入监听;变更时重跑完整流水线,失效虚拟模块并通知浏览器 full-reload
575
- 4. **`transform`** — 从 vue / nvue / uvue 文件中移除 `definePage` 宏调用,避免运行时报错
576
- 5. **`resolveId` / `load`** — 提供 `virtual:uni-pages` 虚拟模块,暴露所有页面元数据
1065
+ 1. **插件工厂调用**(同步):预检 `pages.json` 是否存在且合法(此时 `config.root` 未知,回退到 `VITE_ROOT_DIR` / `process.cwd()` 解析路径)
1066
+ 2. **`configResolved`**(异步):占用当前实例,创建 `PageContext` 或接管匹配的 `prepare()` 结果,检测是否与 `vite-plugin-uni-platform` 协同,完成首次 pages.json 生成;`build --watch` 模式下另建 watcher,接收配置文件事件与页面目录结构通知,同时启动自有页面监听器
1067
+ 3. **`configureServer`**(dev):从 Vite 的 `server.watcher` 接收配置监听计划命中与页面目录结构通知,并按 Vite 的监听开关、忽略规则和轮询设置启动自有页面监听器;产物更新后失效虚拟模块并通知浏览器 full-reload
1068
+ 4. **`options`**(build):将本插件的虚拟模块与关闭钩子绑定到当前会话,供 Rollup 构建使用;watcher 各轮沿用该绑定,已关闭构建的迟到钩子及已排队轮次仍属于原会话
1069
+ 5. **`transform`**:从 vue / nvue / uvue 文件中移除 `definePage` 宏调用,避免运行时报错
1070
+ 6. **`resolveId` / `load`**:提供 `virtual:uni-pages` 虚拟模块,暴露所有页面元数据
1071
+ 7. **`closeWatcher` / `closeBundle`**:普通构建和 dev 的 `closeBundle`、构建监听的 `closeWatcher` 结束会话;构建监听每轮 `closeBundle` 保留会话。结束时解绑共享 watcher 回调、关闭自有 watcher 并排空已开始的生成,完成后释放实例占用和当前上下文;共享 watcher 由 Vite 关闭
577
1072
 
578
1073
  ### 关键设计决策
579
1074
 
580
- - **统一入口**:`PageContext.updatePagesJSON()` 是唯一的完整流水线入口,阶段顺序封装其内,调用方(Vite 钩子、watcher 回调、测试)无需知道内部阶段。
1075
+ - **实例边界**:每个插件实例只持有一个活跃会话,重叠占用在初始化前拒绝,虚拟模块与监听始终使用该会话的上下文。顺序复用创建新上下文;已成功接受的显式准备参数独立保存,跨会话持续校验。
1076
+ - **串行生成**:公开方法 `PageContext.updatePagesJSON(filepath?)` 与内部页面事件批次共用串行生成队列,配置加载、扫描、合并和写入的阶段顺序集中维护。传入文件路径时尝试增量读取,省略时执行完整刷新。
581
1077
  - **read-modify-write + 生成标记**:平台状态保存在 `pages.json` 自身(`// GENERATED BY UNI-PAGES, PLATFORM: ...` 标记),无需外部缓存文件;无标记的条目视为用户手写,插件永不修改。过期生成物基于该标记自动清理。
582
1078
  - **文件锁 + 原子写入**:整个「读 → 合并 → 写」在同一个 `withFileLock` 内执行,写入走 tmp + rename,多终端并发(如同时跑 mp-weixin 和 mp-alipay)不会互相覆盖或读到写了一半的内容。
583
1079
  - **内容不变就不写**:写入前先和磁盘上的内容比一遍,没变化就跳过,避免触发下游不必要的重编译。
584
- - **统一监听**:配置文件与页面文件共用同一个 chokidar(dev 下直接复用 Vite 的 watcher,不额外创建);配置变更不做增量,直接重跑完整流水线,配合「内容不变就不写」避免多余的磁盘写入。
585
- - **增量页面更新**:页面文件变更时先经 `hasChanged()` 检测页面配置是否真的变化,无变化则跳过整轮流水线。
1080
+ - **监听职责**:配置事件来自 Vite 的共享 watcher 或 `build --watch` 中创建的 watcher,触发完整生成;其中有效页面目录的结构通知也会触发补扫。`PageSourceWatch` 从声明目录发现顶层和嵌套链接,统一处理物理目标的页面文件事件,并管理非递归恢复入口。发现过程不依赖已收录页面,因此空目录和悬空链接也能恢复;原生目录通知补充 chokidar 不投递 `add` 的悬空链接创建。按当前全部来源汇总物理订阅,来源或目录身份变化时重新建立订阅,避免复用旧目标句柄;关闭后不再发现或订阅。
1081
+ - **增量页面更新**:普通内容变更只重读相关物理文件的缓存候选,元数据未变时跳过配置加载与产物生成。页面事件按待处理批次合并;批次内含新增、删除、目标切换或恢复等结构变化时执行完整刷新,生成期间到达的事件留给下一轮。
586
1082
  - **宏解析失败隔离**:每个 `<script>` 块独立解析,单块语法错误(如 Babel 8 已移除的 `assert` import attributes)只影响该块,不会阻断其它块的宏移除。
587
1083
  - **无导入时副作用**:所有文件系统操作都在插件生命周期内执行,而非模块导入时,模块可独立测试。
588
1084
  - **可从外部传入的入口**:`pipeline.ts` 的 `root` / `platform` 从外部传入,测试不绑定进程环境变量,平台相关快照结果确定。