@uni-helper/vite-plugin-uni-pages 0.4.8 → 0.5.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.
package/dist/index.mjs CHANGED
@@ -1,69 +1,65 @@
1
1
  import { createRequire } from "node:module";
2
2
  import path, { dirname, extname, join, resolve } from "node:path";
3
3
  import process from "node:process";
4
- import { babelParse, isCallOf } from "ast-kit";
5
4
  import chokidar from "chokidar";
6
5
  import MagicString from "magic-string";
7
6
  import { createLogger, normalizePath } from "vite";
8
- import fs, { existsSync } from "node:fs";
9
7
  import { platform } from "@uni-helper/uni-env";
10
8
  import { CommentArray, parse, stringify } from "comment-json";
11
9
  import Debug from "debug";
12
- import detectIndent from "detect-indent";
13
- import detectNewline from "detect-newline";
10
+ import groupBy from "lodash.groupby";
14
11
  import { loadConfig } from "unconfig";
15
- import writeFileAtomic from "write-file-atomic";
12
+ import fs, { existsSync } from "node:fs";
16
13
  import { mkdir, readFile, writeFile } from "node:fs/promises";
17
14
  import lockfile from "proper-lockfile";
18
15
  import { globSync } from "tinyglobby";
19
16
  import vm from "node:vm";
20
- import babelGenerator from "@babel/generator";
21
- import groupBy from "lodash.groupby";
22
- import * as ts from "typescript";
17
+ import babelGenerate from "@babel/generator";
23
18
  import * as t from "@babel/types";
24
19
  import { parse as parse$1 } from "@vue/compiler-sfc";
20
+ import { babelParse, isCallOf } from "ast-kit";
21
+ import * as ts from "typescript";
22
+ import writeFileAtomic from "write-file-atomic";
25
23
  //#region src/constant.ts
26
- /** Virtual module identifier for Vite module resolution */
24
+ /** Vite 模块解析用的虚拟模块标识 */
27
25
  const MODULE_ID_VIRTUAL = "virtual:uni-pages";
28
- /** Resolved virtual module identifier with \0 prefix to avoid conflicts with other modules */
26
+ /** 加 \0 前缀解析后的虚拟模块标识,避免与其他模块冲突 */
29
27
  const RESOLVED_MODULE_ID_VIRTUAL = `\0${MODULE_ID_VIRTUAL}`;
30
- /** Output filename, the uni-app page configuration file */
28
+ /** 输出文件名,即 uni-app 的页面配置文件 */
31
29
  const OUTPUT_NAME = "pages.json";
32
- /** Supported page file extensions */
30
+ /** 支持的页面文件扩展名 */
33
31
  const FILE_EXTENSIONS = [
34
32
  "vue",
35
33
  "nvue",
36
34
  "uvue"
37
35
  ];
38
36
  //#endregion
39
- //#region ../../node_modules/.pnpm/@antfu+utils@9.3.0/node_modules/@antfu/utils/dist/index.mjs
40
- function slash(str) {
41
- return str.replace(/\\/g, "/");
42
- }
43
- //#endregion
44
37
  //#region src/declaration.ts
45
38
  /**
46
- * Generate TypeScript declaration file content
47
- * Provide type hints for uni-app navigation APIs (navigateTo, redirectTo, switchTab, reLaunch)
48
- * Ensure page paths get type checking at compile time
39
+ * 生成 TypeScript 声明文件内容
40
+ * 为 uni-app 的导航 API(navigateTo、redirectTo、switchTab、reLaunch)提供类型提示,
41
+ * 让页面路径在编译期获得类型检查
49
42
  *
50
- * @param ctx - Page context instance
51
- * @returns Declaration file code string
43
+ * @param input - 合并后的路由数据
44
+ * @returns 声明文件代码字符串
52
45
  */
53
- function getDeclaration(ctx) {
54
- const subPagesPath = ctx.subPageMetaData.map((sub) => {
46
+ function getDeclaration(input) {
47
+ const subPagePaths = input.subPackages.map((sub) => {
55
48
  return sub.pages.map((v) => `"/${normalizePath(join(sub.root, v.path))}"`);
56
49
  }).flat();
57
- const tabsPagesPath = ctx.pagesGlobConfig?.tabBar?.list?.map((v) => {
58
- return `"/${v.pagePath}"`;
59
- }) ?? [];
50
+ const tabBarRawPaths = input.tabBar?.list?.map((v) => v.pagePath) ?? input.globConfig?.tabBar?.list?.map((v) => v.pagePath) ?? [];
51
+ const tabBarPathSet = new Set(tabBarRawPaths);
52
+ const allPagePaths = [...input.pages.filter((page) => !tabBarPathSet.has(page.path)).map((v) => `"/${v.path}"`), ...subPagePaths];
53
+ const tabBarPagePaths = tabBarRawPaths.map((p) => `"/${p}"`);
60
54
  return `/* eslint-disable */
61
55
  /* prettier-ignore */
56
+ /* oxlint-disable */
57
+ /* oxfmt-ignore */
62
58
  // @ts-nocheck
63
59
  // Generated by vite-plugin-uni-pages
64
60
 
65
61
  type _LocationUrl =
66
- ${[...ctx.pageMetaData.filter((page) => !tabsPagesPath.includes(page.path)).map((v) => `"/${v.path}"`), ...subPagesPath].join(" |\n ")};
62
+ ${allPagePaths.length > 0 ? allPagePaths.join(" |\n ") : "string"};
67
63
 
68
64
  interface NavigateToOptions {
69
65
  url: _LocationUrl;
@@ -71,7 +67,7 @@ interface NavigateToOptions {
71
67
  interface RedirectToOptions extends NavigateToOptions {}
72
68
 
73
69
  interface SwitchTabOptions {
74
- ${tabsPagesPath.length ? `url: ${tabsPagesPath.join(" | ")}` : ""}
70
+ ${tabBarPagePaths.length ? `url: ${tabBarPagePaths.join(" | ")}` : ""}
75
71
  }
76
72
 
77
73
  type ReLaunchOptions = NavigateToOptions | SwitchTabOptions;
@@ -89,327 +85,192 @@ declare module "virtual:uni-pages" {
89
85
  `;
90
86
  }
91
87
  /**
92
- * Write file, automatically create directories
93
- * @param filePath - File path
94
- * @param content - File content
88
+ * 写入文件,自动创建所在目录
89
+ * @param filePath - 文件路径
90
+ * @param content - 文件内容
95
91
  */
96
92
  async function writeFile$1(filePath, content) {
97
93
  await mkdir(dirname(filePath), { recursive: true });
98
94
  return await writeFile(filePath, content, "utf-8");
99
95
  }
100
96
  /**
101
- * Write declaration file to disk
102
- * Only writes when content changes to avoid unnecessary file operations
97
+ * 将声明文件写入磁盘
98
+ * 仅在内容变化时写入,避免不必要的文件操作
103
99
  *
104
- * @param ctx - Page context instance
105
- * @param filepath - Declaration file output path
100
+ * @param input - 合并后的路由数据
101
+ * @param filepath - 声明文件输出路径
106
102
  */
107
- async function writeDeclaration(ctx, filepath) {
103
+ async function writeDeclaration(input, filepath) {
108
104
  const originalContent = existsSync(filepath) ? await readFile(filepath, "utf-8") : "";
109
- const code = getDeclaration(ctx);
105
+ const code = getDeclaration(input);
110
106
  if (!code) return;
111
107
  if (code !== originalContent) await writeFile$1(filepath, code);
112
108
  }
113
109
  //#endregion
114
- //#region src/utils.ts
115
- /**
116
- * Invalidate virtual module to trigger HMR update
117
- * When page configuration changes, the virtual module needs to be invalidated to regenerate content
118
- *
119
- * @param server - Vite dev server instance
120
- */
121
- function invalidatePagesModule(server) {
122
- const { moduleGraph } = server;
123
- const mods = moduleGraph.getModulesByFile(RESOLVED_MODULE_ID_VIRTUAL);
124
- if (mods) {
125
- const seen = /* @__PURE__ */ new Set();
126
- mods.forEach((mod) => {
127
- moduleGraph.invalidateModule(mod, seen);
128
- });
129
- }
130
- }
110
+ //#region src/logger.ts
131
111
  /**
132
- * Debug logging utility
133
- * Uses debug package for leveled log output
134
- * Enable via environment variable DEBUG=vite-plugin-uni-pages:*
112
+ * 调试日志工具
113
+ * 基于 debug 包做分级日志输出
114
+ * 通过环境变量 DEBUG=vite-plugin-uni-pages:* 开启
135
115
  */
136
116
  const debug = {
137
- /** HMR related logs */
117
+ /** HMR 相关日志 */
138
118
  hmr: Debug("vite-plugin-uni-pages:hmr"),
139
- /** Configuration options related logs */
119
+ /** 配置项相关日志 */
140
120
  options: Debug("vite-plugin-uni-pages:options"),
141
- /** Main package page scanning logs */
121
+ /** 主包页面扫描日志 */
142
122
  pages: Debug("vite-plugin-uni-pages:pages"),
143
- /** Sub-package page scanning logs */
123
+ /** 子包页面扫描日志 */
144
124
  subPages: Debug("vite-plugin-uni-pages:subPages"),
145
- /** Error logs */
125
+ /** 错误日志 */
146
126
  error: Debug("vite-plugin-uni-pages:error"),
147
- /** Cache related logs */
127
+ /** 缓存相关日志 */
148
128
  cache: Debug("vite-plugin-uni-pages:cache"),
149
- /** Declaration file generation logs */
129
+ /** 声明文件生成日志 */
150
130
  declaration: Debug("vite-plugin-uni-pages:declaration"),
151
- /** definePage macro parsing logs */
131
+ /** definePage 宏解析日志 */
152
132
  definePage: Debug("vite-plugin-uni-pages:definePage")
153
133
  };
134
+ //#endregion
135
+ //#region src/files.ts
154
136
  /**
155
- * Convert file extensions array to glob pattern
156
- * @param extensions - File extensions array
157
- * @returns Glob pattern string
137
+ * 解析给定上下文下可作为有效页面的文件列表。
158
138
  */
159
- function extsToGlob(extensions) {
160
- return extensions.length > 1 ? `{${extensions.join(",")}}` : extensions[0] || "";
139
+ function getPageFiles(path, options) {
140
+ const { exclude } = options;
141
+ const ext = FILE_EXTENSIONS.length > 1 ? `{${FILE_EXTENSIONS.join(",")}}` : FILE_EXTENSIONS[0] || "";
142
+ return globSync(`**/*.${ext}`, {
143
+ ignore: exclude,
144
+ onlyFiles: true,
145
+ cwd: path
146
+ });
161
147
  }
162
148
  /**
163
- * Check if file is a target page file
164
- * Determine if the file extension is in the supported page file types list
149
+ * 检查文件是否为目标页面文件
150
+ * 判断文件扩展名是否在支持的页面文件类型列表中
165
151
  *
166
- * @param path - File path
167
- * @returns Whether it's a target file
152
+ * @param path - 文件路径
153
+ * @returns 是否为目标文件
168
154
  */
169
155
  function isTargetFile(path) {
170
156
  const ext = path.split(".").pop();
171
157
  return FILE_EXTENSIONS.includes(ext);
172
158
  }
173
159
  /**
174
- * merge page meta data array by path and assign style
175
- * @param pageMetaData page meta data array
176
- * TODO: support merge middleware
177
- */
178
- function mergePageMetaDataArray(pageMetaData) {
179
- const pageMetaDataObj = groupBy(pageMetaData, "path");
180
- const result = [];
181
- for (const path in pageMetaDataObj) {
182
- const _pageMetaData = pageMetaDataObj[path];
183
- const options = _pageMetaData[0];
184
- for (const page of _pageMetaData) {
185
- options.style = Object.assign(options.style ?? {}, page.style ?? {});
186
- Object.assign(options, page);
187
- }
188
- result.push(options);
189
- }
190
- return result;
191
- }
192
- /**
193
- * Convert TypeScript / JavaScript script code to object/function
160
+ * 根据项目根目录与输出目录解析 pages.json 文件路径
194
161
  *
195
- * @param options - Configuration required for script execution
196
- * @param options.imports - List of module import statements to include
197
- * @param options.code - TypeScript code content to execute
198
- * @param options.filename - Script filename for error location and context
199
- * @returns Script execution result, if export is a function then execute and return its return value
200
- */
201
- async function parseCode(options) {
202
- const { imports = [], code, filename } = options;
203
- let jsCode = "";
204
- try {
205
- const tmpCode = `${imports.join("\n")}\n export default ${code}`;
206
- jsCode = ts.transpileModule(tmpCode, {
207
- compilerOptions: {
208
- module: ts.ModuleKind.CommonJS,
209
- target: ts.ScriptTarget.ES2022,
210
- noEmit: true,
211
- strict: false,
212
- noImplicitAny: false,
213
- strictNullChecks: false,
214
- strictFunctionTypes: false,
215
- strictBindCallApply: false,
216
- strictPropertyInitialization: false,
217
- noImplicitThis: false,
218
- alwaysStrict: false,
219
- allowJs: true,
220
- checkJs: false,
221
- skipLibCheck: true,
222
- esModuleInterop: true,
223
- removeComments: true
224
- },
225
- jsDocParsingMode: ts.JSDocParsingMode.ParseNone
226
- }).outputText;
227
- const dir = path.dirname(filename);
228
- const vmContext = {
229
- module: {},
230
- exports: {},
231
- __filename: filename,
232
- __dirname: dir,
233
- require: createRequire(dir),
234
- import: (id) => import(id),
235
- setTimeout,
236
- clearTimeout,
237
- setInterval,
238
- clearInterval,
239
- setImmediate,
240
- clearImmediate,
241
- console,
242
- URL,
243
- URLSearchParams,
244
- performance,
245
- global: globalThis,
246
- globalThis
247
- };
248
- await new vm.Script(jsCode, { filename }).runInNewContext(vmContext, { timeout: 1e3 });
249
- return vmContext.exports.default || vmContext.exports;
250
- } catch (error) {
251
- throw new Error(`EXEC SCRIPT FAIL IN ${filename}: ${error.message} \n\n${jsCode}\n\n`);
252
- }
253
- }
254
- /**
255
- * Async sleep function
256
- * @param ms - Sleep duration in milliseconds
257
- * @returns Promise object
162
+ * @param root - 项目根目录
163
+ * @param outDir - 相对 root 的 pages.json 输出目录
164
+ * @returns pages.json 绝对路径
258
165
  */
259
- function sleep(ms) {
260
- return new Promise((resolve) => setTimeout(resolve, ms));
166
+ function resolvePagesJsonPath(root, outDir) {
167
+ return path.join(root, outDir, OUTPUT_NAME);
261
168
  }
262
169
  /**
263
- * Get default export of a module
264
- * Compatible with CommonJS and ES Module export styles
170
+ * 确保这个路径上有一个能用的 pages.json 文件。
265
171
  *
266
- * @param expr - Module object
267
- * @returns Default export value
268
- */
269
- function getDefaultExport(expr) {
270
- return expr.default === void 0 ? expr : expr.default;
271
- }
272
- /** Babel code generator for converting AST back to code */
273
- const babelGenerate = getDefaultExport(babelGenerator);
274
- //#endregion
275
- //#region src/files.ts
276
- /**
277
- * Resolves the files that are valid pages for the given context.
278
- */
279
- function getPageFiles(path, options) {
280
- const { exclude } = options;
281
- return globSync(`**/*.${extsToGlob(FILE_EXTENSIONS)}`, {
282
- ignore: exclude,
283
- onlyFiles: true,
284
- cwd: path
285
- });
286
- }
287
- /**
288
- * Check the pages.json file at the specified path, create an empty pages.json file if it doesn't exist or is not a valid file
289
- * @param path - File path to check
290
- * @returns boolean - Whether the operation was successful
172
+ * 文件不存在时创建一个占位文件,占位内容很快会被下一次生成覆盖。
173
+ * 已存在的文件绝不删除重建:删它会悄悄弄丢手写的内容(只读的文件
174
+ * 也能被删除,只要它所在的目录可写),所以"路径存在但不是普通文件、
175
+ * 或者读不了也写不了"都算必须由用户自己修的硬错误(例如
176
+ * `chmod u+w`)。
177
+ *
178
+ * @param path - 待检查的文件路径
179
+ * @throws 路径存在但不是普通文件、缺少读写权限,或占位文件创建失败时
180
+ * 抛出
291
181
  */
292
182
  function checkPagesJsonFileSync(path) {
293
- /**
294
- * Create an empty pages.json file
295
- * @param path - File path
296
- * @returns boolean - Whether the creation was successful
297
- */
298
- const createEmptyFile = (path) => {
299
- try {
300
- fs.writeFileSync(path, JSON.stringify({ pages: [{ path: "" }] }, null, 2), { encoding: "utf-8" });
301
- return true;
302
- } catch {
303
- return false;
304
- }
305
- };
306
- /**
307
- * Delete the file at the specified path
308
- * @param path - File path
309
- * @returns boolean - Whether the deletion was successful
310
- */
311
- const unlinkFile = (path) => {
312
- try {
313
- fs.unlinkSync(path);
314
- return true;
315
- } catch {
316
- return false;
317
- }
318
- };
319
183
  try {
320
- try {
321
- fs.accessSync(path, fs.constants.F_OK);
322
- } catch {
323
- return createEmptyFile(path);
324
- }
325
- if (!fs.statSync(path).isFile()) {
326
- if (!unlinkFile(path)) return false;
327
- return createEmptyFile(path);
328
- }
329
- try {
330
- fs.accessSync(path, fs.constants.R_OK | fs.constants.W_OK);
331
- return true;
332
- } catch {
333
- if (!unlinkFile(path)) return false;
334
- return createEmptyFile(path);
335
- }
184
+ fs.accessSync(path, fs.constants.F_OK);
185
+ } catch {
186
+ fs.writeFileSync(path, JSON.stringify({ pages: [{ path: "" }] }, null, 2), { encoding: "utf-8" });
187
+ return;
188
+ }
189
+ if (!fs.statSync(path).isFile()) throw new Error(`[vite-plugin-uni-pages] ${path} exists but is not a regular file. Remove it manually if it is not needed; the plugin refuses to delete or overwrite it.`);
190
+ try {
191
+ fs.accessSync(path, fs.constants.R_OK | fs.constants.W_OK);
336
192
  } catch {
337
- return createEmptyFile(path);
193
+ throw new Error(`[vite-plugin-uni-pages] ${path} is not readable and writable. The plugin rewrites pages.json on every run; fix the file permissions (e.g. chmod u+w) or remove the read-only flag. The plugin refuses to delete and recreate the file because that would lose its content.`);
338
194
  }
339
195
  }
340
196
  /**
341
- * Run a task while holding an exclusive file lock.
197
+ * 同一个进程里按文件路径排的等待队列(先来后到)。
342
198
  *
343
- * Unlike {@link writeFileWithLock}, this protects the whole read-modify-write
344
- * critical section. The lock is held from the moment `task` starts until it
345
- * resolves, so concurrent processes cannot observe or overwrite a half-written
346
- * state. This is required by pages.json generation, where the new content
347
- * depends on the current content (other platforms' `#ifdef` blocks).
199
+ * 同进程的调用方先在这里排队,不直接去抢 OS 锁:抢锁靠不断重试,
200
+ * 可能有人运气差总抢不到(比如 dev server 启动时监听器 `add` 事件
201
+ * 一拥而上)。排队保证人人都轮得到。下面的 OS 锁负责挡住其他进程。
202
+ */
203
+ const lockQueues = /* @__PURE__ */ new Map();
204
+ /**
205
+ * 拿到文件的独占锁后运行任务,运行完自动放锁。
206
+ *
207
+ * 它保护"读文件 → 算新内容 → 写回"的整个过程:锁从 `task` 开始
208
+ * 一直拿到它结束,别的进程看不到、也覆盖不了写了一半的文件。
209
+ * pages.json 的生成靠这一点:新内容要根据当前文件来算(里面有其他
210
+ * 平台的 `#ifdef` 块)。
211
+ *
212
+ * 同进程的调用方先在上面那条按路径的队列里排队,再碰 OS 锁;下面的
213
+ * 重试只用来等其他进程放锁。
348
214
  *
349
- * @param path - File path used as the lock target
350
- * @param task - Async work to run inside the lock; return value is forwarded
351
- * @param retry - Number of retries when lock acquisition fails, defaults to 3
352
- * @returns The value resolved by `task`, or `undefined` if the lock could not be acquired
215
+ * @param path - 要加锁的文件路径
216
+ * @param task - 在锁内运行的异步任务;返回值原样返回给调用方
217
+ * @param retry - 锁被别人拿着时的重试次数,默认 3
218
+ * @returns `task` 的结果;一直拿不到锁时为 `undefined`
353
219
  */
354
- async function withFileLock(path, task, retry = 3) {
355
- if (retry <= 0) {
356
- debug.error(`${path} Failed to acquire file lock, task aborted`);
357
- return;
358
- }
359
- let release;
360
- try {
220
+ function withFileLock(path, task, retry = 3) {
221
+ const current = (lockQueues.get(path) ?? Promise.resolve()).catch(() => {}).then(() => acquireAndRun(path, task, retry));
222
+ lockQueues.set(path, current);
223
+ return current;
224
+ }
225
+ /** 去拿 OS 级的锁(拿不到就等一会儿再试,防的是其他进程),拿到后运行任务 */
226
+ async function acquireAndRun(path, task, retry) {
227
+ for (let attempt = retry; attempt > 0; attempt--) {
228
+ let release;
361
229
  try {
362
230
  release = await lockfile.lock(path, { realpath: false });
363
231
  } catch {
364
232
  await sleep(500);
365
- return withFileLock(path, task, retry - 1);
233
+ continue;
234
+ }
235
+ try {
236
+ return await task();
237
+ } finally {
238
+ await release();
366
239
  }
367
- return await task();
368
- } finally {
369
- if (release) await release();
370
240
  }
241
+ debug.error(`${path} Failed to acquire file lock, task aborted`);
371
242
  }
372
243
  /**
373
- * Safely write file using file lock
374
- * Avoid data corruption caused by concurrent writes through file lock
375
- * Use atomic write to ensure file write integrity
376
- *
377
- * Note: this only makes the *write* atomic. Callers that need to read the
378
- * current content before computing the new one must use {@link withFileLock}
379
- * so the read and write belong to the same critical section.
380
- *
381
- * @param path - File path
382
- * @param content - File content
383
- * @param retry - Number of retries when lock acquisition fails, defaults to 3
244
+ * 异步休眠函数
245
+ * @param ms - 休眠毫秒数
246
+ * @returns 指定延时后 resolve 的 Promise
384
247
  */
385
- async function writeFileWithLock(path, content, retry = 3) {
386
- return withFileLock(path, async () => {
387
- await writeFileAtomic(path, content);
388
- }, retry);
248
+ function sleep(ms) {
249
+ return new Promise((resolve) => setTimeout(resolve, ms));
389
250
  }
390
251
  //#endregion
391
252
  //#region src/options.ts
392
253
  /**
393
- * Resolve user configuration options
394
- * Merge user-provided configuration with default values and handle path resolution
254
+ * 解析用户配置项
255
+ * 将用户提供的配置与默认值合并,并处理路径解析
395
256
  *
396
- * @param userOptions - User configuration options
397
- * @param viteRoot - Vite project root directory
398
- * @returns Resolved configuration options
257
+ * @param userOptions - 用户配置项
258
+ * @param viteRoot - Vite 项目根目录
259
+ * @returns 解析后的配置项
399
260
  */
400
261
  function resolveOptions(userOptions, viteRoot = process.cwd()) {
401
262
  const { dts = true, configSource = "pages.config", homePage = ["pages/index", "pages/index/index"], mergePages = true, dir = "src/pages", subPackages = [], outDir = "src", exclude = [
402
263
  "node_modules",
403
264
  ".git",
404
265
  "**/__*__/**"
405
- ], minify = false, debug = false, onBeforeLoadUserConfig = () => {}, onAfterLoadUserConfig = () => {}, onBeforeScanPages = () => {}, onAfterScanPages = () => {}, onBeforeMergePageMetaData = () => {}, onAfterMergePageMetaData = () => {}, onBeforeWriteFile = () => {}, onAfterWriteFile = () => {} } = userOptions;
406
- const root = viteRoot || slash(process.env.VITE_ROOT_DIR || process.cwd());
266
+ ], minify = false, insertFinalNewline = false, indent = 2, eol = "\n", debug = false, onBeforeLoadUserConfig = () => {}, onAfterLoadUserConfig = () => {}, onBeforeScanPages = () => {}, onAfterScanPages = () => {}, onBeforeMergePageMetaData = () => {}, onAfterMergePageMetaData = () => {}, onBeforeWriteFile = () => {}, onAfterWriteFile = () => {} } = userOptions;
267
+ const root = viteRoot || normalizePath(process.env.VITE_ROOT_DIR || process.cwd());
407
268
  const resolvedDirs = resolvePageDirs(dir, root, exclude);
408
269
  const subPackageRootMap = /* @__PURE__ */ new Map();
409
270
  const resolvedSubDirs = [];
410
- for (const sub of subPackages) if (typeof sub === "string") resolvedSubDirs.push(slash(sub));
271
+ for (const sub of subPackages) if (typeof sub === "string") resolvedSubDirs.push(normalizePath(sub));
411
272
  else {
412
- const dirPath = slash(sub.dir);
273
+ const dirPath = normalizePath(sub.dir);
413
274
  resolvedSubDirs.push(dirPath);
414
275
  subPackageRootMap.set(dirPath, sub.root);
415
276
  }
@@ -427,6 +288,9 @@ function resolveOptions(userOptions, viteRoot = process.cwd()) {
427
288
  exclude,
428
289
  root,
429
290
  minify,
291
+ insertFinalNewline,
292
+ indent,
293
+ eol,
430
294
  debug,
431
295
  onBeforeLoadUserConfig,
432
296
  onAfterLoadUserConfig,
@@ -439,10 +303,14 @@ function resolveOptions(userOptions, viteRoot = process.cwd()) {
439
303
  };
440
304
  }
441
305
  /**
442
- * Resolves the page dirs for its for its given globs
306
+ * 根据给定的 glob 模式解析页面目录
307
+ * @param dir - 页面目录 glob 模式
308
+ * @param root - 项目根目录
309
+ * @param exclude - 需要排除的 glob 模式
310
+ * @returns 匹配到的目录路径
443
311
  */
444
312
  function resolvePageDirs(dir, root, exclude) {
445
- return globSync(slash(dir), {
313
+ return globSync(normalizePath(dir), {
446
314
  ignore: exclude,
447
315
  onlyDirectories: true,
448
316
  expandDirectories: false,
@@ -451,115 +319,118 @@ function resolvePageDirs(dir, root, exclude) {
451
319
  });
452
320
  }
453
321
  //#endregion
454
- //#region src/page.ts
322
+ //#region src/condition.ts
323
+ /** 判断一个值是否为普通对象(非 null、非数组) */
324
+ function isPlainObject(val) {
325
+ return typeof val === "object" && val !== null && !Array.isArray(val);
326
+ }
455
327
  /**
456
- * Page class representing a Vue page file
328
+ * 把 `partial` 合并进 `base`,两边都不改动
457
329
  *
458
- * Responsibilities:
459
- * 1. Read page file content
460
- * 2. Parse page metadata defined by definePage macro
461
- * 3. Provide tabBar configuration information
462
- * 4. Track page file change status
330
+ * 对象一层层往里合并;数组和普通值直接整个替换。规则和
331
+ * @uni-ku/pages-json 保持一致。
463
332
  */
464
- var Page = class {
465
- /** Page context instance */
466
- ctx;
467
- /** Page path information containing relative and absolute paths */
468
- path;
469
- /** Page URI used for pages.json path field */
470
- uri;
471
- /** Whether the page has changed, used for incremental update judgment */
472
- changed = true;
473
- /** Raw JSON string of page metadata for change detection */
474
- raw = "";
475
- /** Parsed page metadata */
476
- meta;
477
- /**
478
- * Create a page instance
479
- * @param ctx - Page context instance
480
- * @param path - Page path information
481
- */
482
- constructor(ctx, path) {
483
- this.ctx = ctx;
484
- this.path = path;
485
- this.uri = normalizePath(path.relativePath.replace(extname(path.relativePath), ""));
333
+ function deepMerge(base, partial) {
334
+ const result = { ...base };
335
+ for (const key of Object.keys(partial)) {
336
+ const baseVal = result[key];
337
+ const partialVal = partial[key];
338
+ result[key] = isPlainObject(baseVal) && isPlainObject(partialVal) ? deepMerge(baseVal, partialVal) : partialVal;
486
339
  }
487
- /**
488
- * Get page metadata
489
- * Parse configuration defined by definePage macro and return metadata for pages.json
490
- *
491
- * @param forceUpdate - Whether to force update, ignoring cache
492
- * @returns Page metadata object
493
- */
494
- async getPageMeta(forceUpdate = false) {
495
- if (forceUpdate || !this.meta) await this.read();
496
- const { path, tabBar: _, ...others } = this.meta || {};
497
- return {
498
- path: path ?? this.uri,
499
- ...others
500
- };
340
+ return result;
341
+ }
342
+ function toPlatforms(platform) {
343
+ return Array.isArray(platform) ? platform : [platform];
344
+ }
345
+ /** uni-app 生态中同一浏览器目标的平台标识别名 */
346
+ const H5_ALIASES = ["h5", "web"];
347
+ /**
348
+ * `platform` 是否匹配 `platforms` 列表
349
+ *
350
+ * uni-env 的 `isH5`/`isWeb` 同时认 `h5` 和 `web` 两个写法(较新的
351
+ * uni-app H5 构建设置的是 UNI_PLATFORM=web),所以列表里写了其中
352
+ * 任何一个写法,都算匹配。
353
+ */
354
+ function platformMatches(platforms, platform) {
355
+ if (platforms.includes(platform)) return true;
356
+ return H5_ALIASES.includes(platform) && platforms.some((p) => H5_ALIASES.includes(p));
357
+ }
358
+ /**
359
+ * 可以链式调用的"按平台写配置"对象
360
+ *
361
+ * 用户在函数式 definePage 里通过 define() 创建。分支按写的顺序生效,
362
+ * 后写的同名键会盖掉先写的。
363
+ */
364
+ var DefineConditional = class {
365
+ /** 所有平台共用的基础配置 */
366
+ base;
367
+ /** 按写的顺序记下的条件分支 */
368
+ branches = [];
369
+ constructor(base) {
370
+ this.base = base;
501
371
  }
502
- /**
503
- * Get page tabBar configuration
504
- * Extract tabBar related configuration from definePage macro
505
- *
506
- * @param forceUpdate - Whether to force update, ignoring cache
507
- * @returns tabBar configuration object, or undefined if page doesn't define tabBar
508
- */
509
- async getTabBar(forceUpdate = false) {
510
- if (forceUpdate || !this.meta) await this.read();
511
- const { tabBar } = this.meta || {};
512
- if (tabBar === void 0) return;
513
- return {
514
- ...tabBar,
515
- pagePath: tabBar.pagePath || this.uri,
516
- index: tabBar.index ?? 0
517
- };
372
+ /** 仅在给定平台应用 `partial` */
373
+ ifdef(platform, partial) {
374
+ this.branches.push({
375
+ condition: "ifdef",
376
+ platforms: toPlatforms(platform),
377
+ partial
378
+ });
379
+ return this;
518
380
  }
519
- /**
520
- * Check if the page has changed
521
- * @returns Whether the page has changed
522
- */
523
- hasChanged() {
524
- return this.changed;
381
+ /** 在除给定平台之外的所有平台应用 `partial` */
382
+ ifndef(platform, partial) {
383
+ this.branches.push({
384
+ condition: "ifndef",
385
+ platforms: toPlatforms(platform),
386
+ partial
387
+ });
388
+ return this;
525
389
  }
526
390
  /**
527
- * Read page file and parse metadata
528
- * Extract configuration defined by definePage macro from Vue SFC
391
+ * 算出某个平台最终用的普通配置:从基础配置开始,按写的顺序把每个
392
+ * 匹配的分支合并进去。平台列表里 `h5` 和 `web` 互相通用(见
393
+ * platformMatches)。
529
394
  */
530
- async read() {
531
- let meta;
532
- try {
533
- meta = await this.readPageMetaFromFile();
534
- } catch (err) {
535
- debug.error(err);
536
- return;
537
- }
538
- let raw = "";
539
- try {
540
- raw = JSON.stringify(meta);
541
- } catch {}
542
- this.changed = this.raw !== raw;
543
- this.meta = meta;
544
- this.raw = raw;
545
- }
546
- async readPageMetaFromFile() {
547
- try {
548
- const meta = await tryPageMetaFromMacro(parseSFC(await fs.promises.readFile(this.path.absolutePath, { encoding: "utf-8" }), { filename: this.path.absolutePath }));
549
- if (meta) return meta;
550
- return { type: "page" };
551
- } catch (err) {
552
- throw new Error(`Read page meta fail in ${this.path.relativePath}\n${err.message}`);
553
- }
395
+ resolve(platform) {
396
+ let result = this.base;
397
+ for (const branch of this.branches) if (branch.condition === "ifdef" ? platformMatches(branch.platforms, platform) : !platformMatches(branch.platforms, platform)) result = deepMerge(result, branch.partial);
398
+ return result;
554
399
  }
555
400
  };
401
+ /** 判断宏求值的结果是不是一个条件定义 */
402
+ function isConditional(obj) {
403
+ return obj instanceof DefineConditional;
404
+ }
405
+ /**
406
+ * 把条件定义算成某个平台的普通配置
407
+ *
408
+ * 简单包了一层 {@link DefineConditional.resolve},调用方不用导入
409
+ * 这个类。
410
+ */
411
+ function resolveConditional(cond, platform) {
412
+ return cond.resolve(platform);
413
+ }
414
+ //#endregion
415
+ //#region src/macro.ts
416
+ /**
417
+ * definePage 宏的解析与求值
418
+ *
419
+ * 这个文件负责看懂一个 definePage 宏要的全部步骤:解析 Vue 单文件
420
+ * 组件、逐个 script 块解析(单个块失败不影响别的块)、找到宏调用、
421
+ * 收集 import 语句、放进沙箱求值。外部只用两个函数:
422
+ * evaluateDefinePage(扫描时用)和 findDefinePageMacro(转换时用)。
423
+ *
424
+ * 两个入口都从 {@link parseSfcBlocks} 的块模型取宏:「两边按同一套
425
+ * 规则找到同一个宏」由这个共享模型保证,不靠两边各自实现再对齐。
426
+ */
556
427
  /**
557
- * Parse Vue Single File Component (SFC)
558
- * Compatible with different versions of @vue/compiler-sfc
428
+ * 解析 Vue 单文件组件(SFC)
429
+ * 兼容不同版本的 @vue/compiler-sfc
559
430
  *
560
- * @param code - Vue SFC source code
561
- * @param options - Parse options
562
- * @returns SFC descriptor object
431
+ * @param code - Vue SFC 源码
432
+ * @param options - 解析选项
433
+ * @returns SFC descriptor 对象
563
434
  */
564
435
  function parseSFC(code, options) {
565
436
  return parse$1(code, {
@@ -571,39 +442,112 @@ function parseSFC(code, options) {
571
442
  });
572
443
  }
573
444
  /**
574
- * Try to extract page metadata defined by definePage macro from SFC
575
- * Support using definePage in script setup or regular script
445
+ * 把 SFC 的两个 script 块各自解析成 AST(共享的块模型)。
446
+ *
447
+ * 块序固定先 `<script setup>` 后 `<script>`;一个块解析失败不拦另一
448
+ * 个块(宏多半写在另一个块里,比如 @babel/parser 8 删掉的旧版
449
+ * `assert { ... }` import 属性会让还在用它的块失败)。失败记录在块
450
+ * 的 `error` 上,由调用方决定上报还是抛出。
451
+ *
452
+ * @param code - Vue SFC 源码
453
+ * @param filename - 用于错误提示和模块解析的 SFC 文件名
454
+ * @returns 按固定顺序排列的块列表
455
+ */
456
+ function parseSfcBlocks(code, filename) {
457
+ const sfc = parseSFC(code, { filename });
458
+ const blocks = [];
459
+ for (const [name, script] of [["<script setup>", sfc.scriptSetup], ["<script>", sfc.script]]) {
460
+ if (!script) continue;
461
+ try {
462
+ const body = babelParse(script.content, script.lang || "js").body;
463
+ blocks.push({
464
+ name,
465
+ body,
466
+ imports: findImports(body)
467
+ });
468
+ } catch (error) {
469
+ blocks.push({
470
+ name,
471
+ body: [],
472
+ imports: [],
473
+ error
474
+ });
475
+ }
476
+ }
477
+ return blocks;
478
+ }
479
+ /**
480
+ * 求值 Vue SFC 里的 definePage 宏,返回页面信息
576
481
  *
577
- * @param sfc - Vue SFC descriptor
578
- * @returns Page metadata object, or undefined if definePage is not found
482
+ * 扫描时用。解析或求值出错时,错误会交给调用方(Page.read),它会把
483
+ * 这个页面退回成只含路径的简单信息。
484
+ *
485
+ * @param code - Vue SFC 源码
486
+ * @param filename - SFC 文件名,用于错误提示和模块解析
487
+ * @param platform - 当前平台标识,传给函数式宏;默认取 uni-env 的平台
488
+ * @returns 页面信息对象;宏明确退出(definePage(null) 或函数返回
489
+ * null)时为 `null`;没找到 definePage 时为 undefined
579
490
  */
580
- async function tryPageMetaFromMacro(sfc) {
581
- const sfcScript = sfc.scriptSetup || sfc.script;
582
- if (!sfcScript) return;
583
- const ast = babelParse(sfcScript.content, sfcScript.lang || "js", { plugins: [["importAttributes", { deprecatedAssertSyntax: true }]] });
584
- const macro = findMacro(ast.body, sfc.filename);
585
- if (macro) {
586
- const imports = findImports(ast.body).filter((imp) => !!imp.specifiers.length).map((imp) => babelGenerate(imp).code);
491
+ async function evaluateDefinePage(code, filename, platform$2 = platform) {
492
+ const blocks = parseSfcBlocks(code, filename);
493
+ for (const { body, error } of blocks) {
494
+ if (error) continue;
495
+ const macro = findMacro(body, filename);
496
+ if (!macro) continue;
587
497
  const [macroOption] = macro.arguments;
588
- const code = babelGenerate(macroOption).code;
498
+ if (!macroOption) return void 0;
499
+ const macroCode = babelGenerate(macroOption).code;
589
500
  const parsed = await parseCode({
590
- imports,
591
- code,
592
- filename: sfc.filename
501
+ imports: pickEvalImports(blocks.map((block) => block.imports), macroOption),
502
+ code: macroCode,
503
+ filename
593
504
  });
505
+ const parsedMeta = typeof parsed === "function" ? await parsed({
506
+ platform: platform$2,
507
+ define: (base) => new DefineConditional(base)
508
+ }) : parsed;
509
+ const resolvedMeta = isConditional(parsedMeta) ? parsedMeta.resolve(platform$2) : parsedMeta;
510
+ if (resolvedMeta === null) return null;
594
511
  return {
595
512
  type: "page",
596
- ...typeof parsed === "function" ? await Promise.resolve(parsed()) : await Promise.resolve(parsed)
513
+ ...resolvedMeta
597
514
  };
598
515
  }
516
+ const failed = blocks.find((block) => block.error);
517
+ if (failed) throw failed.error;
518
+ }
519
+ /**
520
+ * 在 Vue SFC 中定位 definePage 宏调用但不求值
521
+ *
522
+ * 转换插件用它找到宏再删掉,不求值。块解析失败通过 `onParseError`
523
+ * 报出来,不抛错。
524
+ *
525
+ * @param code - Vue SFC 源码
526
+ * @param filename - 用于错误上报的 SFC 文件名
527
+ * @param options - 可选的按块解析失败钩子
528
+ * @param options.onParseError - 以失败的 script 块名与错误对象调用
529
+ * @returns definePage 调用表达式,未找到时为 undefined
530
+ */
531
+ function findDefinePageMacro(code, filename, options = {}) {
532
+ for (const { name, body, error } of parseSfcBlocks(code, filename)) {
533
+ if (error) {
534
+ options.onParseError?.(name, error);
535
+ continue;
536
+ }
537
+ const macro = findMacro(body, filename);
538
+ if (macro) return macro;
539
+ }
599
540
  }
600
541
  /**
601
- * Find definePage macro call in AST
602
- * Support function expressions, arrow functions and object expressions as arguments
542
+ * 在 AST 中查找 definePage 宏调用
543
+ * 支持函数表达式、箭头函数与对象表达式作为参数
544
+ *
545
+ * 已知限制:只在块的顶层语句里找,包进 if/try 等语句块里的调用
546
+ * 不在此列(用户侧的说明写在 README 的 definePage 章节)
603
547
  *
604
- * @param stmts - AST statement array
605
- * @param filename - Filename for error reporting
606
- * @returns definePage call expression, or undefined if not found
548
+ * @param stmts - AST 语句数组
549
+ * @param filename - 用于错误上报的文件名
550
+ * @returns definePage 调用表达式,未找到时为 undefined
607
551
  */
608
552
  function findMacro(stmts, filename) {
609
553
  let macro;
@@ -617,74 +561,1065 @@ function findMacro(stmts, filename) {
617
561
  }
618
562
  if (!macro) return;
619
563
  const [opt] = macro.arguments;
620
- if (opt && !t.isFunctionExpression(opt) && !t.isArrowFunctionExpression(opt) && !t.isObjectExpression(opt)) {
621
- debug.definePage(`definePage() 参数仅支持函数或对象:${filename}`);
564
+ if (opt && !t.isFunctionExpression(opt) && !t.isArrowFunctionExpression(opt) && !t.isObjectExpression(opt) && !t.isNullLiteral(opt)) {
565
+ debug.definePage(`definePage() only supports a function, object literal or null as argument: ${filename}`);
622
566
  return;
623
567
  }
624
568
  return macro;
625
569
  }
626
570
  /**
627
- * Extract all import declarations from AST
628
- * Used to provide necessary imports when executing definePage arguments
571
+ * 从 AST 中提取全部导入声明
572
+ * 用于在执行 definePage 参数时提供必要的导入
629
573
  *
630
- * @param stmts - AST statement array
631
- * @returns Import declaration array
574
+ * @param stmts - AST 语句数组
575
+ * @returns 导入声明数组
632
576
  */
633
577
  function findImports(stmts) {
634
- const imports = [];
635
- for (const stmt of stmts) if (t.isImportDeclaration(stmt)) imports.push(stmt);
636
- return imports;
578
+ return stmts.filter(t.isImportDeclaration);
637
579
  }
638
- //#endregion
639
- //#region src/context.ts
640
580
  /**
641
- * Page context class responsible for page scanning, config loading, page metadata merging and pages.json generation
581
+ * 从两个 script 块的全部 import 里,挑出宏参数真正用到的那些语句。
642
582
  *
643
- * Core responsibilities:
644
- * 1. Scan page directories and collect page files
645
- * 2. Load user configuration files (pages.config.ts, etc.)
646
- * 3. Parse page metadata (definePage macro)
647
- * 4. Merge page configurations and generate pages.json
648
- * 5. Provide virtual module and HMR support
583
+ * 挑选按"名字被宏引用"来:没被用到的 import 不进沙箱,免得为了求值
584
+ * 一个宏,把整个页面的依赖都加载一遍(比如普通 `<script>` 里 import
585
+ * 的 `.vue` 组件,require 加载不动它,还会让本来好好的页面求值失败)。
586
+ * 两个块里写了完全相同的 import 时只留一份,不然编译出的代码会重复
587
+ * 声明同一个名字
649
588
  */
650
- var PageContext = class {
651
- _server;
652
- /** Parsed configuration object from user config file (pages.config.ts) */
653
- pagesGlobConfig;
654
- /** Source path list of user configuration files */
655
- pagesConfigSourcePaths = [];
656
- /** Main package page map, key is the absolute path of the page file */
589
+ function pickEvalImports(allBlocks, macroArg) {
590
+ const referenced = /* @__PURE__ */ new Set();
591
+ collectReferencedIdentifiers(macroArg, referenced);
592
+ const codes = /* @__PURE__ */ new Set();
593
+ for (const imports of allBlocks) for (const imp of imports) {
594
+ if (!imp.specifiers.some((spec) => referenced.has(spec.local.name))) continue;
595
+ codes.add(babelGenerate(imp).code);
596
+ }
597
+ return [...codes];
598
+ }
599
+ /**
600
+ * 递归收集节点里出现的标识符名字。
601
+ *
602
+ * 故意宁可多收、不可漏收:`obj.key` 里的 key、`{ style: s }` 里的
603
+ * style 这类"名字位置"的标识符也会被收进来。多收的后果只是多带一条
604
+ * import(那条语句本来就写在用户自己的文件里),漏收才会让宏求值
605
+ * 莫名失败
606
+ */
607
+ function collectReferencedIdentifiers(node, names) {
608
+ if (Array.isArray(node)) {
609
+ for (const item of node) collectReferencedIdentifiers(item, names);
610
+ return;
611
+ }
612
+ if (node === null || typeof node !== "object") return;
613
+ const astNode = node;
614
+ if (astNode.type === "Identifier") {
615
+ names.add(astNode.name);
616
+ return;
617
+ }
618
+ if (astNode.type === "MemberExpression" && !astNode.computed) {
619
+ collectReferencedIdentifiers(astNode.object, names);
620
+ return;
621
+ }
622
+ if (astNode.type === "ObjectProperty" && !astNode.computed) {
623
+ collectReferencedIdentifiers(astNode.value, names);
624
+ return;
625
+ }
626
+ for (const [key, value] of Object.entries(astNode)) {
627
+ if (key === "loc") continue;
628
+ collectReferencedIdentifiers(value, names);
629
+ }
630
+ }
631
+ /**
632
+ * 将 TypeScript / JavaScript 脚本代码转换为对象/函数
633
+ *
634
+ * @param options - 脚本执行所需配置
635
+ * @param options.imports - 需要包含的模块导入语句列表
636
+ * @param options.code - 待执行的 TypeScript 代码内容
637
+ * @param options.filename - 用于错误定位与上下文的脚本文件名
638
+ * @returns 脚本执行结果,export 为函数时会执行并返回其返回值
639
+ */
640
+ async function parseCode(options) {
641
+ const { imports = [], code, filename } = options;
642
+ let jsCode = "";
643
+ try {
644
+ const tmpCode = `${imports.join("\n")}\n export default ${code}`;
645
+ jsCode = ts.transpileModule(tmpCode, {
646
+ compilerOptions: {
647
+ module: ts.ModuleKind.CommonJS,
648
+ target: ts.ScriptTarget.ES2022,
649
+ noEmit: true,
650
+ strict: false,
651
+ noImplicitAny: false,
652
+ strictNullChecks: false,
653
+ strictFunctionTypes: false,
654
+ strictBindCallApply: false,
655
+ strictPropertyInitialization: false,
656
+ noImplicitThis: false,
657
+ alwaysStrict: false,
658
+ allowJs: true,
659
+ checkJs: false,
660
+ skipLibCheck: true,
661
+ esModuleInterop: true,
662
+ removeComments: true
663
+ },
664
+ jsDocParsingMode: ts.JSDocParsingMode.ParseNone
665
+ }).outputText;
666
+ const dir = path.dirname(filename);
667
+ const vmContext = {
668
+ module: {},
669
+ exports: {},
670
+ __filename: filename,
671
+ __dirname: dir,
672
+ require: createRequire(path.join(dir, "define-page.mjs")),
673
+ import: (id) => import(id),
674
+ setTimeout,
675
+ clearTimeout,
676
+ setInterval,
677
+ clearInterval,
678
+ setImmediate,
679
+ clearImmediate,
680
+ console,
681
+ URL,
682
+ URLSearchParams,
683
+ performance,
684
+ global: globalThis,
685
+ globalThis
686
+ };
687
+ await new vm.Script(jsCode, { filename }).runInNewContext(vmContext, { timeout: 1e3 });
688
+ const exportsObj = vmContext.exports;
689
+ return "default" in exportsObj ? exportsObj.default : exportsObj;
690
+ } catch (error) {
691
+ throw new Error(`EXEC SCRIPT FAIL IN ${filename}: ${error.message} \n\n${jsCode}\n\n`);
692
+ }
693
+ }
694
+ //#endregion
695
+ //#region src/page.ts
696
+ /**
697
+ * 表示一个 Vue 页面文件的 Page 类
698
+ *
699
+ * 职责:
700
+ * 1. 读取页面文件内容
701
+ * 2. 解析 definePage 宏定义的页面配置
702
+ * 3. 提供 tabBar 配置信息
703
+ * 4. 跟踪页面文件变更状态
704
+ */
705
+ var Page = class {
706
+ /** 页面上下文实例 */
707
+ ctx;
708
+ /** 页面路径信息,包含相对路径与绝对路径 */
709
+ path;
710
+ /** 页面 URI,用于 pages.json 的 path 字段 */
711
+ uri;
712
+ /** 页面是否发生变化,用于增量更新判断 */
713
+ changed = true;
714
+ /**
715
+ * 页面是否通过 `definePage(null)` 或函数式宏在当前平台返回 null 而
716
+ * 退出了 pages.json
717
+ */
718
+ skipped = false;
719
+ /** 页面文件是否至少被读取过一次 */
720
+ loaded = false;
721
+ /** 页面配置的原始 JSON 字符串,用于变更检测 */
722
+ raw = "";
723
+ /** 解析后的页面配置 */
724
+ meta;
725
+ /**
726
+ * 创建页面实例
727
+ * @param ctx - 页面上下文实例
728
+ * @param path - 页面路径信息
729
+ */
730
+ constructor(ctx, path) {
731
+ this.ctx = ctx;
732
+ this.path = path;
733
+ const ext = extname(path.relativePath);
734
+ this.uri = normalizePath(ext ? path.relativePath.slice(0, -ext.length) : path.relativePath);
735
+ }
736
+ /**
737
+ * 获取页面配置
738
+ * 解析 definePage 宏定义的配置并返回用于 pages.json 的配置
739
+ *
740
+ * @param forceUpdate - 是否强制更新,忽略缓存
741
+ * @returns 页面配置对象
742
+ */
743
+ async getPageMeta(forceUpdate = false) {
744
+ if (forceUpdate || !this.loaded) await this.read();
745
+ const { path, tabBar: _, ...others } = this.meta || {};
746
+ return {
747
+ path: path ?? this.uri,
748
+ ...others
749
+ };
750
+ }
751
+ /**
752
+ * 获取页面 tabBar 配置
753
+ * 从 definePage 宏中提取 tabBar 相关配置
754
+ *
755
+ * @param forceUpdate - 是否强制更新,忽略缓存
756
+ * @returns tabBar 配置对象,页面未定义 tabBar 时返回 undefined
757
+ */
758
+ async getTabBar(forceUpdate = false) {
759
+ if (forceUpdate || !this.loaded) await this.read();
760
+ if (this.skipped) return void 0;
761
+ const { tabBar } = this.meta || {};
762
+ if (tabBar === void 0) return;
763
+ return {
764
+ ...tabBar,
765
+ pagePath: tabBar.pagePath || this.uri,
766
+ index: tabBar.index ?? 0
767
+ };
768
+ }
769
+ /**
770
+ * 确保页面文件至少被读取过一次,使 `skipped` 与缓存的配置反映
771
+ * 当前文件内容
772
+ */
773
+ async ensureLoaded() {
774
+ if (!this.loaded) await this.read();
775
+ }
776
+ /**
777
+ * 检查页面是否发生变化
778
+ * @returns 页面是否发生变化
779
+ */
780
+ hasChanged() {
781
+ return this.changed;
782
+ }
783
+ /**
784
+ * 读取页面文件并解析配置
785
+ * 从 Vue SFC 中提取 definePage 宏定义的配置
786
+ */
787
+ async read() {
788
+ let meta;
789
+ let skipped = false;
790
+ try {
791
+ const result = await this.readPageMetaFromFile();
792
+ if (result === null) skipped = true;
793
+ else meta = result;
794
+ } catch (err) {
795
+ debug.error(err);
796
+ this.ctx.logger?.warn(err?.message ?? String(err), { timestamp: true });
797
+ return;
798
+ }
799
+ let raw = "";
800
+ try {
801
+ raw = JSON.stringify(meta) ?? "";
802
+ } catch {}
803
+ this.changed = this.raw !== raw || this.skipped !== skipped;
804
+ this.loaded = true;
805
+ this.meta = meta;
806
+ this.raw = raw;
807
+ this.skipped = skipped;
808
+ }
809
+ async readPageMetaFromFile() {
810
+ try {
811
+ const meta = await evaluateDefinePage(await fs.promises.readFile(this.path.absolutePath, { encoding: "utf-8" }), this.path.absolutePath, this.ctx.platform);
812
+ return meta === void 0 ? { type: "page" } : meta;
813
+ } catch (err) {
814
+ throw new Error(`Read page meta fail in ${this.path.relativePath}\n${err.message}`);
815
+ }
816
+ }
817
+ };
818
+ //#endregion
819
+ //#region src/platform-variants.ts
820
+ /**
821
+ * 把 `||` 分隔的平台列表拆开,去掉当前平台,剩下的排好序(规则 1)
822
+ *
823
+ * 空平台名一律丢掉:残缺的生成标记行(手改时把平台列表删空了)和
824
+ * 手写的尾巴多一个分隔符的指令(如 `#ifdef H5 ||`)都会拆出空串。
825
+ * 若留着它,它会混进平台全集并永久留在标记里,还会让条目包上
826
+ * `#ifdef || MP-WEIXIN` 这样的非法指令(空平台名不是一个真实平台,
827
+ * 过滤掉永远不会出错)
828
+ */
829
+ function platformsExcluding(platformList, currentPlatform) {
830
+ return platformList.split("||").map((p) => p.trim()).filter((p) => p && p !== currentPlatform).sort();
831
+ }
832
+ /**
833
+ * 把本次运行的一个载荷并入变体列表(规则 2):
834
+ * - 内容相同的手写 #ifndef 变体优先:当前平台没被排除时,那条
835
+ * #ifndef 已经覆盖了它;当前平台正好被排除时,尊重手写的隐藏规
836
+ * 则。两种情况下再单独写一份都会产生重复路由或覆盖用户的条件
837
+ * - 内容相同的普通变体:当前平台叠进它的平台列表
838
+ * - 没有内容相同的变体:新建只属于当前平台的变体
839
+ *
840
+ * @param variants - 同一个键下已有的变体列表,原地修改
841
+ * @param payload - 本次运行的载荷
842
+ * @param valueStr - 载荷的比较字符串
843
+ * @param currentPlatform - 当前平台标识(大写)
844
+ * @param compatible - 可选的额外相容判断。数组侧用它区分两个平台各自
845
+ * 声明的不同首页:内容相同但首页标记冲突时不合并
846
+ */
847
+ function upsertCurrentVariant(variants, payload, valueStr, currentPlatform, compatible) {
848
+ const isSame = (variant) => variant.valueStr === valueStr && (compatible ? compatible(variant.payload, payload) : true);
849
+ if (variants.some((variant) => variant.platforms === null && isSame(variant))) return;
850
+ const equal = variants.find((variant) => variant.platforms !== null && isSame(variant));
851
+ if (equal) {
852
+ equal.platforms.push(currentPlatform);
853
+ equal.platforms.sort();
854
+ equal.platformStr = equal.platforms.join(" || ");
855
+ return;
856
+ }
857
+ variants.push({
858
+ payload,
859
+ valueStr,
860
+ platforms: [currentPlatform],
861
+ platformStr: currentPlatform
862
+ });
863
+ }
864
+ /**
865
+ * 输出前按平台列表把变体排好序(规则 4)
866
+ *
867
+ * 当前平台的变体在收集时被排除、并入时又补到列表末尾,两个平台轮流
868
+ * 运行(dev:h5 和 dev:mp-weixin 交替写)时,同一个键的变体顺序就会
869
+ * 来回翻转——条件编译的结果没变,但文件每次都有无意义的变动。
870
+ * platformStr 由排好序的平台列表拼成,手写 #ifndef 变体是空串、排最
871
+ * 前。排序是稳定的:并列时(同键两组平台列表相同,只出现在手写文件
872
+ * 里)保持收集顺序
873
+ */
874
+ function sortVariants(variants) {
875
+ variants.sort((a, b) => a.platformStr < b.platformStr ? -1 : a.platformStr > b.platformStr ? 1 : 0);
876
+ }
877
+ /**
878
+ * 收集所有变体记录过的平台(手写 #ifndef 不算,它没有正向列表),
879
+ * 这个集合同时也是「拥有至少一个变体」的平台集合
880
+ */
881
+ function collectVariantPlatforms(mergedMap) {
882
+ const platforms = /* @__PURE__ */ new Set();
883
+ for (const variants of mergedMap.values()) for (const { platforms: variantPlatforms } of variants) if (variantPlatforms) for (const platform of variantPlatforms) platforms.add(platform);
884
+ return platforms;
885
+ }
886
+ /**
887
+ * 把所有变体记录的平台合在一起,算出「平台全集」
888
+ *
889
+ * 平台全集会写进生成标记,也决定哪些变体可以不加 #ifdef 直接输出:
890
+ * 没被包裹的条目在 uni-app 条件编译下所有平台都看得见,所以只有
891
+ * 覆盖了全部平台的变体才能"裸着"输出(规则 5)。
892
+ *
893
+ * 当前平台一定在全集里,哪怕这次运行一个变体都没贡献——比如页面
894
+ * 只在 H5 存在,而 MP-WEIXIN 运行把每个页面都 definePage(null) 掉了。
895
+ * 如果只从剩下的条目推全集,全集就只有 'H5',H5 专属条目看起来
896
+ * "覆盖了全集",会不加包裹地输出,泄漏进微信的构建产物。老版本按
897
+ * "最常用平台组合当默认值"的做法也有同样的问题:可能算出单平台
898
+ * 组合,把那个平台的条目裸着输出,变成其他平台里的重复路由。这个
899
+ * 全集同样管着 tabBar 属性的包裹:没有它,一次没有 tabBar 的运行
900
+ * 会以为"拥有平台 == 全集",把别的平台的 tabBar 裸着输出出去。
901
+ *
902
+ * 上一次运行记在标记里的平台也进全集({@link extractLastPlatforms})。
903
+ * 否则当某平台的条目都被重新扫描替换后,它"来过"这件事只记在标记
904
+ * 里:只看条目推全集,全集会一会儿大一会儿小,条目一会儿包一会儿
905
+ * 裸。把标记也算进来,全集就只增不减;多算的平台最多带来多余的包
906
+ * 裹,不会泄漏。
907
+ *
908
+ * 只增不减的代价:彻底不用的平台会一直留在标记里。这无害,但别
909
+ * 手工去改标记行——想清理就删掉整个生成的 pages.json,下次运行会
910
+ * 从头生成。
911
+ */
912
+ function resolvePlatformUnion(mergedMap, currentPlatform, lastPlatforms) {
913
+ return [.../* @__PURE__ */ new Set([
914
+ currentPlatform,
915
+ ...lastPlatforms,
916
+ ...collectVariantPlatforms(mergedMap)
917
+ ])].sort();
918
+ }
919
+ const GENERATION_MARKER_PREFIX = "GENERATED BY UNI-PAGES, PLATFORM:";
920
+ /** 判断注释令牌是否为 pages.json 的生成标记行 */
921
+ function isGenerationMarker(token) {
922
+ return token.type !== "BlankLine" && typeof token.value === "string" && token.value.trim().startsWith("GENERATED BY UNI-PAGES, PLATFORM:");
923
+ }
924
+ /** 判断一个 pages 数组是否由插件写入(携带生成标记) */
925
+ function hasGenerationMarker(src) {
926
+ if (!src) return false;
927
+ return (src[Symbol.for("before:0")] || []).some(isGenerationMarker);
928
+ }
929
+ /**
930
+ * 从 before:0 的生成标记里读出上一次运行记录了哪些平台,
931
+ * 去掉当前平台后返回
932
+ */
933
+ function extractLastPlatforms(src, currentPlatform) {
934
+ let lastPlatforms = [];
935
+ for (const comment of src[Symbol.for("before:0")] || []) {
936
+ if (comment.type === "BlankLine") continue;
937
+ const trimmed = comment.value.trim();
938
+ if (trimmed.startsWith("GENERATED BY UNI-PAGES, PLATFORM:")) lastPlatforms = platformsExcluding(trimmed.split(":")[1], currentPlatform);
939
+ }
940
+ return lastPlatforms;
941
+ }
942
+ //#endregion
943
+ //#region src/jsonc-scan.ts
944
+ /**
945
+ * 跳过空白和注释往前走。遇到行注释或块注释时,注释里的文字(去掉
946
+ * 首尾空白)会收集到 `comments` 里(如果传了)
947
+ */
948
+ function skipJsoncFiller(content, index, comments) {
949
+ let i = index;
950
+ while (i < content.length) {
951
+ const ch = content[i];
952
+ if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
953
+ i++;
954
+ continue;
955
+ }
956
+ if (ch === "/" && content[i + 1] === "/") {
957
+ const end = content.indexOf("\n", i);
958
+ const stop = end === -1 ? content.length : end;
959
+ comments?.push(content.slice(i + 2, stop).trim());
960
+ i = stop;
961
+ continue;
962
+ }
963
+ if (ch === "/" && content[i + 1] === "*") {
964
+ const end = content.indexOf("*/", i + 2);
965
+ const stop = end === -1 ? content.length : end + 2;
966
+ comments?.push(content.slice(i + 2, end === -1 ? content.length : end).trim());
967
+ i = stop;
968
+ continue;
969
+ }
970
+ return i;
971
+ }
972
+ return i;
973
+ }
974
+ /** 从起始引号扫描 JSON 字符串字面量;返回闭合引号之后的下标 */
975
+ function scanJsonString(content, start) {
976
+ let i = start + 1;
977
+ while (i < content.length) {
978
+ if (content[i] === "\\") {
979
+ i += 2;
980
+ continue;
981
+ }
982
+ if (content[i] === "\"") return i + 1;
983
+ i++;
984
+ }
985
+ return content.length;
986
+ }
987
+ /** 扫描 `start` 处(定位在值的首字符)的 JSON 值;返回其后的下标 */
988
+ function scanJsonValue(content, start) {
989
+ const ch = content[start];
990
+ if (ch === "\"") return scanJsonString(content, start);
991
+ if (ch === "{" || ch === "[") {
992
+ let depth = 0;
993
+ let i = start;
994
+ while (i < content.length) {
995
+ const c = content[i];
996
+ if (c === "\"") {
997
+ i = scanJsonString(content, i);
998
+ continue;
999
+ }
1000
+ if (c === "/" && (content[i + 1] === "/" || content[i + 1] === "*")) {
1001
+ i = skipJsoncFiller(content, i);
1002
+ continue;
1003
+ }
1004
+ if (c === "{" || c === "[") {
1005
+ depth++;
1006
+ i++;
1007
+ continue;
1008
+ }
1009
+ if (c === "}" || c === "]") {
1010
+ depth--;
1011
+ i++;
1012
+ if (depth === 0) return i;
1013
+ continue;
1014
+ }
1015
+ i++;
1016
+ }
1017
+ return content.length;
1018
+ }
1019
+ let i = start;
1020
+ while (i < content.length && !",{}[] \r\n\"".includes(content[i])) i++;
1021
+ return i;
1022
+ }
1023
+ /** 读取 `start` 处(其起始引号)的 JSON 字符串字面量;返回解析文本与其后的下标 */
1024
+ function readJsonStringToken(content, start) {
1025
+ const end = scanJsonString(content, start);
1026
+ try {
1027
+ return [JSON.parse(content.slice(start, end)), end];
1028
+ } catch {
1029
+ return null;
1030
+ }
1031
+ }
1032
+ /**
1033
+ * 按文本扫描 pages.json 里的 tabBar 对象,把每个顶层属性连同它的
1034
+ * 条件编译包裹一起收集起来。
1035
+ *
1036
+ * 为什么要按文本扫:comment-json 解析时会按普通 JS 对象的规则,
1037
+ * 同一个键出现多次只留最后一个,而"同一个属性按平台写多个值"靠的
1038
+ * 就是重复键。
1039
+ * 这些信息一旦解析就丢了,所以必须从原始文本里捞回来。
1040
+ * tabBar 不是对象、或文件结构看不懂时返回 undefined。
1041
+ */
1042
+ function extractTabBarProps(content) {
1043
+ let i = skipJsoncFiller(content, 0);
1044
+ if (content[i] !== "{") return void 0;
1045
+ i = skipJsoncFiller(content, i + 1);
1046
+ while (i < content.length) {
1047
+ if (content[i] === "}" || content[i] !== "\"") return void 0;
1048
+ const token = readJsonStringToken(content, i);
1049
+ if (!token) return void 0;
1050
+ const [key, keyEnd] = token;
1051
+ i = skipJsoncFiller(content, keyEnd);
1052
+ if (content[i] !== ":") return void 0;
1053
+ i = skipJsoncFiller(content, i + 1);
1054
+ if (key === "tabBar") {
1055
+ if (content[i] !== "{") return void 0;
1056
+ return parseTabBarProps(content, i + 1);
1057
+ }
1058
+ i = skipJsoncFiller(content, scanJsonValue(content, i));
1059
+ if (content[i] === ",") i = skipJsoncFiller(content, i + 1);
1060
+ else if (content[i] !== "}") return void 0;
1061
+ }
1062
+ }
1063
+ /** 从 tabBar 对象起始 `{` 之后开始解析其内部 */
1064
+ function parseTabBarProps(content, interiorStart) {
1065
+ const props = /* @__PURE__ */ new Map();
1066
+ let beforeComments = [];
1067
+ let i = skipJsoncFiller(content, interiorStart, beforeComments);
1068
+ const openConditions = [];
1069
+ const applyCommentsToStack = (comments) => {
1070
+ for (const comment of comments) {
1071
+ const trimmed = comment.trim();
1072
+ if (trimmed.startsWith("#endif")) openConditions.pop();
1073
+ else if (trimmed.startsWith("#ifdef") || trimmed.startsWith("#ifndef")) openConditions.push(trimmed);
1074
+ }
1075
+ };
1076
+ applyCommentsToStack(beforeComments);
1077
+ while (true) {
1078
+ if (i >= content.length) return void 0;
1079
+ if (content[i] === "}") return props;
1080
+ if (content[i] === ",") {
1081
+ const postComma = [];
1082
+ i = skipJsoncFiller(content, i + 1, postComma);
1083
+ applyCommentsToStack(postComma);
1084
+ beforeComments = postComma;
1085
+ continue;
1086
+ }
1087
+ if (content[i] !== "\"") return void 0;
1088
+ const token = readJsonStringToken(content, i);
1089
+ if (!token) return void 0;
1090
+ const [key, keyEnd] = token;
1091
+ i = skipJsoncFiller(content, keyEnd);
1092
+ if (content[i] !== ":") return void 0;
1093
+ i = skipJsoncFiller(content, i + 1);
1094
+ const valueEnd = scanJsonValue(content, i);
1095
+ const rawValue = content.slice(i, valueEnd);
1096
+ const afterComments = [];
1097
+ i = skipJsoncFiller(content, valueEnd, afterComments);
1098
+ if (content[i] !== "," && content[i] !== "}") return void 0;
1099
+ const condition = openConditions[openConditions.length - 1] ?? null;
1100
+ applyCommentsToStack(afterComments);
1101
+ try {
1102
+ const value = parse(rawValue);
1103
+ let storedBefore = [...beforeComments];
1104
+ let storedAfter = afterComments;
1105
+ if (condition?.startsWith("#ifndef")) {
1106
+ storedBefore = [condition, ...beforeComments.filter((c) => !/^#(?:ifdef|ifndef|endif)/.test(c.trim()))];
1107
+ storedAfter = ["#endif", ...afterComments.filter((c) => !c.trim().startsWith("#endif"))];
1108
+ }
1109
+ const raw = {
1110
+ key,
1111
+ value,
1112
+ condition,
1113
+ beforeComments: storedBefore,
1114
+ afterComments: storedAfter
1115
+ };
1116
+ const variants = props.get(key) || [];
1117
+ variants.push(raw);
1118
+ props.set(key, variants);
1119
+ } catch {}
1120
+ }
1121
+ }
1122
+ //#endregion
1123
+ //#region src/tab-bar.ts
1124
+ /**
1125
+ * 跨平台合并 tabBar 的外观属性(`list` 以外的所有属性)。
1126
+ * 规则见 platform-variants.ts(和数组侧的 mergePlatformItems 共用):
1127
+ * - 包在 #ifdef 里的属性,属于注释里列出的那些平台
1128
+ * - 没包裹的属性,属于除当前平台外的所有平台(当前平台会写自己的)
1129
+ * - 一个值最后一个平台都不剩了,就删掉它
1130
+ * - 手写的 #ifndef 属性原样保留、永不删除,和数组侧的处理一致
1131
+ */
1132
+ function mergeTabBarProps(existingContent, currentProps, contributesProps, currentPlatform, platformUnion) {
1133
+ const rawProps = extractTabBarProps(existingContent);
1134
+ const merged = /* @__PURE__ */ new Map();
1135
+ const variantsOf = (key) => {
1136
+ const list = merged.get(key) || [];
1137
+ merged.set(key, list);
1138
+ return list;
1139
+ };
1140
+ for (const [key, variants] of rawProps || []) {
1141
+ if (key === "list") continue;
1142
+ for (const raw of variants) {
1143
+ const valueStr = JSON.stringify(raw.value);
1144
+ if (valueStr === void 0) continue;
1145
+ if (raw.condition?.startsWith("#ifndef")) {
1146
+ variantsOf(key).push({
1147
+ payload: raw.value,
1148
+ valueStr,
1149
+ platforms: null,
1150
+ platformStr: "",
1151
+ passthrough: {
1152
+ before: raw.beforeComments,
1153
+ after: raw.afterComments
1154
+ }
1155
+ });
1156
+ continue;
1157
+ }
1158
+ let platforms;
1159
+ if (raw.condition?.startsWith("#ifdef")) platforms = platformsExcluding(raw.condition.slice(6).trim(), currentPlatform);
1160
+ else platforms = platformUnion.filter((p) => p !== currentPlatform);
1161
+ if (platforms.length === 0) continue;
1162
+ variantsOf(key).push({
1163
+ payload: raw.value,
1164
+ valueStr,
1165
+ platforms,
1166
+ platformStr: platforms.join(" || "),
1167
+ passthrough: null
1168
+ });
1169
+ }
1170
+ }
1171
+ if (contributesProps) for (const [key, value] of Object.entries(currentProps)) {
1172
+ const valueStr = JSON.stringify(value);
1173
+ if (valueStr === void 0) continue;
1174
+ upsertCurrentVariant(variantsOf(key), value, valueStr, currentPlatform);
1175
+ }
1176
+ for (const variants of merged.values()) sortVariants(variants);
1177
+ return merged;
1178
+ }
1179
+ /** 一层缩进是多少(空格数或字符串) */
1180
+ function indentUnitOf(format) {
1181
+ const indent = format?.indent ?? 2;
1182
+ return typeof indent === "number" ? " ".repeat(indent) : indent;
1183
+ }
1184
+ /** 给除第一行以外的每一行加缩进(用于多行值拼进属性行的场景) */
1185
+ function indentContinuation(text, extra) {
1186
+ return text.split("\n").map((line, index) => index === 0 ? line : extra + line).join("\n");
1187
+ }
1188
+ /**
1189
+ * 把 tabBar 外观属性渲染成带缩进的输出行。所有平台都适用的值直接
1190
+ * 输出;其余每个值都写在自己的 `#ifdef` 块里(同一个键出现多次),
1191
+ * uni-app 按平台剥掉注释后,每个平台只会看到一个值。
1192
+ */
1193
+ function renderTabBarPropLines(merged, platformUnion, format) {
1194
+ const unit = indentUnitOf(format);
1195
+ const unionStr = platformUnion.join(" || ");
1196
+ const lines = [];
1197
+ for (const [key, variants] of merged) for (const variant of variants) {
1198
+ const valueText = indentContinuation(stringify(variant.payload, null, format?.indent ?? 2), unit + unit);
1199
+ const propLine = `${unit}${unit}${JSON.stringify(key)}: ${valueText},`;
1200
+ if (variant.passthrough) {
1201
+ for (const comment of variant.passthrough.before) if (comment) lines.push(`${unit}${unit}// ${comment}`);
1202
+ lines.push(propLine);
1203
+ for (const comment of variant.passthrough.after) if (comment) lines.push(`${unit}${unit}// ${comment}`);
1204
+ continue;
1205
+ }
1206
+ if (variant.platformStr === unionStr) lines.push(propLine);
1207
+ else {
1208
+ lines.push(`${unit}${unit}// #ifdef ${variant.platformStr}`);
1209
+ lines.push(propLine);
1210
+ lines.push(`${unit}${unit}// #endif`);
1211
+ }
1212
+ }
1213
+ return lines;
1214
+ }
1215
+ /** 把合并后的 tabBar list 渲染成带缩进的输出行(list 总是 tabBar 的最后一个属性) */
1216
+ function renderTabBarListLines(list, format) {
1217
+ const unit = indentUnitOf(format);
1218
+ return stringify(list, null, format?.indent ?? 2).split("\n").map((line, index) => {
1219
+ if (index === 0) return `${unit}${unit}"list": ${line}`;
1220
+ return line === "" ? line : unit + unit + line;
1221
+ });
1222
+ }
1223
+ //#endregion
1224
+ //#region src/pages-json.ts
1225
+ /**
1226
+ * tabBar 主体以文本方式渲染时,先在对象里放这个占位符序列化,最后
1227
+ * 再把渲染好的正文拼上去:按平台区分的属性需要同一个键出现多次,
1228
+ * comment-json 的对象做不到这一点,只能不用它、自己拼字符串
1229
+ * (见 {@link mergePagesJson})
1230
+ */
1231
+ const TAB_BAR_PLACEHOLDER = "@@uni-pages-tab-bar-placeholder@@";
1232
+ /**
1233
+ * 把路由数据合并进 pages.json 并写回
1234
+ *
1235
+ * 整个"读取 → 合并 → 写回"都锁在同一把文件锁里:因为新内容要参考
1236
+ * 当前文件里其他平台的 `#ifdef` 块,两个终端同时跑(比如
1237
+ * dev:mp-weixin + dev:mp-alipay)时才不会互相覆盖或读到写了一半的
1238
+ * 内容。写入用临时文件 + rename,中途崩溃也不会留下截断的文件。
1239
+ *
1240
+ * @param jsonPath - pages.json 文件路径
1241
+ * @param data - 由扫描/合并流水线组装的路由数据
1242
+ * @param options - 平台、用户配置与序列化格式
1243
+ * @returns 写入结果;获取不到文件锁时为 undefined
1244
+ */
1245
+ async function writePagesJson(jsonPath, data, options) {
1246
+ return withFileLock(jsonPath, async () => {
1247
+ const existingContent = await fs.promises.readFile(jsonPath, { encoding: "utf-8" }).catch(() => "");
1248
+ const content = mergePagesJson(existingContent, data, options);
1249
+ if (existingContent === content) {
1250
+ debug.pages("PagesJson Not have change");
1251
+ return {
1252
+ updated: false,
1253
+ content
1254
+ };
1255
+ }
1256
+ await writeFileAtomic(jsonPath, content);
1257
+ return {
1258
+ updated: true,
1259
+ content
1260
+ };
1261
+ });
1262
+ }
1263
+ /**
1264
+ * 根据现有文件内容计算合并后的 pages.json 对象
1265
+ *
1266
+ * @returns pages.json 对象;美化输出模式下附带预渲染的 tabBar 主体
1267
+ * 行,用于替换序列化出的占位符(见 {@link mergePagesJson});
1268
+ * 为 undefined 表示没有 tabBar 或为紧凑输出
1269
+ */
1270
+ function mergeIntoPagesJson(existingContent, data, options) {
1271
+ let oldConfig;
1272
+ try {
1273
+ oldConfig = parse(existingContent || "{}");
1274
+ } catch (error) {
1275
+ throw new Error(`[vite-plugin-uni-pages] Failed to parse the existing pages.json, please fix its syntax first: ${error?.message ?? error}`, { cause: error });
1276
+ }
1277
+ const { pages: oldPages, subPackages: oldSubPackages, tabBar: oldTabBar } = oldConfig;
1278
+ for (const [field, value] of Object.entries({
1279
+ pages: oldPages,
1280
+ subPackages: oldSubPackages
1281
+ })) if (value != null && !Array.isArray(value)) throw new Error(`[vite-plugin-uni-pages] The "${field}" in the existing pages.json must be an array, please fix its syntax first`);
1282
+ const { pages: _pages, subPackages: _subPackages, tabBar: _tabBar, ...pageJson } = options.globConfig || {};
1283
+ const currentPlatform = options.platform.toUpperCase();
1284
+ pageJson.pages = mergePlatformItems(oldPages, currentPlatform, data.pages, "path").items;
1285
+ ensureHomePageFirst(pageJson.pages, data.homePath);
1286
+ pageJson.subPackages = oldSubPackages || new CommentArray();
1287
+ const newSubPackages = /* @__PURE__ */ new Map();
1288
+ for (const item of data.subPackages) newSubPackages.set(item.root, item);
1289
+ const subPackagesArray = pageJson.subPackages;
1290
+ const staleRoots = [];
1291
+ for (const existing of subPackagesArray) {
1292
+ const sub = newSubPackages.get(existing.root);
1293
+ if (sub) {
1294
+ const wasGenerated = hasGenerationMarker(existing.pages);
1295
+ existing.pages = mergePlatformItems(existing.pages, currentPlatform, sub.pages, "path").items;
1296
+ if (sub.plugins) existing.plugins = sub.plugins;
1297
+ else if (wasGenerated) Reflect.deleteProperty(existing, "plugins");
1298
+ newSubPackages.delete(existing.root);
1299
+ } else if (hasGenerationMarker(existing.pages)) staleRoots.push(existing.root);
1300
+ }
1301
+ for (let i = subPackagesArray.length - 1; i >= 0; i--) {
1302
+ const existing = subPackagesArray[i];
1303
+ if (!staleRoots.includes(existing.root)) continue;
1304
+ debug.subPages(`Removing converged sub-package root: ${existing.root}`);
1305
+ Reflect.deleteProperty(subPackagesArray, Symbol.for(`before:${i}`));
1306
+ Reflect.deleteProperty(subPackagesArray, Symbol.for(`after:${i}`));
1307
+ Reflect.deleteProperty(subPackagesArray, Symbol.for(`after-value:${i}`));
1308
+ subPackagesArray.splice(i, 1);
1309
+ }
1310
+ for (const [_, newSub] of newSubPackages) {
1311
+ const subPackage = {
1312
+ root: newSub.root,
1313
+ pages: mergePlatformItems(void 0, currentPlatform, newSub.pages, "path").items
1314
+ };
1315
+ if (newSub.plugins) subPackage.plugins = newSub.plugins;
1316
+ pageJson.subPackages.push(subPackage);
1317
+ }
1318
+ const { list, ...tabBarOthers } = data.tabBar || {};
1319
+ const contributesProps = !!(list && list.length);
1320
+ const oldTabBarObj = oldTabBar;
1321
+ const tabBarMerge = mergePlatformItems(oldTabBarObj?.list, currentPlatform, list || [], "pagePath");
1322
+ let tabBarBodyLines;
1323
+ if (tabBarMerge.items.length === 0) pageJson.tabBar = void 0;
1324
+ else {
1325
+ if (isPrettyFormat(options.format)) {
1326
+ tabBarBodyLines = [...renderTabBarPropLines(mergeTabBarProps(existingContent, tabBarOthers, contributesProps, currentPlatform, tabBarMerge.platformUnion), tabBarMerge.platformUnion, options.format), ...renderTabBarListLines(tabBarMerge.items, options.format)];
1327
+ pageJson.tabBar = TAB_BAR_PLACEHOLDER;
1328
+ } else {
1329
+ const { list: _oldList, ...oldTabBarOthers } = oldTabBarObj || {};
1330
+ pageJson.tabBar = {
1331
+ ...contributesProps ? tabBarOthers : oldTabBarOthers,
1332
+ list: tabBarMerge.items
1333
+ };
1334
+ }
1335
+ const tabBarPlatformStr = tabBarMerge.owningPlatforms.join(" || ");
1336
+ if (tabBarPlatformStr && tabBarPlatformStr !== tabBarMerge.platformUnion.join(" || ")) {
1337
+ const commentPageJson = pageJson;
1338
+ commentPageJson[Symbol.for("before:tabBar")] = [lineComment(` #ifdef ${tabBarPlatformStr}`)];
1339
+ commentPageJson[Symbol.for("after:tabBar")] = [lineComment(" #endif")];
1340
+ }
1341
+ }
1342
+ return {
1343
+ pageJson,
1344
+ tabBarBodyLines
1345
+ };
1346
+ }
1347
+ /**
1348
+ * 把所有首页条目移到最前面,注释跟着条目一起移动
1349
+ *
1350
+ * 每个平台可以在自己的 #ifdef 块里声明自己的首页,没被包裹的条目
1351
+ * 所有平台都能看到。所以一个平台的首页必须排在它可见的所有条目
1352
+ * 之前。解决办法很简单:把所有平台的首页都放到所有非首页条目的
1353
+ * 前面(各组内部顺序不变),一次满足所有平台。
1354
+ *
1355
+ * 只移动当前平台的首页是不够的:就算它已经在第 0 位,另一个平台的
1356
+ * 首页可能还排在它自己可见的某个非首页条目后面。
1357
+ *
1358
+ * 判断哪些条目是首页:先看条目自带的 `type: 'home'` 标记(扫描出来
1359
+ * 的条目都带),再看 homePath 路径兜底(手写条目不带标记)。标记在
1360
+ * 合并时不会保留下来,所以每次运行都要重新判断一遍。
1361
+ */
1362
+ function ensureHomePageFirst(pagesArray, homePath) {
1363
+ if (!pagesArray || pagesArray.length === 0) return;
1364
+ const isHome = pagesArray.map((page) => page.type === "home");
1365
+ if (homePath && !pagesArray.some((page) => page.path === homePath && page.type === "home")) pagesArray.forEach((page, index) => {
1366
+ if (page.path === homePath) isHome[index] = true;
1367
+ });
1368
+ const homeCount = isHome.filter(Boolean).length;
1369
+ if (homeCount === 0) return;
1370
+ if (isHome.slice(0, homeCount).every(Boolean)) return;
1371
+ const commentArray = pagesArray;
1372
+ const length = pagesArray.length;
1373
+ const beforeTokens = [];
1374
+ const afterTokens = [];
1375
+ for (let i = 0; i < length; i++) {
1376
+ beforeTokens.push(commentArray[Symbol.for(`before:${i}`)] || []);
1377
+ afterTokens.push(commentArray[Symbol.for(`after:${i}`)] || []);
1378
+ Reflect.deleteProperty(commentArray, Symbol.for(`before:${i}`));
1379
+ Reflect.deleteProperty(commentArray, Symbol.for(`after:${i}`));
1380
+ }
1381
+ const markerTokens = beforeTokens[0].filter(isGenerationMarker);
1382
+ beforeTokens[0] = beforeTokens[0].filter((token) => !isGenerationMarker(token));
1383
+ const order = [];
1384
+ for (let i = 0; i < length; i++) if (isHome[i]) order.push(i);
1385
+ for (let i = 0; i < length; i++) if (!isHome[i]) order.push(i);
1386
+ pagesArray.splice(0, length, ...order.map((i) => pagesArray[i]));
1387
+ for (let i = 0; i < length; i++) {
1388
+ const sourceIndex = order[i];
1389
+ const before = i === 0 ? [...markerTokens, ...beforeTokens[sourceIndex]] : beforeTokens[sourceIndex];
1390
+ if (before.length > 0) commentArray[Symbol.for(`before:${i}`)] = before;
1391
+ if (afterTokens[sourceIndex].length > 0) commentArray[Symbol.for(`after:${i}`)] = afterTokens[sourceIndex];
1392
+ }
1393
+ }
1394
+ /**
1395
+ * 判断输出是不是带注释的多行格式(只有多行才放得下按平台区分的
1396
+ * `#ifdef` 块)。`indent: 0` 或空字符串会让 comment-json 输出成
1397
+ * 单行,这里和它的行为保持一致。
1398
+ */
1399
+ function isPrettyFormat(format) {
1400
+ return !(format?.minify ?? false) && !!(format?.indent ?? 2);
1401
+ }
1402
+ /**
1403
+ * 不碰文件系统,只做计算:把路由数据合并进现有 pages.json 文本
1404
+ * (多平台 `#ifdef` 块、首页重排、生成标记、过期子包清理)并序列化
1405
+ * 成字符串。文件锁、变更检测和原子写入都在 {@link writePagesJson}
1406
+ * 里;测试直接用字符串检查这个函数。
1407
+ *
1408
+ * @param existingContent - 当前 pages.json 文本,没有时传空字符串
1409
+ * @param data - 由扫描/合并流水线组装的路由数据
1410
+ * @param options - 平台、用户配置与序列化格式
1411
+ * @returns 序列化后的 pages.json 内容
1412
+ */
1413
+ function mergePagesJson(existingContent, data, options) {
1414
+ const { pageJson, tabBarBodyLines } = mergeIntoPagesJson(existingContent, data, options);
1415
+ const minify = options.format?.minify ?? false;
1416
+ const indent = options.format?.indent ?? 2;
1417
+ const eol = options.format?.eol ?? "\n";
1418
+ let content = stringify(pageJson, null, minify ? void 0 : indent);
1419
+ if (tabBarBodyLines) {
1420
+ const unit = typeof indent === "number" ? " ".repeat(indent) : indent;
1421
+ content = content.replace(`"tabBar": ${JSON.stringify(TAB_BAR_PLACEHOLDER)}`, () => `"tabBar": {\n${tabBarBodyLines.join("\n")}\n${unit}}`);
1422
+ }
1423
+ if (eol !== "\n") content = content.replaceAll("\n", eol);
1424
+ if (options.format?.insertFinalNewline) content += eol;
1425
+ return content;
1426
+ }
1427
+ /** 构建用于序列化 pages.json 输出的行注释令牌 */
1428
+ function lineComment(value) {
1429
+ return {
1430
+ type: "LineComment",
1431
+ value,
1432
+ inline: false,
1433
+ loc: {
1434
+ start: {
1435
+ line: 0,
1436
+ column: 0
1437
+ },
1438
+ end: {
1439
+ line: 0,
1440
+ column: 0
1441
+ }
1442
+ }
1443
+ };
1444
+ }
1445
+ /**
1446
+ * 把条目转成字符串用于比较,比较前要先把内部的 `type` 标记
1447
+ * ('home' | 'page')拿掉。
1448
+ *
1449
+ * 从 pages.json 读回来的条目可能没有这个标记(手写条目从不带它),
1450
+ * 直接 JSON.stringify 会把同一个页面当成两个不同的条目,多平台
1451
+ * 运行时就会生成重复的路由
1452
+ * (见 https://github.com/uni-helper/vite-plugin-uni-pages/issues/283)。
1453
+ *
1454
+ * 键的顺序也参与比较(JSON.stringify 按写入顺序输出):同一页面在
1455
+ * 两个平台下键序不同时不会被合并,会各占一个 #ifdef 块留在文件
1456
+ * 里。代价只是文件里多一份内容相同的变体,构建时每个平台仍然只看
1457
+ * 得见自己那一条、不会出现重复路由,不值得为它做键序归一化。
1458
+ */
1459
+ function stringifyForCompare(val) {
1460
+ if ("type" in val) {
1461
+ const { type: _type, ...rest } = val;
1462
+ return JSON.stringify(rest);
1463
+ }
1464
+ return JSON.stringify(val);
1465
+ }
1466
+ /**
1467
+ * 两个内容相同的条目,首页状态是否一致?
1468
+ *
1469
+ * `type` 标记不参与内容比较(见 stringifyForCompare),没有标记的
1470
+ * 条目也能和扫描到的条目合并。但如果两边都明确带着标记、而且一个
1471
+ * 是 'home' 一个是 'page',说明这是两个平台各自声明的不同首页,
1472
+ * 必须作为两个条目待在各自的 #ifdef 块里,不能合并成一条——之前
1473
+ * 的合并正是"切换首页后旧首页标记残留、某个平台的首页声明被悄悄
1474
+ * 丢掉"这两个 bug 的根源。
1475
+ */
1476
+ function homeStatusCompatible(a, b) {
1477
+ const typeA = a.type;
1478
+ const typeB = b.type;
1479
+ return typeA === void 0 || typeB === void 0 || typeA === typeB;
1480
+ }
1481
+ /**
1482
+ * 合并多平台的配置条目(数组侧适配器)。
1483
+ *
1484
+ * 处理条件编译注释(#ifdef / #ifndef / #endif),把 pages.json 里
1485
+ * 已有的条目和本次扫描的条目合并成一个数组;合并规则(同值叠平台、
1486
+ * 排除当前平台、排序、并集)用 platform-variants.ts 的共享核心:
1487
+ * - 内容相同的条目自动合并,平台标识叠在一起
1488
+ * - 内容不同的条目各写各的 #ifdef 块
1489
+ * - 手写的 `#ifndef` 块原样保留:否定条件翻译不成正向平台列表,
1490
+ * 所以条目连同原始包裹一起输出,永不修改、也永不清理
1491
+ *
1492
+ * @param source - 现有条目数组(来自 pages.json)
1493
+ * @param currentPlatform - 当前平台标识(如 H5、MP-WEIXIN)
1494
+ * @param items - 新配置条目数组
1495
+ * @param uniqueKeyName - 用于识别条目唯一性的字段名(如 'path' 或 'pagePath')
1496
+ * @returns 带 #ifdef 块的合并条目,以及平台全集与拥有平台
1497
+ */
1498
+ function mergePlatformItems(source, currentPlatform, items, uniqueKeyName) {
1499
+ const src = source || new CommentArray();
1500
+ currentPlatform = currentPlatform.toUpperCase();
1501
+ const lastPlatforms = extractLastPlatforms(src, currentPlatform);
1502
+ const mergedMap = /* @__PURE__ */ new Map();
1503
+ const variantsOf = (uniqueKey) => {
1504
+ const list = mergedMap.get(uniqueKey) || [];
1505
+ mergedMap.set(uniqueKey, list);
1506
+ return list;
1507
+ };
1508
+ for (let i = 0; i < src.length; i++) {
1509
+ const item = src[i];
1510
+ const uniqueKey = item[uniqueKeyName];
1511
+ if (!uniqueKey) continue;
1512
+ const beforeComments = src[Symbol.for(`before:${i}`)];
1513
+ const conditionalComment = beforeComments?.find((c) => c.type !== "BlankLine" && /^#(?:ifdef|ifndef)/.test(c.value.trim()));
1514
+ if (conditionalComment?.value.trim().startsWith("#ifndef")) {
1515
+ variantsOf(uniqueKey).push({
1516
+ payload: item,
1517
+ valueStr: stringifyForCompare(item),
1518
+ platforms: null,
1519
+ platformStr: "",
1520
+ passthrough: {
1521
+ before: [...beforeComments || []].filter((c) => !isGenerationMarker(c)),
1522
+ after: [lineComment(" #endif")]
1523
+ }
1524
+ });
1525
+ continue;
1526
+ }
1527
+ let platforms = [...lastPlatforms];
1528
+ if (conditionalComment) {
1529
+ const match = conditionalComment.value.match(/#ifdef\s+(.+)/);
1530
+ if (match) platforms = platformsExcluding(match[1], currentPlatform);
1531
+ }
1532
+ if (platforms.length === 0) continue;
1533
+ variantsOf(uniqueKey).push({
1534
+ payload: item,
1535
+ valueStr: stringifyForCompare(item),
1536
+ platforms,
1537
+ platformStr: platforms.join(" || ")
1538
+ });
1539
+ }
1540
+ for (const item of items) {
1541
+ const uniqueKey = item[uniqueKeyName];
1542
+ if (!uniqueKey) continue;
1543
+ upsertCurrentVariant(variantsOf(uniqueKey), item, stringifyForCompare(item), currentPlatform, homeStatusCompatible);
1544
+ }
1545
+ const result = new CommentArray();
1546
+ for (const variants of mergedMap.values()) sortVariants(variants);
1547
+ const platformUnion = resolvePlatformUnion(mergedMap, currentPlatform, lastPlatforms);
1548
+ const platformUnionStr = platformUnion.join(" || ");
1549
+ const owningPlatforms = collectVariantPlatforms(mergedMap);
1550
+ result[Symbol.for("before:0")] = [lineComment(` ${GENERATION_MARKER_PREFIX} ${platformUnionStr}`)];
1551
+ for (const [_, list] of mergedMap) for (const { payload: item, platformStr, passthrough } of list) {
1552
+ result.push(item);
1553
+ if (passthrough) {
1554
+ result[Symbol.for(`before:${result.length - 1}`)] = [...result[Symbol.for(`before:${result.length - 1}`)] || [], ...passthrough.before];
1555
+ if (passthrough.after.length > 0) result[Symbol.for(`after:${result.length - 1}`)] = passthrough.after;
1556
+ continue;
1557
+ }
1558
+ if (platformStr !== platformUnionStr) {
1559
+ result[Symbol.for(`before:${result.length - 1}`)] = [...result[Symbol.for(`before:${result.length - 1}`)] || [], lineComment(` #ifdef ${platformStr}`)];
1560
+ result[Symbol.for(`after:${result.length - 1}`)] = [lineComment(" #endif")];
1561
+ }
1562
+ }
1563
+ return {
1564
+ items: result,
1565
+ platformUnion,
1566
+ owningPlatforms: [...owningPlatforms].sort()
1567
+ };
1568
+ }
1569
+ //#endregion
1570
+ //#region src/context.ts
1571
+ /**
1572
+ * 页面上下文:负责扫描页面、加载配置、合并页面信息、生成 pages.json
1573
+ *
1574
+ * 做的事情按固定顺序是:加载用户配置 → 扫描页面文件 → 合并页面
1575
+ * 信息 → 写入 pages.json。这个顺序由 {@link updatePagesJSON}(完整
1576
+ * 流程)或 {@link scanAndMerge}(只算不写)管理,调用方不用关心
1577
+ * 内部步骤。
1578
+ */
1579
+ var PageContext = class {
1580
+ _server;
1581
+ /** 从用户配置文件(pages.config.ts)解析出的配置对象 */
1582
+ pagesGlobConfig;
1583
+ /** 用户配置文件的来源路径列表 */
1584
+ pagesConfigSourcePaths = [];
1585
+ /** 主包页面映射,键为页面文件的绝对路径 */
657
1586
  pages = /* @__PURE__ */ new Map();
658
- /** Sub-package page map, key is the sub-package root directory, value is the page map under that sub-package */
1587
+ /** 子包页面映射,键为子包根目录,值为该子包下的页面映射 */
659
1588
  subPages = /* @__PURE__ */ new Map();
660
- /** Main package page metadata array for generating pages.json pages field */
1589
+ /** 主包页面配置数组,用于生成 pages.json 的 pages 字段 */
661
1590
  pageMetaData = [];
662
- /** Sub-package page metadata array for generating pages.json subPackages field */
1591
+ /** 子包页面配置数组,用于生成 pages.json 的 subPackages 字段 */
663
1592
  subPageMetaData = [];
664
- /** Generated pages.json file path */
1593
+ /**
1594
+ * 最近一次合并出的 tabBar(配置文件与 definePage 声明的合体)。
1595
+ * 类型声明生成 tab 页列表时用它,definePage 声明的 tabBar 页才能
1596
+ * 一起从 navigateTo 的 url 类型里排除
1597
+ */
1598
+ tabBar;
1599
+ /** 生成的 pages.json 文件路径 */
665
1600
  resolvedPagesJSONPath = "";
666
- resolvedPagesJSONIndent;
667
- resolvedPagesJSONNewline;
668
- resolvedPagesJSONEofNewline;
669
- /** Original options passed by the user */
670
- rawOptions;
671
- /** Project root directory */
1601
+ /** 项目根目录 */
672
1602
  root;
673
- /** Resolved configuration options */
1603
+ /** 解析后的配置项 */
674
1604
  options;
1605
+ /** 当前平台标识,如 'mp-weixin';由调用方传入,不依赖模块加载时冻结的环境 */
1606
+ platform;
675
1607
  logger;
676
- /** Whether to work with vite-plugin-uni-platform plugin */
1608
+ /** 是否与 vite-plugin-uni-platform 插件协同工作 */
677
1609
  withUniPlatform = false;
678
- /** Cached previous pages.json content to avoid redundant writes */
679
- lastPagesJson = "";
1610
+ /** pages.json 中页面路径的解析基准路径 */
1611
+ get basePath() {
1612
+ return resolveBasePath(this.options);
1613
+ }
680
1614
  /**
681
- * Create a PageContext instance
682
- * @param userOptions - User configuration options
683
- * @param viteRoot - Vite project root directory, defaults to current working directory
1615
+ * 创建 PageContext 实例
1616
+ * @param userOptions - 用户配置项
1617
+ * @param viteRoot - Vite 项目根目录,默认为当前工作目录
1618
+ * @param platform - 当前平台标识,默认取 uni-env 的平台
684
1619
  */
685
- constructor(userOptions, viteRoot = process.cwd()) {
686
- this.rawOptions = userOptions;
687
- this.root = slash(viteRoot);
1620
+ constructor(userOptions, viteRoot = process.cwd(), platform$1 = platform) {
1621
+ this.root = normalizePath(viteRoot);
1622
+ this.platform = platform$1;
688
1623
  debug.options("root", this.root);
689
1624
  this.options = resolveOptions(userOptions, this.root);
690
1625
  const debugOption = this.options.debug;
@@ -693,19 +1628,19 @@ var PageContext = class {
693
1628
  const suffix = typeof debugOption === "boolean" ? "*" : debugOption;
694
1629
  Debug.enable(`${prefix}${suffix}`);
695
1630
  }
696
- this.resolvedPagesJSONPath = path.join(this.root, this.options.outDir, OUTPUT_NAME);
1631
+ this.resolvedPagesJSONPath = resolvePagesJsonPath(this.root, this.options.outDir);
697
1632
  debug.options(this.options);
698
1633
  }
699
1634
  /**
700
- * Set the Vite logger
701
- * @param logger - Vite logger instance
1635
+ * 设置 Vite logger
1636
+ * @param logger - Vite logger 实例
702
1637
  */
703
1638
  setLogger(logger) {
704
1639
  this.logger = logger;
705
1640
  }
706
1641
  /**
707
- * Load user page configuration file (e.g. pages.config.ts)
708
- * Uses unconfig to load configuration, supporting multiple config file formats
1642
+ * 加载用户页面配置文件(如 pages.config.ts)
1643
+ * 使用 unconfig 加载配置,支持多种配置文件格式
709
1644
  */
710
1645
  async loadUserPagesConfig() {
711
1646
  const configSource = this.options.configSource;
@@ -715,51 +1650,22 @@ var PageContext = class {
715
1650
  defaults: {}
716
1651
  });
717
1652
  this.pagesGlobConfig = config.default || config;
718
- this.pagesConfigSourcePaths = sources;
1653
+ this.pagesConfigSourcePaths = sources.map(normalizePath);
719
1654
  debug.options(this.pagesGlobConfig);
720
1655
  }
721
1656
  /**
722
- * Scan main package page directories and collect all page file paths
723
- * Scan corresponding directories based on the configured dirs option
724
- */
725
- async scanPages() {
726
- const paths = this.options.dirs.map((dir) => {
727
- return {
728
- dir,
729
- files: getPagePaths(dir, this.options)
730
- };
731
- }).map((page) => page.files).flat();
732
- debug.pages(paths);
733
- const pages = /* @__PURE__ */ new Map();
734
- for (const path of paths) {
735
- const page = this.pages.get(path.absolutePath) || new Page(this, path);
736
- pages.set(path.absolutePath, page);
737
- }
738
- this.pages = pages;
739
- }
740
- /**
741
- * Scan sub-package page directories and collect all sub-package page file paths
742
- * Scan corresponding directories based on the configured subPackages option
1657
+ * 按顺序跑完扫描与合并:先扫描主包和子包的页面,再合并它们的
1658
+ * 信息。步骤顺序只维护在这里,调用方和测试都不用关心。
743
1659
  */
744
- async scanSubPages() {
745
- const paths = {};
746
- const subPages = /* @__PURE__ */ new Map();
747
- for (const dir of this.options.subPackages) {
748
- const pagePaths = getPagePaths(dir, this.options);
749
- paths[dir] = pagePaths;
750
- const pages = /* @__PURE__ */ new Map();
751
- for (const path of pagePaths) {
752
- const page = this.subPages.get(dir)?.get(path.absolutePath) || new Page(this, path);
753
- pages.set(path.absolutePath, page);
754
- }
755
- subPages.set(dir, pages);
756
- }
757
- debug.subPages(JSON.stringify(paths, null, 2));
758
- this.subPages = subPages;
1660
+ async scanAndMerge() {
1661
+ await this.scanPages();
1662
+ await this.scanSubPages();
1663
+ await this.mergePageMetaData();
1664
+ await this.mergeSubPageMetaData();
759
1665
  }
760
1666
  /**
761
- * Set up Vite dev server for HMR and file watching
762
- * @param server - Vite dev server instance
1667
+ * 设置 Vite 开发服务器,用于 HMR 与文件监听
1668
+ * @param server - Vite 开发服务器实例
763
1669
  */
764
1670
  setupViteServer(server) {
765
1671
  if (this._server === server) return;
@@ -767,47 +1673,45 @@ var PageContext = class {
767
1673
  this.setupWatcher(server.watcher);
768
1674
  }
769
1675
  /**
770
- * Set up file watcher to monitor page file and config file changes
771
- * Automatically update pages.json when page files or config files change
772
- * @param watcher - chokidar file watcher instance
1676
+ * 设置文件监听器,监听页面文件与配置文件变更
1677
+ * 页面文件或配置文件变化时自动更新 pages.json
1678
+ * @param watcher - chokidar 文件监听器实例
773
1679
  */
774
1680
  async setupWatcher(watcher) {
775
1681
  watcher.add(this.pagesConfigSourcePaths);
776
- const targetDirs = [...this.options.dirs, ...this.options.subPackages].map((v) => slash(path.resolve(this.root, v)));
777
- const isInTargetDirs = (filePath) => targetDirs.some((v) => slash(path.resolve(this.root, filePath)).startsWith(v));
1682
+ const targetDirs = [...this.options.dirs, ...this.options.subPackages].map((v) => normalizePath(path.resolve(this.root, v)));
1683
+ const isWatchedPageFile = (filePath) => {
1684
+ const absolute = normalizePath(path.resolve(this.root, filePath));
1685
+ return isTargetFile(filePath) && targetDirs.some((v) => absolute === v || absolute.startsWith(`${v}/`));
1686
+ };
778
1687
  watcher.on("add", async (path) => {
779
- path = slash(path);
780
- if (!isTargetFile(path)) return;
781
- if (!isInTargetDirs(path)) return;
1688
+ path = normalizePath(path);
1689
+ if (!isWatchedPageFile(path)) return;
782
1690
  debug.pages(`File added: ${path}`);
783
1691
  if (await this.updatePagesJSON()) this.onUpdate();
784
1692
  });
785
1693
  watcher.on("change", async (path) => {
786
- path = slash(path);
787
- if (!isTargetFile(path)) return;
788
- if (!isInTargetDirs(path)) return;
789
- debug.pages(`File changed: ${path}`);
790
- debug.pages(targetDirs);
791
- debug.pages(isInTargetDirs(path));
792
- if (await this.updatePagesJSON(path)) this.onUpdate();
793
- });
794
- watcher.on("change", async (path) => {
1694
+ path = normalizePath(path);
795
1695
  if (this.pagesConfigSourcePaths.includes(path)) {
796
1696
  debug.pages(`Config source changed: ${path}`);
797
1697
  if (await this.updatePagesJSON()) this.onUpdate();
1698
+ return;
798
1699
  }
1700
+ path = normalizePath(path);
1701
+ if (!isWatchedPageFile(path)) return;
1702
+ debug.pages(`File changed: ${path}`);
1703
+ if (await this.updatePagesJSON(path)) this.onUpdate();
799
1704
  });
800
1705
  watcher.on("unlink", async (path) => {
801
- path = slash(path);
802
- if (!isTargetFile(path)) return;
803
- if (!isInTargetDirs(path)) return;
1706
+ path = normalizePath(path);
1707
+ if (!isWatchedPageFile(path)) return;
804
1708
  debug.pages(`File removed: ${path}`);
805
1709
  if (await this.updatePagesJSON()) this.onUpdate();
806
1710
  });
807
1711
  }
808
1712
  /**
809
- * Page update callback, triggered when page files or configuration changes
810
- * Responsible for invalidating virtual modules and notifying the browser to do a full reload
1713
+ * 页面更新回调,页面文件或配置变化时触发
1714
+ * 负责使虚拟模块失效并通知浏览器整页刷新
811
1715
  */
812
1716
  onUpdate() {
813
1717
  if (!this._server) return;
@@ -816,115 +1720,19 @@ var PageContext = class {
816
1720
  this._server.ws.send({ type: "full-reload" });
817
1721
  }
818
1722
  /**
819
- * parse pages rules && set page type
820
- * @param pages page path array
821
- * @param type page type
822
- * @param overrides custom page config
823
- * @returns pages rules
824
- */
825
- async parsePages(pages, type, overrides) {
826
- const jobs = [];
827
- for (const [_, page] of pages) jobs.push(page.getPageMeta());
828
- const generatedPageMetaData = await Promise.all(jobs);
829
- const customPageMetaData = overrides || [];
830
- const result = customPageMetaData.length ? mergePageMetaDataArray(generatedPageMetaData.concat(customPageMetaData)) : generatedPageMetaData;
831
- const parseMeta = Array.from(result.reduce((map, page) => {
832
- map.set(page.path, page);
833
- return map;
834
- }, /* @__PURE__ */ new Map()).values());
835
- return type === "main" ? this.setHomePage(parseMeta) : parseMeta;
836
- }
837
- /**
838
- * set home page
839
- * @param result pages rules array
840
- * @returns pages rules
841
- */
842
- setHomePage(result) {
843
- if (!result.some(({ type }) => type === "home")) {
844
- const basePath = slash(path.join(this.options.root, this.options.outDir));
845
- const resolvedHomePages = this.options.homePage.map((v) => {
846
- return slash(path.relative(basePath, slash(path.resolve(basePath, v))));
847
- });
848
- const matchHomePage = (itemPath, configPath) => {
849
- if (itemPath === configPath) return true;
850
- const normalizedItem = itemPath.replace(/\\/g, "/");
851
- const normalizedConfig = configPath.replace(/\\/g, "/");
852
- return normalizedItem.endsWith(`/${normalizedConfig}`);
853
- };
854
- if (!result.some((item) => {
855
- const isFound = resolvedHomePages.some((expectedPath) => matchHomePage(item.path, expectedPath));
856
- if (isFound) item.type = "home";
857
- return isFound;
858
- })) this.logger?.warn("No home page found, check the configuration of pages.config.ts, or add the `homePage` option to UniPages in vite.config.js, or add `definePage({ type: \"home\" })` in your vue page.", { timestamp: true });
859
- }
860
- result.sort((page) => page.type === "home" ? -1 : 0);
861
- return result;
862
- }
863
- /**
864
- * Merge main package page metadata
865
- * Filter out pages belonging to sub-packages, then parse page metadata and merge user configuration
866
- */
867
- async mergePageMetaData() {
868
- const subPageAbsolutePaths = Array.from(this.subPages.values()).flatMap((v) => Array.from(v.keys()));
869
- for (const subPageAbsolutePath of subPageAbsolutePaths) this.pages.delete(subPageAbsolutePath);
870
- const pageMetaData = await this.parsePages(this.pages, "main", this.pagesGlobConfig?.pages);
871
- this.pageMetaData = pageMetaData;
872
- debug.pages(this.pageMetaData);
873
- }
874
- /**
875
- * Merge sub-package page metadata
876
- * Parse page metadata for each sub-package and handle sub-package configuration inheritance
877
- * Preserves sub-package level properties like plugins from user config
878
- */
879
- async mergeSubPageMetaData() {
880
- const subPageMaps = {};
881
- const subPlugins = {};
882
- const subPackages = this.pagesGlobConfig?.subPackages || [];
883
- for (const [dir, pages] of this.subPages) {
884
- const basePath = slash(path.join(this.options.root, this.options.outDir));
885
- const root = this.options.subPackageRootMap.get(dir) ?? slash(path.relative(basePath, path.join(this.options.root, dir)));
886
- const globPackage = subPackages?.find((v) => v.root === root);
887
- subPageMaps[root] = await this.parsePages(pages, "sub", globPackage?.pages);
888
- subPageMaps[root] = subPageMaps[root].map((page) => ({
889
- ...page,
890
- path: slash(path.relative(root, page.path))
891
- }));
892
- if (globPackage?.plugins) subPlugins[root] = globPackage.plugins;
893
- }
894
- for (const { root, pages, plugins } of subPackages) if (root && !subPageMaps[root]) {
895
- subPageMaps[root] = pages || [];
896
- if (plugins) subPlugins[root] = plugins;
897
- }
898
- const subPageMetaData = Object.keys(subPageMaps).map((root) => ({
899
- root,
900
- pages: subPageMaps[root],
901
- ...subPlugins[root] && { plugins: subPlugins[root] }
902
- })).filter((meta) => meta.pages.length > 0);
903
- this.subPageMetaData = subPageMetaData;
904
- debug.subPages(this.subPageMetaData);
905
- }
906
- /**
907
- * Update pages.json file
908
- * This is the core method responsible for coordinating the entire page configuration generation flow:
909
- * 1. Check file changes (if filepath is specified)
910
- * 2. Load user configuration
911
- * 3. Scan page files
912
- * 4. Merge page metadata
913
- * 5. Generate and write pages.json
914
- * @param filepath - Changed file path for incremental update judgment
915
- * @returns Whether pages.json was successfully updated
1723
+ * 更新 pages.json 文件
1724
+ * 这是负责协调整个页面配置生成流程的核心方法:
1725
+ * 1. 检查文件变更(指定 filepath 时)
1726
+ * 2. 加载用户配置
1727
+ * 3. 扫描页面文件
1728
+ * 4. 合并页面配置
1729
+ * 5. 生成并写入 pages.json
1730
+ * @param filepath - 发生变更的文件路径,用于增量更新判断
1731
+ * @returns pages.json 是否成功更新
916
1732
  */
917
1733
  async updatePagesJSON(filepath) {
918
1734
  if (filepath) {
919
- let page = this.pages.get(filepath);
920
- if (!page) {
921
- let subPage;
922
- for (const [_, pages] of this.subPages) {
923
- subPage = pages.get(filepath);
924
- if (subPage) break;
925
- }
926
- page = subPage;
927
- }
1735
+ const page = this.findPage(filepath);
928
1736
  if (page) {
929
1737
  await page.read();
930
1738
  if (!page.hasChanged()) {
@@ -934,66 +1742,69 @@ var PageContext = class {
934
1742
  }
935
1743
  }
936
1744
  checkPagesJsonFileSync(this.resolvedPagesJSONPath);
937
- this.options.onBeforeLoadUserConfig(this);
1745
+ this.options.onBeforeLoadUserConfig();
938
1746
  await this.loadUserPagesConfig();
939
- this.options.onAfterLoadUserConfig(this);
1747
+ this.options.onAfterLoadUserConfig(this.pagesGlobConfig);
940
1748
  if (this.options.mergePages) {
941
- this.options.onBeforeScanPages(this);
1749
+ this.options.onBeforeScanPages();
942
1750
  await this.scanPages();
943
1751
  await this.scanSubPages();
944
- this.options.onAfterScanPages(this);
1752
+ this.options.onAfterScanPages(this.pages, this.subPages);
945
1753
  }
946
- this.options.onBeforeMergePageMetaData(this);
1754
+ this.options.onBeforeMergePageMetaData(this.pages, this.pagesGlobConfig);
947
1755
  await this.mergePageMetaData();
948
1756
  await this.mergeSubPageMetaData();
949
- this.options.onAfterMergePageMetaData(this);
950
- const pagesMap = /* @__PURE__ */ new Map();
951
- (this.withUniPlatform ? this.pageMetaData.filter((v) => !/\..*$/.test(v.path) || v.path.includes(platform)).map((v) => {
952
- v.path = v.path.replace(/\..*$/, "");
953
- return v;
954
- }) : this.pageMetaData).forEach((v) => pagesMap.set(v.path, v));
955
- this.pageMetaData = [...pagesMap.values()];
956
- this.options.onBeforeWriteFile(this);
957
- const updated = await withFileLock(this.resolvedPagesJSONPath, async () => {
958
- const pagesJson = stringify(await this.genratePagesJSON(), null, this.options.minify ? void 0 : await this.getIndent()) + (await this.getEndOfLine() ? await this.getNewline() : "");
959
- if (this.lastPagesJson === pagesJson) {
960
- debug.pages("PagesJson Not have change");
961
- return false;
1757
+ this.options.onAfterMergePageMetaData(this.pageMetaData, this.subPageMetaData);
1758
+ const pages = this.withUniPlatform ? filterPlatformSuffixPages(this.pageMetaData, this.platform) : this.pageMetaData;
1759
+ this.pageMetaData = dedupeByPath(pages);
1760
+ this.options.onBeforeWriteFile(this.resolvedPagesJSONPath);
1761
+ const homePath = this.pageMetaData.find((meta) => meta.type === "home")?.path;
1762
+ this.tabBar = await this.resolveTabBar();
1763
+ const result = await writePagesJson(this.resolvedPagesJSONPath, {
1764
+ pages: this.pageMetaData,
1765
+ subPackages: this.subPageMetaData,
1766
+ tabBar: this.tabBar,
1767
+ homePath
1768
+ }, {
1769
+ platform: this.platform,
1770
+ globConfig: this.pagesGlobConfig,
1771
+ format: {
1772
+ minify: this.options.minify,
1773
+ indent: this.options.indent,
1774
+ eol: this.options.eol,
1775
+ insertFinalNewline: this.options.insertFinalNewline
962
1776
  }
963
- await writeFileAtomic(this.resolvedPagesJSONPath, pagesJson);
964
- this.lastPagesJson = pagesJson;
965
- return true;
966
1777
  });
967
1778
  this.generateDeclaration();
968
- if (updated) this.options.onAfterWriteFile(this);
969
- return updated ?? false;
1779
+ if (result?.updated) this.options.onAfterWriteFile(this.resolvedPagesJSONPath, result.content);
1780
+ return result?.updated ?? false;
970
1781
  }
971
1782
  /**
972
- * Generate virtual module content
973
- * Returns JavaScript code containing pages and subPackages exports
974
- * @returns Virtual module code string
1783
+ * 生成虚拟模块内容
1784
+ * 返回包含 pages 与 subPackages 导出的 JavaScript 代码
1785
+ * @returns 虚拟模块代码字符串
975
1786
  */
976
1787
  virtualModule() {
977
1788
  return [`export const pages = ${this.resolveRoutes()};`, `export const subPackages = ${this.resolveSubRoutes()};`].join("\n");
978
1789
  }
979
1790
  /**
980
- * Resolve main package route data to JSON string
981
- * @returns JSON string of main package page metadata
1791
+ * 将主包路由数据解析为 JSON 字符串
1792
+ * @returns 主包页面配置的 JSON 字符串
982
1793
  */
983
1794
  resolveRoutes() {
984
1795
  return stringify(this.pageMetaData, null, 2);
985
1796
  }
986
1797
  /**
987
- * Resolve sub-package route data to JSON string
988
- * @returns JSON string of sub-package page metadata
1798
+ * 将子包路由数据解析为 JSON 字符串
1799
+ * @returns 子包页面配置的 JSON 字符串
989
1800
  */
990
1801
  resolveSubRoutes() {
991
1802
  return stringify(this.subPageMetaData, null, 2);
992
1803
  }
993
1804
  /**
994
- * Resolve tabBar configuration
995
- * Merges page-defined tabBar items with config-defined tabBar
996
- * @returns Merged tabBar configuration object, or undefined if no tabBar exists
1805
+ * 解析 tabBar 配置
1806
+ * 将页面定义的 tabBar 项与配置文件定义的 tabBar 合并
1807
+ * @returns 合并后的 tabBar 配置对象,无 tabBar 时为 undefined
997
1808
  */
998
1809
  async resolveTabBar() {
999
1810
  const tabBarItems = [];
@@ -1006,8 +1817,7 @@ var PageContext = class {
1006
1817
  ...this.pagesGlobConfig?.tabBar,
1007
1818
  list: this.pagesGlobConfig?.tabBar?.list || []
1008
1819
  };
1009
- const pagePaths = /* @__PURE__ */ new Map();
1010
- for (const item of tabBar.list) pagePaths.set(item.pagePath, true);
1820
+ const pagePaths = new Set(tabBar.list.map((item) => item.pagePath));
1011
1821
  tabBarItems.sort((a, b) => a.index - b.index);
1012
1822
  for (const item of tabBarItems) if (!pagePaths.has(item.pagePath)) {
1013
1823
  const { index: _, ...tabbar } = item;
@@ -1016,262 +1826,322 @@ var PageContext = class {
1016
1826
  return tabBar;
1017
1827
  }
1018
1828
  /**
1019
- * Generate TypeScript declaration file
1020
- * Generate type definitions for page paths to provide type hints during navigation
1829
+ * 生成 TypeScript 声明文件
1830
+ * 为页面路径生成类型定义,导航时提供类型提示
1021
1831
  */
1022
1832
  generateDeclaration() {
1023
1833
  if (!this.options.dts) return;
1024
1834
  debug.declaration("generating");
1025
- return writeDeclaration(this, this.options.dts);
1026
- }
1027
- async genratePagesJSON() {
1028
- const { pages: oldPages, subPackages: oldSubPackages, tabBar: oldTabBar } = parse(await fs.promises.readFile(this.resolvedPagesJSONPath, { encoding: "utf-8" }).catch(() => "") || "{}");
1029
- const { pages: _pages, subPackages: _subPackages, tabBar: _tabBar, ...pageJson } = this.pagesGlobConfig || {};
1030
- const currentPlatform = platform.toUpperCase();
1031
- pageJson.pages = mergePlatformItems(oldPages, currentPlatform, this.pageMetaData, "path");
1032
- if (pageJson.pages && pageJson.pages.length > 0) {
1033
- const pagesArray = pageJson.pages;
1034
- const homeIndex = pagesArray.findIndex((page) => page.type === "home");
1035
- if (homeIndex > 0) {
1036
- const [homePage] = pagesArray.splice(homeIndex, 1);
1037
- pagesArray.unshift(homePage);
1038
- }
1835
+ return writeDeclaration({
1836
+ pages: this.pageMetaData,
1837
+ subPackages: this.subPageMetaData,
1838
+ tabBar: this.tabBar,
1839
+ globConfig: this.pagesGlobConfig
1840
+ }, this.options.dts);
1841
+ }
1842
+ /**
1843
+ * 扫描主包页面目录并收集所有页面文件路径
1844
+ * 根据配置的 dirs 选项扫描对应目录
1845
+ */
1846
+ async scanPages() {
1847
+ const paths = this.options.dirs.flatMap((dir) => getPagePaths(dir, this.options));
1848
+ debug.pages(paths);
1849
+ const pages = /* @__PURE__ */ new Map();
1850
+ for (const path of paths) {
1851
+ const page = this.pages.get(path.absolutePath) || new Page(this, path);
1852
+ pages.set(path.absolutePath, page);
1039
1853
  }
1040
- pageJson.subPackages = oldSubPackages || new CommentArray();
1041
- const newSubPackages = /* @__PURE__ */ new Map();
1042
- for (const item of this.subPageMetaData) newSubPackages.set(item.root, item);
1043
- for (const existing of pageJson.subPackages) {
1044
- const sub = newSubPackages.get(existing.root);
1045
- if (sub) {
1046
- existing.pages = mergePlatformItems(existing.pages, currentPlatform, sub.pages, "path");
1047
- if (sub.plugins) existing.plugins = sub.plugins;
1048
- newSubPackages.delete(existing.root);
1049
- }
1854
+ this.pages = pages;
1855
+ }
1856
+ /**
1857
+ * 按页面文件绝对路径查找被跟踪的页面(主包或任一子包)
1858
+ * @param filepath - 页面文件的绝对路径
1859
+ * @returns 被跟踪的页面,文件未被跟踪时为 undefined
1860
+ */
1861
+ findPage(filepath) {
1862
+ const mainPage = this.pages.get(filepath);
1863
+ if (mainPage) return mainPage;
1864
+ for (const pages of this.subPages.values()) {
1865
+ const subPage = pages.get(filepath);
1866
+ if (subPage) return subPage;
1050
1867
  }
1051
- for (const [_, newSub] of newSubPackages) {
1052
- const subPackage = {
1053
- root: newSub.root,
1054
- pages: mergePlatformItems(void 0, currentPlatform, newSub.pages, "path")
1055
- };
1056
- if (newSub.plugins) subPackage.plugins = newSub.plugins;
1057
- pageJson.subPackages.push(subPackage);
1868
+ }
1869
+ /**
1870
+ * 扫描子包页面目录并收集所有子包页面文件路径
1871
+ * 根据配置的 subPackages 选项扫描对应目录
1872
+ */
1873
+ async scanSubPages() {
1874
+ const paths = {};
1875
+ const subPages = /* @__PURE__ */ new Map();
1876
+ for (const dir of this.options.subPackages) {
1877
+ const pagePaths = getPagePaths(dir, this.options);
1878
+ paths[dir] = pagePaths;
1879
+ const pages = /* @__PURE__ */ new Map();
1880
+ for (const path of pagePaths) {
1881
+ const page = this.subPages.get(dir)?.get(path.absolutePath) || new Page(this, path);
1882
+ pages.set(path.absolutePath, page);
1883
+ }
1884
+ subPages.set(dir, pages);
1058
1885
  }
1059
- const { list, ...tabBarOthers } = await this.resolveTabBar() || {};
1060
- if (list) {
1061
- const { list: oldList } = oldTabBar || {};
1062
- const newList = mergePlatformItems(oldList, currentPlatform, list, "pagePath");
1063
- pageJson.tabBar = {
1064
- ...tabBarOthers,
1065
- list: newList
1066
- };
1067
- } else pageJson.tabBar = void 0;
1068
- return pageJson;
1886
+ debug.subPages(JSON.stringify(paths, null, 2));
1887
+ this.subPages = subPages;
1069
1888
  }
1070
- async readInfoFromPagesJSON() {
1071
- const resolvedPagesJSONContent = await fs.promises.readFile(this.resolvedPagesJSONPath, { encoding: "utf-8" }).catch(() => "");
1072
- this.resolvedPagesJSONIndent = detectIndent(resolvedPagesJSONContent).indent || " ";
1073
- this.resolvedPagesJSONNewline = detectNewline(resolvedPagesJSONContent) || "\n";
1074
- this.resolvedPagesJSONEofNewline = (resolvedPagesJSONContent.at(-1) ?? "\n") === this.resolvedPagesJSONNewline;
1889
+ /**
1890
+ * 解析 pages 规则并设置页面类型
1891
+ * @param pages 页面路径映射
1892
+ * @param packageType 页面包类型(主包或子包)
1893
+ * @param overrides 自定义页面配置
1894
+ * @returns pages 规则
1895
+ */
1896
+ async parsePages(pages, packageType, overrides) {
1897
+ const allPages = Array.from(pages.values());
1898
+ await Promise.all(allPages.map((page) => page.ensureLoaded()));
1899
+ const jobs = allPages.filter((page) => !page.skipped).map((page) => page.getPageMeta());
1900
+ const generatedPageMetaData = await Promise.all(jobs);
1901
+ const customPageMetaData = overrides || [];
1902
+ const parseMeta = dedupeByPath(customPageMetaData.length ? mergePageMetaDataArray(generatedPageMetaData.concat(customPageMetaData)) : generatedPageMetaData);
1903
+ return packageType === "main" ? this.setHomePage(parseMeta) : parseMeta;
1075
1904
  }
1076
- async getIndent() {
1077
- if (!this.resolvedPagesJSONIndent) await this.readInfoFromPagesJSON();
1078
- return this.resolvedPagesJSONIndent;
1905
+ /**
1906
+ * 设置首页
1907
+ * @param result pages 规则数组
1908
+ * @returns pages 规则
1909
+ */
1910
+ setHomePage(result) {
1911
+ if (!result.some(({ type }) => type === "home")) {
1912
+ const basePath = this.basePath;
1913
+ const resolvedHomePages = this.options.homePage.map((v) => {
1914
+ return normalizePath(path.relative(basePath, normalizePath(path.resolve(basePath, v))));
1915
+ });
1916
+ const matchHomePage = (itemPath, configPath) => {
1917
+ if (itemPath === configPath) return true;
1918
+ const normalizedItem = itemPath.replace(/\\/g, "/");
1919
+ const normalizedConfig = configPath.replace(/\\/g, "/");
1920
+ return normalizedItem.endsWith(`/${normalizedConfig}`);
1921
+ };
1922
+ if (!result.some((item) => {
1923
+ const isFound = resolvedHomePages.some((expectedPath) => matchHomePage(item.path, expectedPath));
1924
+ if (isFound) item.type = "home";
1925
+ return isFound;
1926
+ })) this.logger?.warn("No home page found, check the configuration of pages.config.ts, or add the `homePage` option to UniPages in the Vite config file, or add `definePage({ type: \"home\" })` in your vue page.", { timestamp: true });
1927
+ }
1928
+ result.sort((page) => page.type === "home" ? -1 : 0);
1929
+ return result;
1079
1930
  }
1080
- async getNewline() {
1081
- if (!this.resolvedPagesJSONNewline) await this.readInfoFromPagesJSON();
1082
- return this.resolvedPagesJSONNewline;
1931
+ /**
1932
+ * 合并主包页面配置
1933
+ * 过滤掉属于子包的页面,再解析页面配置并与用户配置合并
1934
+ */
1935
+ async mergePageMetaData() {
1936
+ for (const pages of this.subPages.values()) for (const subPageAbsolutePath of pages.keys()) this.pages.delete(subPageAbsolutePath);
1937
+ const pageMetaData = await this.parsePages(this.pages, "main", this.pagesGlobConfig?.pages);
1938
+ this.pageMetaData = pageMetaData;
1939
+ debug.pages(this.pageMetaData);
1083
1940
  }
1084
- async getEndOfLine() {
1085
- if (!this.resolvedPagesJSONEofNewline) await this.readInfoFromPagesJSON();
1086
- return this.resolvedPagesJSONEofNewline;
1941
+ /**
1942
+ * 合并子包页面配置
1943
+ * 为每个子包解析页面配置并处理子包配置继承
1944
+ * 保留用户配置中子包级别的属性(如 plugins)
1945
+ */
1946
+ async mergeSubPageMetaData() {
1947
+ const packagesByRoot = /* @__PURE__ */ new Map();
1948
+ const subPackages = this.pagesGlobConfig?.subPackages || [];
1949
+ for (const [dir, pages] of this.subPages) {
1950
+ const root = this.options.subPackageRootMap.get(dir) ?? normalizePath(path.relative(this.basePath, path.join(this.options.root, dir)));
1951
+ const globPackage = subPackages?.find((v) => v.root === root);
1952
+ const overrides = globPackage?.pages?.map((page) => {
1953
+ if (!page.path || page.path.startsWith(`${root}/`)) return page;
1954
+ return {
1955
+ ...page,
1956
+ path: `${root}/${page.path}`
1957
+ };
1958
+ });
1959
+ const parsedPages = (await this.parsePages(pages, "sub", overrides)).map((page) => ({
1960
+ ...page,
1961
+ path: normalizePath(path.relative(root, page.path))
1962
+ }));
1963
+ packagesByRoot.set(root, {
1964
+ root,
1965
+ pages: parsedPages,
1966
+ ...globPackage?.plugins && { plugins: globPackage.plugins }
1967
+ });
1968
+ }
1969
+ for (const { root, pages, plugins } of subPackages) if (root && !packagesByRoot.has(root)) packagesByRoot.set(root, {
1970
+ root,
1971
+ pages: pages || [],
1972
+ ...plugins && { plugins }
1973
+ });
1974
+ this.subPageMetaData = [...packagesByRoot.values()].filter((meta) => meta.pages.length > 0);
1975
+ debug.subPages(this.subPageMetaData);
1087
1976
  }
1088
1977
  };
1089
1978
  /**
1090
- * Get all page paths in the specified directory
1091
- * @param dir - Page directory path
1092
- * @param options - Resolved configuration options
1093
- * @returns Page path array containing relative and absolute paths
1979
+ * 解析 pages.json 中页面路径的基准路径
1980
+ * @param options - 解析后的配置项
1981
+ * @returns 由 root 与 outDir 拼接并斜杠化后的基准路径
1982
+ */
1983
+ function resolveBasePath(options) {
1984
+ return normalizePath(path.join(options.root, options.outDir));
1985
+ }
1986
+ /**
1987
+ * 获取指定目录下的全部页面路径
1988
+ * @param dir - 页面目录路径
1989
+ * @param options - 解析后的配置项
1990
+ * @returns 包含相对路径与绝对路径的页面路径数组
1094
1991
  */
1095
1992
  function getPagePaths(dir, options) {
1096
- const pagesDirPath = slash(path.resolve(options.root, dir));
1097
- const basePath = slash(path.join(options.root, options.outDir));
1993
+ const pagesDirPath = normalizePath(path.resolve(options.root, dir));
1994
+ const basePath = resolveBasePath(options);
1098
1995
  const files = getPageFiles(pagesDirPath, options);
1099
1996
  debug.pages(dir, files);
1100
- return files.map((file) => slash(file)).map((file) => ({
1101
- relativePath: path.relative(basePath, slash(path.resolve(pagesDirPath, file))),
1102
- absolutePath: slash(path.resolve(pagesDirPath, file))
1997
+ return files.map((file) => normalizePath(file)).map((file) => ({
1998
+ relativePath: path.relative(basePath, normalizePath(path.resolve(pagesDirPath, file))),
1999
+ absolutePath: normalizePath(path.resolve(pagesDirPath, file))
1103
2000
  }));
1104
2001
  }
1105
2002
  /**
1106
- * Merge multi-platform page configuration items
1107
- * Handle conditional compilation comments (#ifdef / #endif), merge configuration items from different platforms into one array
1108
- * Same configuration items will automatically merge platform identifiers, different configuration items will keep conditional compilation comments
2003
+ * 按路径去重页面配置,每个路径保留最后一条
2004
+ * @param pageMetaData - 页面配置数组
2005
+ * @returns 按首次出现顺序去重后的页面配置数组
2006
+ */
2007
+ function dedupeByPath(pageMetaData) {
2008
+ const byPath = /* @__PURE__ */ new Map();
2009
+ for (const page of pageMetaData) byPath.set(page.path, page);
2010
+ return [...byPath.values()];
2011
+ }
2012
+ /**
2013
+ * 应用 vite-plugin-uni-platform 的页面文件名后缀规则(如 `index.h5.vue`
2014
+ * 对应页面 `index`):丢弃其他平台的后缀页面,保留当前平台的并把文件名
2015
+ * 里的后缀剥掉。
1109
2016
  *
1110
- * @param source - Existing configuration item array (from pages.json)
1111
- * @param currentPlatform - Current platform identifier (e.g. H5, MP-WEIXIN)
1112
- * @param items - New configuration item array
1113
- * @param uniqueKeyName - Field name used to identify configuration item uniqueness (e.g. 'path' or 'pagePath')
1114
- * @returns Merged configuration item array with conditional compilation comments
2017
+ * 带点的判断只看路径最后一段(文件名),和 vite-plugin-uni-platform
2018
+ * 自己的规则一致(它也只在文件名上判断后缀),目录名里的点(如
2019
+ * `pages/v1.2/detail`)不算平台后缀。
2020
+ *
2021
+ * 注意后缀匹配仍是原始的 `path.includes(platform)`,没有做 definePage
2022
+ * 那边 h5/web 的别名换算(见 condition.ts 的 platformMatches):
2023
+ * UNI_PLATFORM=web 时,`.h5` 后缀的页面会被过滤掉。这是一直以来的
2024
+ * 行为,为了和 vite-plugin-uni-platform 自己的命名规则保持一致而保留。
2025
+ *
2026
+ * @param pages - 合并后的主包页面配置
2027
+ * @param platform - 当前平台标识
2028
+ * @returns 过滤并剥掉后缀的新数组,入参不被修改
1115
2029
  */
1116
- function mergePlatformItems(source, currentPlatform, items, uniqueKeyName) {
1117
- const src = source || new CommentArray();
1118
- currentPlatform = currentPlatform.toUpperCase();
1119
- let lastPlatforms = [];
1120
- for (const comment of src[Symbol.for("before:0")] || []) {
1121
- const trimed = comment.value.trim();
1122
- if (trimed.startsWith("GENERATED BY UNI-PAGES, PLATFORM:")) lastPlatforms = trimed.split(":")[1].split("||").map((s) => s.trim()).filter((s) => s !== currentPlatform).sort();
1123
- }
1124
- const stringifyForCompare = (val) => {
1125
- if (val && typeof val === "object" && "type" in val) {
1126
- const { type: _type, ...rest } = val;
1127
- return JSON.stringify(rest);
1128
- }
1129
- return JSON.stringify(val);
1130
- };
1131
- const tmpMap = /* @__PURE__ */ new Map();
1132
- for (let i = 0; i < src.length; i++) {
1133
- const item = src[i];
1134
- const uniqueKey = item[uniqueKeyName];
1135
- if (!uniqueKey) continue;
1136
- const ifdefComment = src[Symbol.for(`before:${i}`)]?.find((c) => c.value.trim().startsWith("#ifdef"));
1137
- let platforms = [...lastPlatforms];
1138
- if (ifdefComment) {
1139
- const match = ifdefComment.value.match(/#ifdef\s+(.+)/);
1140
- if (match) platforms = match[1].split("||").map((p) => p.trim()).filter((s) => s !== currentPlatform).sort();
1141
- }
1142
- if (platforms.length === 0) continue;
1143
- const existing = tmpMap.get(uniqueKey) || [];
1144
- existing.push({
1145
- item,
1146
- itemStr: stringifyForCompare(item),
1147
- platforms,
1148
- platformStr: platforms.join(" || ")
1149
- });
1150
- tmpMap.set(uniqueKey, existing);
1151
- }
1152
- for (const item of items) {
1153
- const newItem = item;
1154
- const uniqueKey = item[uniqueKeyName];
1155
- if (!uniqueKey) continue;
1156
- if (!tmpMap.has(uniqueKey)) {
1157
- tmpMap.set(uniqueKey, [{
1158
- item: newItem,
1159
- itemStr: stringifyForCompare(newItem),
1160
- platforms: [currentPlatform],
1161
- platformStr: currentPlatform
1162
- }]);
1163
- continue;
1164
- }
1165
- const existing = tmpMap.get(uniqueKey);
1166
- const newItemStr = stringifyForCompare(newItem);
1167
- const equalObj = existing.find((val) => val.itemStr === newItemStr);
1168
- if (equalObj) {
1169
- equalObj.platforms.push(currentPlatform);
1170
- equalObj.platforms.sort();
1171
- equalObj.platformStr = equalObj.platforms.join(" || ");
1172
- } else existing.push({
1173
- item: newItem,
1174
- itemStr: stringifyForCompare(newItem),
1175
- platforms: [currentPlatform],
1176
- platformStr: currentPlatform
1177
- });
1178
- }
1179
- const result = new CommentArray();
1180
- const platformUsage = {};
1181
- tmpMap.forEach((val) => {
1182
- Object.values(val).forEach((v) => {
1183
- platformUsage[v.platformStr] = (platformUsage[v.platformStr] || 0) + 1;
1184
- });
2030
+ function filterPlatformSuffixPages(pages, platform) {
2031
+ return pages.filter((page) => {
2032
+ return !page.path.slice(page.path.lastIndexOf("/") + 1).includes(".") || page.path.includes(platform);
2033
+ }).map((page) => {
2034
+ const slash = page.path.lastIndexOf("/");
2035
+ const fileName = page.path.slice(slash + 1);
2036
+ const dot = fileName.indexOf(".");
2037
+ return dot === -1 ? page : {
2038
+ ...page,
2039
+ path: `${page.path.slice(0, slash + 1)}${fileName.slice(0, dot)}`
2040
+ };
1185
2041
  });
1186
- const usageKeys = Object.keys(platformUsage);
1187
- const defaultPlatformStr = usageKeys.length ? usageKeys.reduce((a, b) => platformUsage[a] > platformUsage[b] ? a : b) : currentPlatform;
1188
- result[Symbol.for("before:0")] = [{
1189
- type: "LineComment",
1190
- value: ` GENERATED BY UNI-PAGES, PLATFORM: ${defaultPlatformStr}`,
1191
- inline: false,
1192
- loc: {
1193
- start: {
1194
- line: 0,
1195
- column: 0
1196
- },
1197
- end: {
1198
- line: 0,
1199
- column: 0
1200
- }
1201
- }
1202
- }];
1203
- for (const [_, list] of tmpMap) for (const { item, platformStr } of list) {
1204
- result.push(item);
1205
- if (platformStr !== defaultPlatformStr) {
1206
- result[Symbol.for(`before:${result.length - 1}`)] = [{
1207
- type: "LineComment",
1208
- value: ` #ifdef ${platformStr}`,
1209
- inline: false,
1210
- loc: {
1211
- start: {
1212
- line: 0,
1213
- column: 0
1214
- },
1215
- end: {
1216
- line: 0,
1217
- column: 0
1218
- }
1219
- }
1220
- }];
1221
- result[Symbol.for(`after:${result.length - 1}`)] = [{
1222
- type: "LineComment",
1223
- value: " #endif",
1224
- inline: false,
1225
- loc: {
1226
- start: {
1227
- line: 0,
1228
- column: 0
1229
- },
1230
- end: {
1231
- line: 0,
1232
- column: 0
1233
- }
1234
- }
1235
- }];
2042
+ }
2043
+ /**
2044
+ * 按路径合并页面信息并赋 style
2045
+ * @param pageMetaData 页面信息数组
2046
+ * TODO: 支持 middleware 合并
2047
+ */
2048
+ function mergePageMetaDataArray(pageMetaData) {
2049
+ const pageMetaDataObj = groupBy(pageMetaData, "path");
2050
+ const result = [];
2051
+ for (const path in pageMetaDataObj) {
2052
+ const group = pageMetaDataObj[path];
2053
+ const mergedPage = { ...group[0] };
2054
+ for (const page of group) {
2055
+ if (!Object.hasOwn(page, "style")) mergedPage.style = Object.assign({ ...mergedPage.style ?? {} }, page.style ?? {});
2056
+ Object.assign(mergedPage, page);
1236
2057
  }
2058
+ result.push(mergedPage);
1237
2059
  }
1238
2060
  return result;
1239
2061
  }
2062
+ /**
2063
+ * 使虚拟模块失效以触发 HMR 更新
2064
+ * 页面配置变化时,需要使虚拟模块失效以重新生成内容
2065
+ *
2066
+ * @param server - Vite 开发服务器实例
2067
+ */
2068
+ function invalidatePagesModule(server) {
2069
+ const { moduleGraph } = server;
2070
+ const mods = moduleGraph.getModulesByFile(RESOLVED_MODULE_ID_VIRTUAL);
2071
+ if (mods) {
2072
+ const seen = /* @__PURE__ */ new Set();
2073
+ mods.forEach((mod) => {
2074
+ moduleGraph.invalidateModule(mod, seen);
2075
+ });
2076
+ }
2077
+ }
1240
2078
  //#endregion
1241
- //#region src/config/index.ts
2079
+ //#region src/config.ts
1242
2080
  /**
1243
- * Define uni-pages configuration
1244
- * Used in pages.config.ts configuration file to provide type hints and validation
2081
+ * 定义 uni-pages 配置
2082
+ * 在 pages.config.ts 配置文件中使用,提供类型提示与校验
1245
2083
  *
1246
- * @param config - Page configuration object
1247
- * @returns Configuration object
2084
+ * @param config - 页面配置对象
2085
+ * @returns 配置对象
1248
2086
  */
1249
2087
  function defineUniPages(config) {
1250
2088
  return config;
1251
2089
  }
1252
2090
  //#endregion
2091
+ //#region src/pipeline.ts
2092
+ /**
2093
+ * 创建 PageContext,并在内存里跑完「扫描/合并」
2094
+ *
2095
+ * 不写任何文件;路由数据通过 `resolveRoutes`、`resolveSubRoutes`、
2096
+ * `resolveTabBar` 和 `virtualModule` 取。
2097
+ *
2098
+ * @param userOptions - 用户配置项
2099
+ * @param overrides - 从外面传入的 root 与 platform
2100
+ * @returns 完成扫描与合并的 PageContext
2101
+ */
2102
+ async function createPages(userOptions = {}, overrides = {}) {
2103
+ const ctx = new PageContext(userOptions, overrides.root ?? process.cwd(), overrides.platform);
2104
+ await ctx.scanAndMerge();
2105
+ return ctx;
2106
+ }
2107
+ /**
2108
+ * 运行完整流水线:加载用户配置、扫描、合并并写入 pages.json
2109
+ * (以及 TypeScript 声明文件)
2110
+ *
2111
+ * @param userOptions - 用户配置项
2112
+ * @param overrides - 从外面传入的 root 与 platform
2113
+ * @returns pages.json 是否有更新,以及产出它的上下文
2114
+ */
2115
+ async function generateAll(userOptions = {}, overrides = {}) {
2116
+ const ctx = new PageContext(userOptions, overrides.root ?? process.cwd(), overrides.platform);
2117
+ return {
2118
+ updated: await ctx.updatePagesJSON(),
2119
+ ctx
2120
+ };
2121
+ }
2122
+ //#endregion
1253
2123
  //#region src/index.ts
1254
2124
  /**
1255
- * vite-plugin-uni-pages plugin main entry
2125
+ * vite-plugin-uni-pages 插件主入口
1256
2126
  *
1257
- * Automatically scan page directories and generate pages.json configuration file
1258
- * Support definePage macro for defining page metadata
1259
- * Support multi-platform conditional compilation
1260
- * Support sub-package configuration
1261
- * Support TypeScript declaration file generation
2127
+ * 自动扫描页面目录并生成 pages.json 配置文件
2128
+ * 支持 definePage 宏定义页面配置
2129
+ * 支持多平台条件编译
2130
+ * 支持分包配置
2131
+ * 支持 TypeScript 声明文件生成
1262
2132
  *
1263
- * @param userOptions - User configuration options
1264
- * @returns Vite plugin instance
2133
+ * @param userOptions - 用户配置项
2134
+ * @returns Vite 插件实例
1265
2135
  */
1266
2136
  function VitePluginUniPages(userOptions = {}) {
1267
2137
  let ctx;
1268
- checkPagesJsonFileSync(path.join(process.env.VITE_ROOT_DIR || process.cwd(), userOptions.outDir ?? "src", OUTPUT_NAME));
2138
+ checkPagesJsonFileSync(resolvePagesJsonPath(process.env.VITE_ROOT_DIR || process.cwd(), userOptions.outDir ?? "src"));
1269
2139
  return {
1270
2140
  name: "vite-plugin-uni-pages",
1271
2141
  enforce: "pre",
1272
2142
  /**
1273
- * Vite configResolved hook
1274
- * Initialize PageContext, set logger, generate initial pages.json
2143
+ * Vite configResolved 钩子
2144
+ * 初始化 PageContext,设置 logger,生成初始 pages.json
1275
2145
  */
1276
2146
  async configResolved(config) {
1277
2147
  ctx = new PageContext(userOptions, config.root);
@@ -1280,19 +2150,18 @@ function VitePluginUniPages(userOptions = {}) {
1280
2150
  ctx.setLogger(logger);
1281
2151
  await ctx.updatePagesJSON();
1282
2152
  if (config.command === "build") {
1283
- if (config.build.watch) ctx.setupWatcher(chokidar.watch([...ctx.options.dirs, ...ctx.options.subPackages]));
2153
+ if (config.build.watch) ctx.setupWatcher(chokidar.watch([...ctx.options.dirs, ...ctx.options.subPackages].map((v) => normalizePath(path.resolve(config.root, v)))));
1284
2154
  }
1285
2155
  },
1286
2156
  /**
1287
- * Code transform hook
1288
- * Remove definePage macro calls from Vue SFC to avoid runtime errors
2157
+ * 代码转换钩子
2158
+ * 从 Vue SFC 中移除 definePage 宏调用,避免运行时报错
1289
2159
  */
1290
2160
  async transform(code, id) {
1291
2161
  if (!FILE_EXTENSIONS.some((ext) => id.endsWith(ext))) return null;
1292
- const sfc = parseSFC(code, { filename: id });
1293
- let macro;
1294
- if (sfc.scriptSetup) macro = findMacro(babelParse(sfc.scriptSetup.content, sfc.scriptSetup.lang || "js").body, sfc.filename);
1295
- if (!macro && sfc.script) macro = findMacro(babelParse(sfc.script.content, sfc.script.lang || "js").body, sfc.filename);
2162
+ const macro = findDefinePageMacro(code, id, { onParseError: (block, error) => {
2163
+ this.warn(`[vite-plugin-uni-pages] Failed to parse ${block} in ${id}, its definePage macro may stay in the output: ${error instanceof Error ? error?.message : error}`);
2164
+ } });
1296
2165
  if (!macro) return null;
1297
2166
  const s = new MagicString(code);
1298
2167
  s.remove(macro.start, macro.end);
@@ -1302,26 +2171,26 @@ function VitePluginUniPages(userOptions = {}) {
1302
2171
  source: id,
1303
2172
  includeContent: true,
1304
2173
  file: `${id}.map`
1305
- })
2174
+ }).toString()
1306
2175
  };
1307
2176
  },
1308
2177
  /**
1309
- * Configure server hook
1310
- * Set up file watching and HMR support
2178
+ * 配置开发服务器钩子
2179
+ * 设置文件监听与 HMR 支持
1311
2180
  */
1312
2181
  configureServer(server) {
1313
2182
  ctx.setupViteServer(server);
1314
2183
  },
1315
2184
  /**
1316
- * Module resolution hook
1317
- * Resolve virtual module identifier to internal path
2185
+ * 模块解析钩子
2186
+ * 将虚拟模块标识符解析为内部路径
1318
2187
  */
1319
2188
  resolveId(id) {
1320
2189
  if (id === "virtual:uni-pages") return RESOLVED_MODULE_ID_VIRTUAL;
1321
2190
  },
1322
2191
  /**
1323
- * Module load hook
1324
- * Return the code content of the virtual module
2192
+ * 模块加载钩子
2193
+ * 返回虚拟模块的代码内容
1325
2194
  */
1326
2195
  load(id) {
1327
2196
  if (id === RESOLVED_MODULE_ID_VIRTUAL) return ctx.virtualModule();
@@ -1329,4 +2198,4 @@ function VitePluginUniPages(userOptions = {}) {
1329
2198
  };
1330
2199
  }
1331
2200
  //#endregion
1332
- export { FILE_EXTENSIONS, MODULE_ID_VIRTUAL, OUTPUT_NAME, Page, PageContext, RESOLVED_MODULE_ID_VIRTUAL, VitePluginUniPages, VitePluginUniPages as default, babelGenerate, checkPagesJsonFileSync, debug, defineUniPages, extsToGlob, findImports, findMacro, getPageFiles, invalidatePagesModule, isTargetFile, mergePageMetaDataArray, parseCode, parseSFC, resolveOptions, resolvePageDirs, sleep, tryPageMetaFromMacro, withFileLock, writeFileWithLock };
2201
+ export { DefineConditional, FILE_EXTENSIONS, MODULE_ID_VIRTUAL, OUTPUT_NAME, Page, PageContext, RESOLVED_MODULE_ID_VIRTUAL, VitePluginUniPages, VitePluginUniPages as default, checkPagesJsonFileSync, createPages, debug, defineUniPages, evaluateDefinePage, filterPlatformSuffixPages, findDefinePageMacro, generateAll, getPageFiles, isConditional, isTargetFile, mergePagesJson, parseSFC, resolveConditional, resolveOptions, resolvePageDirs, resolvePagesJsonPath, withFileLock, writePagesJson };