@uni-helper/vite-plugin-uni-pages 0.4.9 → 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/README.md +388 -50
- package/client.d.ts +2 -4
- package/dist/index.d.mts +708 -357
- package/dist/index.mjs +1737 -870
- package/package.json +27 -35
- package/dist/index.cjs +0 -1401
- package/dist/index.d.cts +0 -569
- package/index.d.ts +0 -4
package/dist/index.mjs
CHANGED
|
@@ -1,62 +1,56 @@
|
|
|
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
|
|
13
|
-
import detectNewline from "detect-newline";
|
|
10
|
+
import groupBy from "lodash.groupby";
|
|
14
11
|
import { loadConfig } from "unconfig";
|
|
15
|
-
import
|
|
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
|
|
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
|
-
/**
|
|
24
|
+
/** Vite 模块解析用的虚拟模块标识 */
|
|
27
25
|
const MODULE_ID_VIRTUAL = "virtual:uni-pages";
|
|
28
|
-
/**
|
|
26
|
+
/** 加 \0 前缀解析后的虚拟模块标识,避免与其他模块冲突 */
|
|
29
27
|
const RESOLVED_MODULE_ID_VIRTUAL = `\0${MODULE_ID_VIRTUAL}`;
|
|
30
|
-
/**
|
|
28
|
+
/** 输出文件名,即 uni-app 的页面配置文件 */
|
|
31
29
|
const OUTPUT_NAME = "pages.json";
|
|
32
|
-
/**
|
|
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
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
39
|
+
* 生成 TypeScript 声明文件内容
|
|
40
|
+
* 为 uni-app 的导航 API(navigateTo、redirectTo、switchTab、reLaunch)提供类型提示,
|
|
41
|
+
* 让页面路径在编译期获得类型检查
|
|
49
42
|
*
|
|
50
|
-
* @param
|
|
51
|
-
* @returns
|
|
43
|
+
* @param input - 合并后的路由数据
|
|
44
|
+
* @returns 声明文件代码字符串
|
|
52
45
|
*/
|
|
53
|
-
function getDeclaration(
|
|
54
|
-
const
|
|
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
|
|
58
|
-
|
|
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 */
|
|
62
56
|
/* oxlint-disable */
|
|
@@ -65,7 +59,7 @@ function getDeclaration(ctx) {
|
|
|
65
59
|
// Generated by vite-plugin-uni-pages
|
|
66
60
|
|
|
67
61
|
type _LocationUrl =
|
|
68
|
-
${
|
|
62
|
+
${allPagePaths.length > 0 ? allPagePaths.join(" |\n ") : "string"};
|
|
69
63
|
|
|
70
64
|
interface NavigateToOptions {
|
|
71
65
|
url: _LocationUrl;
|
|
@@ -73,7 +67,7 @@ interface NavigateToOptions {
|
|
|
73
67
|
interface RedirectToOptions extends NavigateToOptions {}
|
|
74
68
|
|
|
75
69
|
interface SwitchTabOptions {
|
|
76
|
-
${
|
|
70
|
+
${tabBarPagePaths.length ? `url: ${tabBarPagePaths.join(" | ")}` : ""}
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
type ReLaunchOptions = NavigateToOptions | SwitchTabOptions;
|
|
@@ -91,327 +85,192 @@ declare module "virtual:uni-pages" {
|
|
|
91
85
|
`;
|
|
92
86
|
}
|
|
93
87
|
/**
|
|
94
|
-
*
|
|
95
|
-
* @param filePath -
|
|
96
|
-
* @param content -
|
|
88
|
+
* 写入文件,自动创建所在目录
|
|
89
|
+
* @param filePath - 文件路径
|
|
90
|
+
* @param content - 文件内容
|
|
97
91
|
*/
|
|
98
92
|
async function writeFile$1(filePath, content) {
|
|
99
93
|
await mkdir(dirname(filePath), { recursive: true });
|
|
100
94
|
return await writeFile(filePath, content, "utf-8");
|
|
101
95
|
}
|
|
102
96
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
97
|
+
* 将声明文件写入磁盘
|
|
98
|
+
* 仅在内容变化时写入,避免不必要的文件操作
|
|
105
99
|
*
|
|
106
|
-
* @param
|
|
107
|
-
* @param filepath -
|
|
100
|
+
* @param input - 合并后的路由数据
|
|
101
|
+
* @param filepath - 声明文件输出路径
|
|
108
102
|
*/
|
|
109
|
-
async function writeDeclaration(
|
|
103
|
+
async function writeDeclaration(input, filepath) {
|
|
110
104
|
const originalContent = existsSync(filepath) ? await readFile(filepath, "utf-8") : "";
|
|
111
|
-
const code = getDeclaration(
|
|
105
|
+
const code = getDeclaration(input);
|
|
112
106
|
if (!code) return;
|
|
113
107
|
if (code !== originalContent) await writeFile$1(filepath, code);
|
|
114
108
|
}
|
|
115
109
|
//#endregion
|
|
116
|
-
//#region src/
|
|
117
|
-
/**
|
|
118
|
-
* Invalidate virtual module to trigger HMR update
|
|
119
|
-
* When page configuration changes, the virtual module needs to be invalidated to regenerate content
|
|
120
|
-
*
|
|
121
|
-
* @param server - Vite dev server instance
|
|
122
|
-
*/
|
|
123
|
-
function invalidatePagesModule(server) {
|
|
124
|
-
const { moduleGraph } = server;
|
|
125
|
-
const mods = moduleGraph.getModulesByFile(RESOLVED_MODULE_ID_VIRTUAL);
|
|
126
|
-
if (mods) {
|
|
127
|
-
const seen = /* @__PURE__ */ new Set();
|
|
128
|
-
mods.forEach((mod) => {
|
|
129
|
-
moduleGraph.invalidateModule(mod, seen);
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
110
|
+
//#region src/logger.ts
|
|
133
111
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
112
|
+
* 调试日志工具
|
|
113
|
+
* 基于 debug 包做分级日志输出
|
|
114
|
+
* 通过环境变量 DEBUG=vite-plugin-uni-pages:* 开启
|
|
137
115
|
*/
|
|
138
116
|
const debug = {
|
|
139
|
-
/** HMR
|
|
117
|
+
/** HMR 相关日志 */
|
|
140
118
|
hmr: Debug("vite-plugin-uni-pages:hmr"),
|
|
141
|
-
/**
|
|
119
|
+
/** 配置项相关日志 */
|
|
142
120
|
options: Debug("vite-plugin-uni-pages:options"),
|
|
143
|
-
/**
|
|
121
|
+
/** 主包页面扫描日志 */
|
|
144
122
|
pages: Debug("vite-plugin-uni-pages:pages"),
|
|
145
|
-
/**
|
|
123
|
+
/** 子包页面扫描日志 */
|
|
146
124
|
subPages: Debug("vite-plugin-uni-pages:subPages"),
|
|
147
|
-
/**
|
|
125
|
+
/** 错误日志 */
|
|
148
126
|
error: Debug("vite-plugin-uni-pages:error"),
|
|
149
|
-
/**
|
|
127
|
+
/** 缓存相关日志 */
|
|
150
128
|
cache: Debug("vite-plugin-uni-pages:cache"),
|
|
151
|
-
/**
|
|
129
|
+
/** 声明文件生成日志 */
|
|
152
130
|
declaration: Debug("vite-plugin-uni-pages:declaration"),
|
|
153
|
-
/** definePage
|
|
131
|
+
/** definePage 宏解析日志 */
|
|
154
132
|
definePage: Debug("vite-plugin-uni-pages:definePage")
|
|
155
133
|
};
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/files.ts
|
|
156
136
|
/**
|
|
157
|
-
*
|
|
158
|
-
* @param extensions - File extensions array
|
|
159
|
-
* @returns Glob pattern string
|
|
137
|
+
* 解析给定上下文下可作为有效页面的文件列表。
|
|
160
138
|
*/
|
|
161
|
-
function
|
|
162
|
-
|
|
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
|
+
});
|
|
163
147
|
}
|
|
164
148
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
149
|
+
* 检查文件是否为目标页面文件
|
|
150
|
+
* 判断文件扩展名是否在支持的页面文件类型列表中
|
|
167
151
|
*
|
|
168
|
-
* @param path -
|
|
169
|
-
* @returns
|
|
152
|
+
* @param path - 文件路径
|
|
153
|
+
* @returns 是否为目标文件
|
|
170
154
|
*/
|
|
171
155
|
function isTargetFile(path) {
|
|
172
156
|
const ext = path.split(".").pop();
|
|
173
157
|
return FILE_EXTENSIONS.includes(ext);
|
|
174
158
|
}
|
|
175
159
|
/**
|
|
176
|
-
*
|
|
177
|
-
* @param pageMetaData page meta data array
|
|
178
|
-
* TODO: support merge middleware
|
|
179
|
-
*/
|
|
180
|
-
function mergePageMetaDataArray(pageMetaData) {
|
|
181
|
-
const pageMetaDataObj = groupBy(pageMetaData, "path");
|
|
182
|
-
const result = [];
|
|
183
|
-
for (const path in pageMetaDataObj) {
|
|
184
|
-
const _pageMetaData = pageMetaDataObj[path];
|
|
185
|
-
const options = _pageMetaData[0];
|
|
186
|
-
for (const page of _pageMetaData) {
|
|
187
|
-
options.style = Object.assign(options.style ?? {}, page.style ?? {});
|
|
188
|
-
Object.assign(options, page);
|
|
189
|
-
}
|
|
190
|
-
result.push(options);
|
|
191
|
-
}
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Convert TypeScript / JavaScript script code to object/function
|
|
160
|
+
* 根据项目根目录与输出目录解析 pages.json 文件路径
|
|
196
161
|
*
|
|
197
|
-
* @param
|
|
198
|
-
* @param
|
|
199
|
-
* @
|
|
200
|
-
* @param options.filename - Script filename for error location and context
|
|
201
|
-
* @returns Script execution result, if export is a function then execute and return its return value
|
|
202
|
-
*/
|
|
203
|
-
async function parseCode(options) {
|
|
204
|
-
const { imports = [], code, filename } = options;
|
|
205
|
-
let jsCode = "";
|
|
206
|
-
try {
|
|
207
|
-
const tmpCode = `${imports.join("\n")}\n export default ${code}`;
|
|
208
|
-
jsCode = ts.transpileModule(tmpCode, {
|
|
209
|
-
compilerOptions: {
|
|
210
|
-
module: ts.ModuleKind.CommonJS,
|
|
211
|
-
target: ts.ScriptTarget.ES2022,
|
|
212
|
-
noEmit: true,
|
|
213
|
-
strict: false,
|
|
214
|
-
noImplicitAny: false,
|
|
215
|
-
strictNullChecks: false,
|
|
216
|
-
strictFunctionTypes: false,
|
|
217
|
-
strictBindCallApply: false,
|
|
218
|
-
strictPropertyInitialization: false,
|
|
219
|
-
noImplicitThis: false,
|
|
220
|
-
alwaysStrict: false,
|
|
221
|
-
allowJs: true,
|
|
222
|
-
checkJs: false,
|
|
223
|
-
skipLibCheck: true,
|
|
224
|
-
esModuleInterop: true,
|
|
225
|
-
removeComments: true
|
|
226
|
-
},
|
|
227
|
-
jsDocParsingMode: ts.JSDocParsingMode.ParseNone
|
|
228
|
-
}).outputText;
|
|
229
|
-
const dir = path.dirname(filename);
|
|
230
|
-
const vmContext = {
|
|
231
|
-
module: {},
|
|
232
|
-
exports: {},
|
|
233
|
-
__filename: filename,
|
|
234
|
-
__dirname: dir,
|
|
235
|
-
require: createRequire(dir),
|
|
236
|
-
import: (id) => import(id),
|
|
237
|
-
setTimeout,
|
|
238
|
-
clearTimeout,
|
|
239
|
-
setInterval,
|
|
240
|
-
clearInterval,
|
|
241
|
-
setImmediate,
|
|
242
|
-
clearImmediate,
|
|
243
|
-
console,
|
|
244
|
-
URL,
|
|
245
|
-
URLSearchParams,
|
|
246
|
-
performance,
|
|
247
|
-
global: globalThis,
|
|
248
|
-
globalThis
|
|
249
|
-
};
|
|
250
|
-
await new vm.Script(jsCode, { filename }).runInNewContext(vmContext, { timeout: 1e3 });
|
|
251
|
-
return vmContext.exports.default || vmContext.exports;
|
|
252
|
-
} catch (error) {
|
|
253
|
-
throw new Error(`EXEC SCRIPT FAIL IN ${filename}: ${error.message} \n\n${jsCode}\n\n`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Async sleep function
|
|
258
|
-
* @param ms - Sleep duration in milliseconds
|
|
259
|
-
* @returns Promise object
|
|
162
|
+
* @param root - 项目根目录
|
|
163
|
+
* @param outDir - 相对 root 的 pages.json 输出目录
|
|
164
|
+
* @returns pages.json 绝对路径
|
|
260
165
|
*/
|
|
261
|
-
function
|
|
262
|
-
return
|
|
166
|
+
function resolvePagesJsonPath(root, outDir) {
|
|
167
|
+
return path.join(root, outDir, OUTPUT_NAME);
|
|
263
168
|
}
|
|
264
169
|
/**
|
|
265
|
-
*
|
|
266
|
-
* Compatible with CommonJS and ES Module export styles
|
|
170
|
+
* 确保这个路径上有一个能用的 pages.json 文件。
|
|
267
171
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//#region src/files.ts
|
|
278
|
-
/**
|
|
279
|
-
* Resolves the files that are valid pages for the given context.
|
|
280
|
-
*/
|
|
281
|
-
function getPageFiles(path, options) {
|
|
282
|
-
const { exclude } = options;
|
|
283
|
-
return globSync(`**/*.${extsToGlob(FILE_EXTENSIONS)}`, {
|
|
284
|
-
ignore: exclude,
|
|
285
|
-
onlyFiles: true,
|
|
286
|
-
cwd: path
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* 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
|
|
291
|
-
* @param path - File path to check
|
|
292
|
-
* @returns boolean - Whether the operation was successful
|
|
172
|
+
* 文件不存在时创建一个占位文件,占位内容很快会被下一次生成覆盖。
|
|
173
|
+
* 已存在的文件绝不删除重建:删它会悄悄弄丢手写的内容(只读的文件
|
|
174
|
+
* 也能被删除,只要它所在的目录可写),所以"路径存在但不是普通文件、
|
|
175
|
+
* 或者读不了也写不了"都算必须由用户自己修的硬错误(例如
|
|
176
|
+
* `chmod u+w`)。
|
|
177
|
+
*
|
|
178
|
+
* @param path - 待检查的文件路径
|
|
179
|
+
* @throws 路径存在但不是普通文件、缺少读写权限,或占位文件创建失败时
|
|
180
|
+
* 抛出
|
|
293
181
|
*/
|
|
294
182
|
function checkPagesJsonFileSync(path) {
|
|
295
|
-
/**
|
|
296
|
-
* Create an empty pages.json file
|
|
297
|
-
* @param path - File path
|
|
298
|
-
* @returns boolean - Whether the creation was successful
|
|
299
|
-
*/
|
|
300
|
-
const createEmptyFile = (path) => {
|
|
301
|
-
try {
|
|
302
|
-
fs.writeFileSync(path, JSON.stringify({ pages: [{ path: "" }] }, null, 2), { encoding: "utf-8" });
|
|
303
|
-
return true;
|
|
304
|
-
} catch {
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
/**
|
|
309
|
-
* Delete the file at the specified path
|
|
310
|
-
* @param path - File path
|
|
311
|
-
* @returns boolean - Whether the deletion was successful
|
|
312
|
-
*/
|
|
313
|
-
const unlinkFile = (path) => {
|
|
314
|
-
try {
|
|
315
|
-
fs.unlinkSync(path);
|
|
316
|
-
return true;
|
|
317
|
-
} catch {
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
183
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
try {
|
|
332
|
-
fs.accessSync(path, fs.constants.R_OK | fs.constants.W_OK);
|
|
333
|
-
return true;
|
|
334
|
-
} catch {
|
|
335
|
-
if (!unlinkFile(path)) return false;
|
|
336
|
-
return createEmptyFile(path);
|
|
337
|
-
}
|
|
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);
|
|
338
192
|
} catch {
|
|
339
|
-
|
|
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.`);
|
|
340
194
|
}
|
|
341
195
|
}
|
|
342
196
|
/**
|
|
343
|
-
*
|
|
197
|
+
* 同一个进程里按文件路径排的等待队列(先来后到)。
|
|
344
198
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
* 重试只用来等其他进程放锁。
|
|
350
214
|
*
|
|
351
|
-
* @param path -
|
|
352
|
-
* @param task -
|
|
353
|
-
* @param retry -
|
|
354
|
-
* @returns
|
|
215
|
+
* @param path - 要加锁的文件路径
|
|
216
|
+
* @param task - 在锁内运行的异步任务;返回值原样返回给调用方
|
|
217
|
+
* @param retry - 锁被别人拿着时的重试次数,默认 3
|
|
218
|
+
* @returns `task` 的结果;一直拿不到锁时为 `undefined`
|
|
355
219
|
*/
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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;
|
|
363
229
|
try {
|
|
364
230
|
release = await lockfile.lock(path, { realpath: false });
|
|
365
231
|
} catch {
|
|
366
232
|
await sleep(500);
|
|
367
|
-
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
return await task();
|
|
237
|
+
} finally {
|
|
238
|
+
await release();
|
|
368
239
|
}
|
|
369
|
-
return await task();
|
|
370
|
-
} finally {
|
|
371
|
-
if (release) await release();
|
|
372
240
|
}
|
|
241
|
+
debug.error(`${path} Failed to acquire file lock, task aborted`);
|
|
373
242
|
}
|
|
374
243
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
* Note: this only makes the *write* atomic. Callers that need to read the
|
|
380
|
-
* current content before computing the new one must use {@link withFileLock}
|
|
381
|
-
* so the read and write belong to the same critical section.
|
|
382
|
-
*
|
|
383
|
-
* @param path - File path
|
|
384
|
-
* @param content - File content
|
|
385
|
-
* @param retry - Number of retries when lock acquisition fails, defaults to 3
|
|
244
|
+
* 异步休眠函数
|
|
245
|
+
* @param ms - 休眠毫秒数
|
|
246
|
+
* @returns 指定延时后 resolve 的 Promise
|
|
386
247
|
*/
|
|
387
|
-
|
|
388
|
-
return
|
|
389
|
-
await writeFileAtomic(path, content);
|
|
390
|
-
}, retry);
|
|
248
|
+
function sleep(ms) {
|
|
249
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
391
250
|
}
|
|
392
251
|
//#endregion
|
|
393
252
|
//#region src/options.ts
|
|
394
253
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
254
|
+
* 解析用户配置项
|
|
255
|
+
* 将用户提供的配置与默认值合并,并处理路径解析
|
|
397
256
|
*
|
|
398
|
-
* @param userOptions -
|
|
399
|
-
* @param viteRoot - Vite
|
|
400
|
-
* @returns
|
|
257
|
+
* @param userOptions - 用户配置项
|
|
258
|
+
* @param viteRoot - Vite 项目根目录
|
|
259
|
+
* @returns 解析后的配置项
|
|
401
260
|
*/
|
|
402
261
|
function resolveOptions(userOptions, viteRoot = process.cwd()) {
|
|
403
262
|
const { dts = true, configSource = "pages.config", homePage = ["pages/index", "pages/index/index"], mergePages = true, dir = "src/pages", subPackages = [], outDir = "src", exclude = [
|
|
404
263
|
"node_modules",
|
|
405
264
|
".git",
|
|
406
265
|
"**/__*__/**"
|
|
407
|
-
], minify = false, debug = false, onBeforeLoadUserConfig = () => {}, onAfterLoadUserConfig = () => {}, onBeforeScanPages = () => {}, onAfterScanPages = () => {}, onBeforeMergePageMetaData = () => {}, onAfterMergePageMetaData = () => {}, onBeforeWriteFile = () => {}, onAfterWriteFile = () => {} } = userOptions;
|
|
408
|
-
const root = viteRoot ||
|
|
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());
|
|
409
268
|
const resolvedDirs = resolvePageDirs(dir, root, exclude);
|
|
410
269
|
const subPackageRootMap = /* @__PURE__ */ new Map();
|
|
411
270
|
const resolvedSubDirs = [];
|
|
412
|
-
for (const sub of subPackages) if (typeof sub === "string") resolvedSubDirs.push(
|
|
271
|
+
for (const sub of subPackages) if (typeof sub === "string") resolvedSubDirs.push(normalizePath(sub));
|
|
413
272
|
else {
|
|
414
|
-
const dirPath =
|
|
273
|
+
const dirPath = normalizePath(sub.dir);
|
|
415
274
|
resolvedSubDirs.push(dirPath);
|
|
416
275
|
subPackageRootMap.set(dirPath, sub.root);
|
|
417
276
|
}
|
|
@@ -429,6 +288,9 @@ function resolveOptions(userOptions, viteRoot = process.cwd()) {
|
|
|
429
288
|
exclude,
|
|
430
289
|
root,
|
|
431
290
|
minify,
|
|
291
|
+
insertFinalNewline,
|
|
292
|
+
indent,
|
|
293
|
+
eol,
|
|
432
294
|
debug,
|
|
433
295
|
onBeforeLoadUserConfig,
|
|
434
296
|
onAfterLoadUserConfig,
|
|
@@ -441,10 +303,14 @@ function resolveOptions(userOptions, viteRoot = process.cwd()) {
|
|
|
441
303
|
};
|
|
442
304
|
}
|
|
443
305
|
/**
|
|
444
|
-
*
|
|
306
|
+
* 根据给定的 glob 模式解析页面目录
|
|
307
|
+
* @param dir - 页面目录 glob 模式
|
|
308
|
+
* @param root - 项目根目录
|
|
309
|
+
* @param exclude - 需要排除的 glob 模式
|
|
310
|
+
* @returns 匹配到的目录路径
|
|
445
311
|
*/
|
|
446
312
|
function resolvePageDirs(dir, root, exclude) {
|
|
447
|
-
return globSync(
|
|
313
|
+
return globSync(normalizePath(dir), {
|
|
448
314
|
ignore: exclude,
|
|
449
315
|
onlyDirectories: true,
|
|
450
316
|
expandDirectories: false,
|
|
@@ -453,115 +319,118 @@ function resolvePageDirs(dir, root, exclude) {
|
|
|
453
319
|
});
|
|
454
320
|
}
|
|
455
321
|
//#endregion
|
|
456
|
-
//#region src/
|
|
322
|
+
//#region src/condition.ts
|
|
323
|
+
/** 判断一个值是否为普通对象(非 null、非数组) */
|
|
324
|
+
function isPlainObject(val) {
|
|
325
|
+
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
326
|
+
}
|
|
457
327
|
/**
|
|
458
|
-
*
|
|
328
|
+
* 把 `partial` 合并进 `base`,两边都不改动
|
|
459
329
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* 2. Parse page metadata defined by definePage macro
|
|
463
|
-
* 3. Provide tabBar configuration information
|
|
464
|
-
* 4. Track page file change status
|
|
330
|
+
* 对象一层层往里合并;数组和普通值直接整个替换。规则和
|
|
331
|
+
* @uni-ku/pages-json 保持一致。
|
|
465
332
|
*/
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
uri;
|
|
473
|
-
/** Whether the page has changed, used for incremental update judgment */
|
|
474
|
-
changed = true;
|
|
475
|
-
/** Raw JSON string of page metadata for change detection */
|
|
476
|
-
raw = "";
|
|
477
|
-
/** Parsed page metadata */
|
|
478
|
-
meta;
|
|
479
|
-
/**
|
|
480
|
-
* Create a page instance
|
|
481
|
-
* @param ctx - Page context instance
|
|
482
|
-
* @param path - Page path information
|
|
483
|
-
*/
|
|
484
|
-
constructor(ctx, path) {
|
|
485
|
-
this.ctx = ctx;
|
|
486
|
-
this.path = path;
|
|
487
|
-
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;
|
|
488
339
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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;
|
|
503
371
|
}
|
|
504
|
-
/**
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (forceUpdate || !this.meta) await this.read();
|
|
513
|
-
const { tabBar } = this.meta || {};
|
|
514
|
-
if (tabBar === void 0) return;
|
|
515
|
-
return {
|
|
516
|
-
...tabBar,
|
|
517
|
-
pagePath: tabBar.pagePath || this.uri,
|
|
518
|
-
index: tabBar.index ?? 0
|
|
519
|
-
};
|
|
372
|
+
/** 仅在给定平台应用 `partial` */
|
|
373
|
+
ifdef(platform, partial) {
|
|
374
|
+
this.branches.push({
|
|
375
|
+
condition: "ifdef",
|
|
376
|
+
platforms: toPlatforms(platform),
|
|
377
|
+
partial
|
|
378
|
+
});
|
|
379
|
+
return this;
|
|
520
380
|
}
|
|
521
|
-
/**
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
381
|
+
/** 在除给定平台之外的所有平台应用 `partial` */
|
|
382
|
+
ifndef(platform, partial) {
|
|
383
|
+
this.branches.push({
|
|
384
|
+
condition: "ifndef",
|
|
385
|
+
platforms: toPlatforms(platform),
|
|
386
|
+
partial
|
|
387
|
+
});
|
|
388
|
+
return this;
|
|
527
389
|
}
|
|
528
390
|
/**
|
|
529
|
-
*
|
|
530
|
-
*
|
|
391
|
+
* 算出某个平台最终用的普通配置:从基础配置开始,按写的顺序把每个
|
|
392
|
+
* 匹配的分支合并进去。平台列表里 `h5` 和 `web` 互相通用(见
|
|
393
|
+
* platformMatches)。
|
|
531
394
|
*/
|
|
532
|
-
|
|
533
|
-
let
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
} catch (err) {
|
|
537
|
-
debug.error(err);
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
let raw = "";
|
|
541
|
-
try {
|
|
542
|
-
raw = JSON.stringify(meta);
|
|
543
|
-
} catch {}
|
|
544
|
-
this.changed = this.raw !== raw;
|
|
545
|
-
this.meta = meta;
|
|
546
|
-
this.raw = raw;
|
|
547
|
-
}
|
|
548
|
-
async readPageMetaFromFile() {
|
|
549
|
-
try {
|
|
550
|
-
const meta = await tryPageMetaFromMacro(parseSFC(await fs.promises.readFile(this.path.absolutePath, { encoding: "utf-8" }), { filename: this.path.absolutePath }));
|
|
551
|
-
if (meta) return meta;
|
|
552
|
-
return { type: "page" };
|
|
553
|
-
} catch (err) {
|
|
554
|
-
throw new Error(`Read page meta fail in ${this.path.relativePath}\n${err.message}`);
|
|
555
|
-
}
|
|
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;
|
|
556
399
|
}
|
|
557
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
|
+
*/
|
|
558
427
|
/**
|
|
559
|
-
*
|
|
560
|
-
*
|
|
428
|
+
* 解析 Vue 单文件组件(SFC)
|
|
429
|
+
* 兼容不同版本的 @vue/compiler-sfc
|
|
561
430
|
*
|
|
562
|
-
* @param code - Vue SFC
|
|
563
|
-
* @param options -
|
|
564
|
-
* @returns SFC descriptor
|
|
431
|
+
* @param code - Vue SFC 源码
|
|
432
|
+
* @param options - 解析选项
|
|
433
|
+
* @returns SFC descriptor 对象
|
|
565
434
|
*/
|
|
566
435
|
function parseSFC(code, options) {
|
|
567
436
|
return parse$1(code, {
|
|
@@ -573,39 +442,112 @@ function parseSFC(code, options) {
|
|
|
573
442
|
});
|
|
574
443
|
}
|
|
575
444
|
/**
|
|
576
|
-
*
|
|
577
|
-
*
|
|
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 宏,返回页面信息
|
|
578
481
|
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
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
|
|
581
490
|
*/
|
|
582
|
-
async function
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
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;
|
|
589
497
|
const [macroOption] = macro.arguments;
|
|
590
|
-
|
|
498
|
+
if (!macroOption) return void 0;
|
|
499
|
+
const macroCode = babelGenerate(macroOption).code;
|
|
591
500
|
const parsed = await parseCode({
|
|
592
|
-
imports,
|
|
593
|
-
code,
|
|
594
|
-
filename
|
|
501
|
+
imports: pickEvalImports(blocks.map((block) => block.imports), macroOption),
|
|
502
|
+
code: macroCode,
|
|
503
|
+
filename
|
|
595
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;
|
|
596
511
|
return {
|
|
597
512
|
type: "page",
|
|
598
|
-
...
|
|
513
|
+
...resolvedMeta
|
|
599
514
|
};
|
|
600
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
|
+
}
|
|
601
540
|
}
|
|
602
541
|
/**
|
|
603
|
-
*
|
|
604
|
-
*
|
|
542
|
+
* 在 AST 中查找 definePage 宏调用
|
|
543
|
+
* 支持函数表达式、箭头函数与对象表达式作为参数
|
|
544
|
+
*
|
|
545
|
+
* 已知限制:只在块的顶层语句里找,包进 if/try 等语句块里的调用
|
|
546
|
+
* 不在此列(用户侧的说明写在 README 的 definePage 章节)
|
|
605
547
|
*
|
|
606
|
-
* @param stmts - AST
|
|
607
|
-
* @param filename -
|
|
608
|
-
* @returns definePage
|
|
548
|
+
* @param stmts - AST 语句数组
|
|
549
|
+
* @param filename - 用于错误上报的文件名
|
|
550
|
+
* @returns definePage 调用表达式,未找到时为 undefined
|
|
609
551
|
*/
|
|
610
552
|
function findMacro(stmts, filename) {
|
|
611
553
|
let macro;
|
|
@@ -619,74 +561,1065 @@ function findMacro(stmts, filename) {
|
|
|
619
561
|
}
|
|
620
562
|
if (!macro) return;
|
|
621
563
|
const [opt] = macro.arguments;
|
|
622
|
-
if (opt && !t.isFunctionExpression(opt) && !t.isArrowFunctionExpression(opt) && !t.isObjectExpression(opt)) {
|
|
623
|
-
debug.definePage(`definePage()
|
|
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}`);
|
|
624
566
|
return;
|
|
625
567
|
}
|
|
626
568
|
return macro;
|
|
627
569
|
}
|
|
628
570
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
571
|
+
* 从 AST 中提取全部导入声明
|
|
572
|
+
* 用于在执行 definePage 参数时提供必要的导入
|
|
631
573
|
*
|
|
632
|
-
* @param stmts - AST
|
|
633
|
-
* @returns
|
|
574
|
+
* @param stmts - AST 语句数组
|
|
575
|
+
* @returns 导入声明数组
|
|
634
576
|
*/
|
|
635
577
|
function findImports(stmts) {
|
|
636
|
-
|
|
637
|
-
for (const stmt of stmts) if (t.isImportDeclaration(stmt)) imports.push(stmt);
|
|
638
|
-
return imports;
|
|
578
|
+
return stmts.filter(t.isImportDeclaration);
|
|
639
579
|
}
|
|
640
|
-
//#endregion
|
|
641
|
-
//#region src/context.ts
|
|
642
580
|
/**
|
|
643
|
-
*
|
|
581
|
+
* 从两个 script 块的全部 import 里,挑出宏参数真正用到的那些语句。
|
|
644
582
|
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* 5. Provide virtual module and HMR support
|
|
583
|
+
* 挑选按"名字被宏引用"来:没被用到的 import 不进沙箱,免得为了求值
|
|
584
|
+
* 一个宏,把整个页面的依赖都加载一遍(比如普通 `<script>` 里 import
|
|
585
|
+
* 的 `.vue` 组件,require 加载不动它,还会让本来好好的页面求值失败)。
|
|
586
|
+
* 两个块里写了完全相同的 import 时只留一份,不然编译出的代码会重复
|
|
587
|
+
* 声明同一个名字
|
|
651
588
|
*/
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
+
/** 主包页面映射,键为页面文件的绝对路径 */
|
|
659
1586
|
pages = /* @__PURE__ */ new Map();
|
|
660
|
-
/**
|
|
1587
|
+
/** 子包页面映射,键为子包根目录,值为该子包下的页面映射 */
|
|
661
1588
|
subPages = /* @__PURE__ */ new Map();
|
|
662
|
-
/**
|
|
1589
|
+
/** 主包页面配置数组,用于生成 pages.json 的 pages 字段 */
|
|
663
1590
|
pageMetaData = [];
|
|
664
|
-
/**
|
|
1591
|
+
/** 子包页面配置数组,用于生成 pages.json 的 subPackages 字段 */
|
|
665
1592
|
subPageMetaData = [];
|
|
666
|
-
/**
|
|
1593
|
+
/**
|
|
1594
|
+
* 最近一次合并出的 tabBar(配置文件与 definePage 声明的合体)。
|
|
1595
|
+
* 类型声明生成 tab 页列表时用它,definePage 声明的 tabBar 页才能
|
|
1596
|
+
* 一起从 navigateTo 的 url 类型里排除
|
|
1597
|
+
*/
|
|
1598
|
+
tabBar;
|
|
1599
|
+
/** 生成的 pages.json 文件路径 */
|
|
667
1600
|
resolvedPagesJSONPath = "";
|
|
668
|
-
|
|
669
|
-
resolvedPagesJSONNewline;
|
|
670
|
-
resolvedPagesJSONEofNewline;
|
|
671
|
-
/** Original options passed by the user */
|
|
672
|
-
rawOptions;
|
|
673
|
-
/** Project root directory */
|
|
1601
|
+
/** 项目根目录 */
|
|
674
1602
|
root;
|
|
675
|
-
/**
|
|
1603
|
+
/** 解析后的配置项 */
|
|
676
1604
|
options;
|
|
1605
|
+
/** 当前平台标识,如 'mp-weixin';由调用方传入,不依赖模块加载时冻结的环境 */
|
|
1606
|
+
platform;
|
|
677
1607
|
logger;
|
|
678
|
-
/**
|
|
1608
|
+
/** 是否与 vite-plugin-uni-platform 插件协同工作 */
|
|
679
1609
|
withUniPlatform = false;
|
|
680
|
-
/**
|
|
681
|
-
|
|
1610
|
+
/** pages.json 中页面路径的解析基准路径 */
|
|
1611
|
+
get basePath() {
|
|
1612
|
+
return resolveBasePath(this.options);
|
|
1613
|
+
}
|
|
682
1614
|
/**
|
|
683
|
-
*
|
|
684
|
-
* @param userOptions -
|
|
685
|
-
* @param viteRoot - Vite
|
|
1615
|
+
* 创建 PageContext 实例
|
|
1616
|
+
* @param userOptions - 用户配置项
|
|
1617
|
+
* @param viteRoot - Vite 项目根目录,默认为当前工作目录
|
|
1618
|
+
* @param platform - 当前平台标识,默认取 uni-env 的平台
|
|
686
1619
|
*/
|
|
687
|
-
constructor(userOptions, viteRoot = process.cwd()) {
|
|
688
|
-
this.
|
|
689
|
-
this.
|
|
1620
|
+
constructor(userOptions, viteRoot = process.cwd(), platform$1 = platform) {
|
|
1621
|
+
this.root = normalizePath(viteRoot);
|
|
1622
|
+
this.platform = platform$1;
|
|
690
1623
|
debug.options("root", this.root);
|
|
691
1624
|
this.options = resolveOptions(userOptions, this.root);
|
|
692
1625
|
const debugOption = this.options.debug;
|
|
@@ -695,19 +1628,19 @@ var PageContext = class {
|
|
|
695
1628
|
const suffix = typeof debugOption === "boolean" ? "*" : debugOption;
|
|
696
1629
|
Debug.enable(`${prefix}${suffix}`);
|
|
697
1630
|
}
|
|
698
|
-
this.resolvedPagesJSONPath =
|
|
1631
|
+
this.resolvedPagesJSONPath = resolvePagesJsonPath(this.root, this.options.outDir);
|
|
699
1632
|
debug.options(this.options);
|
|
700
1633
|
}
|
|
701
1634
|
/**
|
|
702
|
-
*
|
|
703
|
-
* @param logger - Vite logger
|
|
1635
|
+
* 设置 Vite logger
|
|
1636
|
+
* @param logger - Vite logger 实例
|
|
704
1637
|
*/
|
|
705
1638
|
setLogger(logger) {
|
|
706
1639
|
this.logger = logger;
|
|
707
1640
|
}
|
|
708
1641
|
/**
|
|
709
|
-
*
|
|
710
|
-
*
|
|
1642
|
+
* 加载用户页面配置文件(如 pages.config.ts)
|
|
1643
|
+
* 使用 unconfig 加载配置,支持多种配置文件格式
|
|
711
1644
|
*/
|
|
712
1645
|
async loadUserPagesConfig() {
|
|
713
1646
|
const configSource = this.options.configSource;
|
|
@@ -717,51 +1650,22 @@ var PageContext = class {
|
|
|
717
1650
|
defaults: {}
|
|
718
1651
|
});
|
|
719
1652
|
this.pagesGlobConfig = config.default || config;
|
|
720
|
-
this.pagesConfigSourcePaths = sources;
|
|
1653
|
+
this.pagesConfigSourcePaths = sources.map(normalizePath);
|
|
721
1654
|
debug.options(this.pagesGlobConfig);
|
|
722
1655
|
}
|
|
723
1656
|
/**
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*/
|
|
727
|
-
async scanPages() {
|
|
728
|
-
const paths = this.options.dirs.map((dir) => {
|
|
729
|
-
return {
|
|
730
|
-
dir,
|
|
731
|
-
files: getPagePaths(dir, this.options)
|
|
732
|
-
};
|
|
733
|
-
}).map((page) => page.files).flat();
|
|
734
|
-
debug.pages(paths);
|
|
735
|
-
const pages = /* @__PURE__ */ new Map();
|
|
736
|
-
for (const path of paths) {
|
|
737
|
-
const page = this.pages.get(path.absolutePath) || new Page(this, path);
|
|
738
|
-
pages.set(path.absolutePath, page);
|
|
739
|
-
}
|
|
740
|
-
this.pages = pages;
|
|
741
|
-
}
|
|
742
|
-
/**
|
|
743
|
-
* Scan sub-package page directories and collect all sub-package page file paths
|
|
744
|
-
* Scan corresponding directories based on the configured subPackages option
|
|
1657
|
+
* 按顺序跑完扫描与合并:先扫描主包和子包的页面,再合并它们的
|
|
1658
|
+
* 信息。步骤顺序只维护在这里,调用方和测试都不用关心。
|
|
745
1659
|
*/
|
|
746
|
-
async
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
paths[dir] = pagePaths;
|
|
752
|
-
const pages = /* @__PURE__ */ new Map();
|
|
753
|
-
for (const path of pagePaths) {
|
|
754
|
-
const page = this.subPages.get(dir)?.get(path.absolutePath) || new Page(this, path);
|
|
755
|
-
pages.set(path.absolutePath, page);
|
|
756
|
-
}
|
|
757
|
-
subPages.set(dir, pages);
|
|
758
|
-
}
|
|
759
|
-
debug.subPages(JSON.stringify(paths, null, 2));
|
|
760
|
-
this.subPages = subPages;
|
|
1660
|
+
async scanAndMerge() {
|
|
1661
|
+
await this.scanPages();
|
|
1662
|
+
await this.scanSubPages();
|
|
1663
|
+
await this.mergePageMetaData();
|
|
1664
|
+
await this.mergeSubPageMetaData();
|
|
761
1665
|
}
|
|
762
1666
|
/**
|
|
763
|
-
*
|
|
764
|
-
* @param server - Vite
|
|
1667
|
+
* 设置 Vite 开发服务器,用于 HMR 与文件监听
|
|
1668
|
+
* @param server - Vite 开发服务器实例
|
|
765
1669
|
*/
|
|
766
1670
|
setupViteServer(server) {
|
|
767
1671
|
if (this._server === server) return;
|
|
@@ -769,47 +1673,45 @@ var PageContext = class {
|
|
|
769
1673
|
this.setupWatcher(server.watcher);
|
|
770
1674
|
}
|
|
771
1675
|
/**
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
* @param watcher - chokidar
|
|
1676
|
+
* 设置文件监听器,监听页面文件与配置文件变更
|
|
1677
|
+
* 页面文件或配置文件变化时自动更新 pages.json
|
|
1678
|
+
* @param watcher - chokidar 文件监听器实例
|
|
775
1679
|
*/
|
|
776
1680
|
async setupWatcher(watcher) {
|
|
777
1681
|
watcher.add(this.pagesConfigSourcePaths);
|
|
778
|
-
const targetDirs = [...this.options.dirs, ...this.options.subPackages].map((v) =>
|
|
779
|
-
const
|
|
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
|
+
};
|
|
780
1687
|
watcher.on("add", async (path) => {
|
|
781
|
-
path =
|
|
782
|
-
if (!
|
|
783
|
-
if (!isInTargetDirs(path)) return;
|
|
1688
|
+
path = normalizePath(path);
|
|
1689
|
+
if (!isWatchedPageFile(path)) return;
|
|
784
1690
|
debug.pages(`File added: ${path}`);
|
|
785
1691
|
if (await this.updatePagesJSON()) this.onUpdate();
|
|
786
1692
|
});
|
|
787
1693
|
watcher.on("change", async (path) => {
|
|
788
|
-
path =
|
|
789
|
-
if (!isTargetFile(path)) return;
|
|
790
|
-
if (!isInTargetDirs(path)) return;
|
|
791
|
-
debug.pages(`File changed: ${path}`);
|
|
792
|
-
debug.pages(targetDirs);
|
|
793
|
-
debug.pages(isInTargetDirs(path));
|
|
794
|
-
if (await this.updatePagesJSON(path)) this.onUpdate();
|
|
795
|
-
});
|
|
796
|
-
watcher.on("change", async (path) => {
|
|
1694
|
+
path = normalizePath(path);
|
|
797
1695
|
if (this.pagesConfigSourcePaths.includes(path)) {
|
|
798
1696
|
debug.pages(`Config source changed: ${path}`);
|
|
799
1697
|
if (await this.updatePagesJSON()) this.onUpdate();
|
|
1698
|
+
return;
|
|
800
1699
|
}
|
|
1700
|
+
path = normalizePath(path);
|
|
1701
|
+
if (!isWatchedPageFile(path)) return;
|
|
1702
|
+
debug.pages(`File changed: ${path}`);
|
|
1703
|
+
if (await this.updatePagesJSON(path)) this.onUpdate();
|
|
801
1704
|
});
|
|
802
1705
|
watcher.on("unlink", async (path) => {
|
|
803
|
-
path =
|
|
804
|
-
if (!
|
|
805
|
-
if (!isInTargetDirs(path)) return;
|
|
1706
|
+
path = normalizePath(path);
|
|
1707
|
+
if (!isWatchedPageFile(path)) return;
|
|
806
1708
|
debug.pages(`File removed: ${path}`);
|
|
807
1709
|
if (await this.updatePagesJSON()) this.onUpdate();
|
|
808
1710
|
});
|
|
809
1711
|
}
|
|
810
1712
|
/**
|
|
811
|
-
*
|
|
812
|
-
*
|
|
1713
|
+
* 页面更新回调,页面文件或配置变化时触发
|
|
1714
|
+
* 负责使虚拟模块失效并通知浏览器整页刷新
|
|
813
1715
|
*/
|
|
814
1716
|
onUpdate() {
|
|
815
1717
|
if (!this._server) return;
|
|
@@ -818,115 +1720,19 @@ var PageContext = class {
|
|
|
818
1720
|
this._server.ws.send({ type: "full-reload" });
|
|
819
1721
|
}
|
|
820
1722
|
/**
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const generatedPageMetaData = await Promise.all(jobs);
|
|
831
|
-
const customPageMetaData = overrides || [];
|
|
832
|
-
const result = customPageMetaData.length ? mergePageMetaDataArray(generatedPageMetaData.concat(customPageMetaData)) : generatedPageMetaData;
|
|
833
|
-
const parseMeta = Array.from(result.reduce((map, page) => {
|
|
834
|
-
map.set(page.path, page);
|
|
835
|
-
return map;
|
|
836
|
-
}, /* @__PURE__ */ new Map()).values());
|
|
837
|
-
return type === "main" ? this.setHomePage(parseMeta) : parseMeta;
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* set home page
|
|
841
|
-
* @param result pages rules array
|
|
842
|
-
* @returns pages rules
|
|
843
|
-
*/
|
|
844
|
-
setHomePage(result) {
|
|
845
|
-
if (!result.some(({ type }) => type === "home")) {
|
|
846
|
-
const basePath = slash(path.join(this.options.root, this.options.outDir));
|
|
847
|
-
const resolvedHomePages = this.options.homePage.map((v) => {
|
|
848
|
-
return slash(path.relative(basePath, slash(path.resolve(basePath, v))));
|
|
849
|
-
});
|
|
850
|
-
const matchHomePage = (itemPath, configPath) => {
|
|
851
|
-
if (itemPath === configPath) return true;
|
|
852
|
-
const normalizedItem = itemPath.replace(/\\/g, "/");
|
|
853
|
-
const normalizedConfig = configPath.replace(/\\/g, "/");
|
|
854
|
-
return normalizedItem.endsWith(`/${normalizedConfig}`);
|
|
855
|
-
};
|
|
856
|
-
if (!result.some((item) => {
|
|
857
|
-
const isFound = resolvedHomePages.some((expectedPath) => matchHomePage(item.path, expectedPath));
|
|
858
|
-
if (isFound) item.type = "home";
|
|
859
|
-
return isFound;
|
|
860
|
-
})) 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 });
|
|
861
|
-
}
|
|
862
|
-
result.sort((page) => page.type === "home" ? -1 : 0);
|
|
863
|
-
return result;
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Merge main package page metadata
|
|
867
|
-
* Filter out pages belonging to sub-packages, then parse page metadata and merge user configuration
|
|
868
|
-
*/
|
|
869
|
-
async mergePageMetaData() {
|
|
870
|
-
const subPageAbsolutePaths = Array.from(this.subPages.values()).flatMap((v) => Array.from(v.keys()));
|
|
871
|
-
for (const subPageAbsolutePath of subPageAbsolutePaths) this.pages.delete(subPageAbsolutePath);
|
|
872
|
-
const pageMetaData = await this.parsePages(this.pages, "main", this.pagesGlobConfig?.pages);
|
|
873
|
-
this.pageMetaData = pageMetaData;
|
|
874
|
-
debug.pages(this.pageMetaData);
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Merge sub-package page metadata
|
|
878
|
-
* Parse page metadata for each sub-package and handle sub-package configuration inheritance
|
|
879
|
-
* Preserves sub-package level properties like plugins from user config
|
|
880
|
-
*/
|
|
881
|
-
async mergeSubPageMetaData() {
|
|
882
|
-
const subPageMaps = {};
|
|
883
|
-
const subPlugins = {};
|
|
884
|
-
const subPackages = this.pagesGlobConfig?.subPackages || [];
|
|
885
|
-
for (const [dir, pages] of this.subPages) {
|
|
886
|
-
const basePath = slash(path.join(this.options.root, this.options.outDir));
|
|
887
|
-
const root = this.options.subPackageRootMap.get(dir) ?? slash(path.relative(basePath, path.join(this.options.root, dir)));
|
|
888
|
-
const globPackage = subPackages?.find((v) => v.root === root);
|
|
889
|
-
subPageMaps[root] = await this.parsePages(pages, "sub", globPackage?.pages);
|
|
890
|
-
subPageMaps[root] = subPageMaps[root].map((page) => ({
|
|
891
|
-
...page,
|
|
892
|
-
path: slash(path.relative(root, page.path))
|
|
893
|
-
}));
|
|
894
|
-
if (globPackage?.plugins) subPlugins[root] = globPackage.plugins;
|
|
895
|
-
}
|
|
896
|
-
for (const { root, pages, plugins } of subPackages) if (root && !subPageMaps[root]) {
|
|
897
|
-
subPageMaps[root] = pages || [];
|
|
898
|
-
if (plugins) subPlugins[root] = plugins;
|
|
899
|
-
}
|
|
900
|
-
const subPageMetaData = Object.keys(subPageMaps).map((root) => ({
|
|
901
|
-
root,
|
|
902
|
-
pages: subPageMaps[root],
|
|
903
|
-
...subPlugins[root] && { plugins: subPlugins[root] }
|
|
904
|
-
})).filter((meta) => meta.pages.length > 0);
|
|
905
|
-
this.subPageMetaData = subPageMetaData;
|
|
906
|
-
debug.subPages(this.subPageMetaData);
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* Update pages.json file
|
|
910
|
-
* This is the core method responsible for coordinating the entire page configuration generation flow:
|
|
911
|
-
* 1. Check file changes (if filepath is specified)
|
|
912
|
-
* 2. Load user configuration
|
|
913
|
-
* 3. Scan page files
|
|
914
|
-
* 4. Merge page metadata
|
|
915
|
-
* 5. Generate and write pages.json
|
|
916
|
-
* @param filepath - Changed file path for incremental update judgment
|
|
917
|
-
* @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 是否成功更新
|
|
918
1732
|
*/
|
|
919
1733
|
async updatePagesJSON(filepath) {
|
|
920
1734
|
if (filepath) {
|
|
921
|
-
|
|
922
|
-
if (!page) {
|
|
923
|
-
let subPage;
|
|
924
|
-
for (const [_, pages] of this.subPages) {
|
|
925
|
-
subPage = pages.get(filepath);
|
|
926
|
-
if (subPage) break;
|
|
927
|
-
}
|
|
928
|
-
page = subPage;
|
|
929
|
-
}
|
|
1735
|
+
const page = this.findPage(filepath);
|
|
930
1736
|
if (page) {
|
|
931
1737
|
await page.read();
|
|
932
1738
|
if (!page.hasChanged()) {
|
|
@@ -936,66 +1742,69 @@ var PageContext = class {
|
|
|
936
1742
|
}
|
|
937
1743
|
}
|
|
938
1744
|
checkPagesJsonFileSync(this.resolvedPagesJSONPath);
|
|
939
|
-
this.options.onBeforeLoadUserConfig(
|
|
1745
|
+
this.options.onBeforeLoadUserConfig();
|
|
940
1746
|
await this.loadUserPagesConfig();
|
|
941
|
-
this.options.onAfterLoadUserConfig(this);
|
|
1747
|
+
this.options.onAfterLoadUserConfig(this.pagesGlobConfig);
|
|
942
1748
|
if (this.options.mergePages) {
|
|
943
|
-
this.options.onBeforeScanPages(
|
|
1749
|
+
this.options.onBeforeScanPages();
|
|
944
1750
|
await this.scanPages();
|
|
945
1751
|
await this.scanSubPages();
|
|
946
|
-
this.options.onAfterScanPages(this);
|
|
1752
|
+
this.options.onAfterScanPages(this.pages, this.subPages);
|
|
947
1753
|
}
|
|
948
|
-
this.options.onBeforeMergePageMetaData(this);
|
|
1754
|
+
this.options.onBeforeMergePageMetaData(this.pages, this.pagesGlobConfig);
|
|
949
1755
|
await this.mergePageMetaData();
|
|
950
1756
|
await this.mergeSubPageMetaData();
|
|
951
|
-
this.options.onAfterMergePageMetaData(this);
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
|
964
1776
|
}
|
|
965
|
-
await writeFileAtomic(this.resolvedPagesJSONPath, pagesJson);
|
|
966
|
-
this.lastPagesJson = pagesJson;
|
|
967
|
-
return true;
|
|
968
1777
|
});
|
|
969
1778
|
this.generateDeclaration();
|
|
970
|
-
if (updated) this.options.onAfterWriteFile(this);
|
|
971
|
-
return updated ?? false;
|
|
1779
|
+
if (result?.updated) this.options.onAfterWriteFile(this.resolvedPagesJSONPath, result.content);
|
|
1780
|
+
return result?.updated ?? false;
|
|
972
1781
|
}
|
|
973
1782
|
/**
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
* @returns
|
|
1783
|
+
* 生成虚拟模块内容
|
|
1784
|
+
* 返回包含 pages 与 subPackages 导出的 JavaScript 代码
|
|
1785
|
+
* @returns 虚拟模块代码字符串
|
|
977
1786
|
*/
|
|
978
1787
|
virtualModule() {
|
|
979
1788
|
return [`export const pages = ${this.resolveRoutes()};`, `export const subPackages = ${this.resolveSubRoutes()};`].join("\n");
|
|
980
1789
|
}
|
|
981
1790
|
/**
|
|
982
|
-
*
|
|
983
|
-
* @returns JSON
|
|
1791
|
+
* 将主包路由数据解析为 JSON 字符串
|
|
1792
|
+
* @returns 主包页面配置的 JSON 字符串
|
|
984
1793
|
*/
|
|
985
1794
|
resolveRoutes() {
|
|
986
1795
|
return stringify(this.pageMetaData, null, 2);
|
|
987
1796
|
}
|
|
988
1797
|
/**
|
|
989
|
-
*
|
|
990
|
-
* @returns JSON
|
|
1798
|
+
* 将子包路由数据解析为 JSON 字符串
|
|
1799
|
+
* @returns 子包页面配置的 JSON 字符串
|
|
991
1800
|
*/
|
|
992
1801
|
resolveSubRoutes() {
|
|
993
1802
|
return stringify(this.subPageMetaData, null, 2);
|
|
994
1803
|
}
|
|
995
1804
|
/**
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
* @returns
|
|
1805
|
+
* 解析 tabBar 配置
|
|
1806
|
+
* 将页面定义的 tabBar 项与配置文件定义的 tabBar 合并
|
|
1807
|
+
* @returns 合并后的 tabBar 配置对象,无 tabBar 时为 undefined
|
|
999
1808
|
*/
|
|
1000
1809
|
async resolveTabBar() {
|
|
1001
1810
|
const tabBarItems = [];
|
|
@@ -1008,8 +1817,7 @@ var PageContext = class {
|
|
|
1008
1817
|
...this.pagesGlobConfig?.tabBar,
|
|
1009
1818
|
list: this.pagesGlobConfig?.tabBar?.list || []
|
|
1010
1819
|
};
|
|
1011
|
-
const pagePaths =
|
|
1012
|
-
for (const item of tabBar.list) pagePaths.set(item.pagePath, true);
|
|
1820
|
+
const pagePaths = new Set(tabBar.list.map((item) => item.pagePath));
|
|
1013
1821
|
tabBarItems.sort((a, b) => a.index - b.index);
|
|
1014
1822
|
for (const item of tabBarItems) if (!pagePaths.has(item.pagePath)) {
|
|
1015
1823
|
const { index: _, ...tabbar } = item;
|
|
@@ -1018,262 +1826,322 @@ var PageContext = class {
|
|
|
1018
1826
|
return tabBar;
|
|
1019
1827
|
}
|
|
1020
1828
|
/**
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1829
|
+
* 生成 TypeScript 声明文件
|
|
1830
|
+
* 为页面路径生成类型定义,导航时提供类型提示
|
|
1023
1831
|
*/
|
|
1024
1832
|
generateDeclaration() {
|
|
1025
1833
|
if (!this.options.dts) return;
|
|
1026
1834
|
debug.declaration("generating");
|
|
1027
|
-
return writeDeclaration(
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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);
|
|
1041
1853
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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;
|
|
1052
1867
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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);
|
|
1060
1885
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const { list: oldList } = oldTabBar || {};
|
|
1064
|
-
const newList = mergePlatformItems(oldList, currentPlatform, list, "pagePath");
|
|
1065
|
-
pageJson.tabBar = {
|
|
1066
|
-
...tabBarOthers,
|
|
1067
|
-
list: newList
|
|
1068
|
-
};
|
|
1069
|
-
} else pageJson.tabBar = void 0;
|
|
1070
|
-
return pageJson;
|
|
1886
|
+
debug.subPages(JSON.stringify(paths, null, 2));
|
|
1887
|
+
this.subPages = subPages;
|
|
1071
1888
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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;
|
|
1077
1904
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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;
|
|
1081
1930
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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);
|
|
1085
1940
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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);
|
|
1089
1976
|
}
|
|
1090
1977
|
};
|
|
1091
1978
|
/**
|
|
1092
|
-
*
|
|
1093
|
-
* @param
|
|
1094
|
-
* @
|
|
1095
|
-
|
|
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 包含相对路径与绝对路径的页面路径数组
|
|
1096
1991
|
*/
|
|
1097
1992
|
function getPagePaths(dir, options) {
|
|
1098
|
-
const pagesDirPath =
|
|
1099
|
-
const basePath =
|
|
1993
|
+
const pagesDirPath = normalizePath(path.resolve(options.root, dir));
|
|
1994
|
+
const basePath = resolveBasePath(options);
|
|
1100
1995
|
const files = getPageFiles(pagesDirPath, options);
|
|
1101
1996
|
debug.pages(dir, files);
|
|
1102
|
-
return files.map((file) =>
|
|
1103
|
-
relativePath: path.relative(basePath,
|
|
1104
|
-
absolutePath:
|
|
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))
|
|
1105
2000
|
}));
|
|
1106
2001
|
}
|
|
1107
2002
|
/**
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
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
|
+
* 里的后缀剥掉。
|
|
1111
2016
|
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
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 过滤并剥掉后缀的新数组,入参不被修改
|
|
1117
2029
|
*/
|
|
1118
|
-
function
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
return JSON.stringify(rest);
|
|
1130
|
-
}
|
|
1131
|
-
return JSON.stringify(val);
|
|
1132
|
-
};
|
|
1133
|
-
const tmpMap = /* @__PURE__ */ new Map();
|
|
1134
|
-
for (let i = 0; i < src.length; i++) {
|
|
1135
|
-
const item = src[i];
|
|
1136
|
-
const uniqueKey = item[uniqueKeyName];
|
|
1137
|
-
if (!uniqueKey) continue;
|
|
1138
|
-
const ifdefComment = src[Symbol.for(`before:${i}`)]?.find((c) => c.value.trim().startsWith("#ifdef"));
|
|
1139
|
-
let platforms = [...lastPlatforms];
|
|
1140
|
-
if (ifdefComment) {
|
|
1141
|
-
const match = ifdefComment.value.match(/#ifdef\s+(.+)/);
|
|
1142
|
-
if (match) platforms = match[1].split("||").map((p) => p.trim()).filter((s) => s !== currentPlatform).sort();
|
|
1143
|
-
}
|
|
1144
|
-
if (platforms.length === 0) continue;
|
|
1145
|
-
const existing = tmpMap.get(uniqueKey) || [];
|
|
1146
|
-
existing.push({
|
|
1147
|
-
item,
|
|
1148
|
-
itemStr: stringifyForCompare(item),
|
|
1149
|
-
platforms,
|
|
1150
|
-
platformStr: platforms.join(" || ")
|
|
1151
|
-
});
|
|
1152
|
-
tmpMap.set(uniqueKey, existing);
|
|
1153
|
-
}
|
|
1154
|
-
for (const item of items) {
|
|
1155
|
-
const newItem = item;
|
|
1156
|
-
const uniqueKey = item[uniqueKeyName];
|
|
1157
|
-
if (!uniqueKey) continue;
|
|
1158
|
-
if (!tmpMap.has(uniqueKey)) {
|
|
1159
|
-
tmpMap.set(uniqueKey, [{
|
|
1160
|
-
item: newItem,
|
|
1161
|
-
itemStr: stringifyForCompare(newItem),
|
|
1162
|
-
platforms: [currentPlatform],
|
|
1163
|
-
platformStr: currentPlatform
|
|
1164
|
-
}]);
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
const existing = tmpMap.get(uniqueKey);
|
|
1168
|
-
const newItemStr = stringifyForCompare(newItem);
|
|
1169
|
-
const equalObj = existing.find((val) => val.itemStr === newItemStr);
|
|
1170
|
-
if (equalObj) {
|
|
1171
|
-
equalObj.platforms.push(currentPlatform);
|
|
1172
|
-
equalObj.platforms.sort();
|
|
1173
|
-
equalObj.platformStr = equalObj.platforms.join(" || ");
|
|
1174
|
-
} else existing.push({
|
|
1175
|
-
item: newItem,
|
|
1176
|
-
itemStr: stringifyForCompare(newItem),
|
|
1177
|
-
platforms: [currentPlatform],
|
|
1178
|
-
platformStr: currentPlatform
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
const result = new CommentArray();
|
|
1182
|
-
const platformUsage = {};
|
|
1183
|
-
tmpMap.forEach((val) => {
|
|
1184
|
-
Object.values(val).forEach((v) => {
|
|
1185
|
-
platformUsage[v.platformStr] = (platformUsage[v.platformStr] || 0) + 1;
|
|
1186
|
-
});
|
|
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
|
+
};
|
|
1187
2041
|
});
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
}
|
|
1204
|
-
}];
|
|
1205
|
-
for (const [_, list] of tmpMap) for (const { item, platformStr } of list) {
|
|
1206
|
-
result.push(item);
|
|
1207
|
-
if (platformStr !== defaultPlatformStr) {
|
|
1208
|
-
result[Symbol.for(`before:${result.length - 1}`)] = [{
|
|
1209
|
-
type: "LineComment",
|
|
1210
|
-
value: ` #ifdef ${platformStr}`,
|
|
1211
|
-
inline: false,
|
|
1212
|
-
loc: {
|
|
1213
|
-
start: {
|
|
1214
|
-
line: 0,
|
|
1215
|
-
column: 0
|
|
1216
|
-
},
|
|
1217
|
-
end: {
|
|
1218
|
-
line: 0,
|
|
1219
|
-
column: 0
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
}];
|
|
1223
|
-
result[Symbol.for(`after:${result.length - 1}`)] = [{
|
|
1224
|
-
type: "LineComment",
|
|
1225
|
-
value: " #endif",
|
|
1226
|
-
inline: false,
|
|
1227
|
-
loc: {
|
|
1228
|
-
start: {
|
|
1229
|
-
line: 0,
|
|
1230
|
-
column: 0
|
|
1231
|
-
},
|
|
1232
|
-
end: {
|
|
1233
|
-
line: 0,
|
|
1234
|
-
column: 0
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}];
|
|
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);
|
|
1238
2057
|
}
|
|
2058
|
+
result.push(mergedPage);
|
|
1239
2059
|
}
|
|
1240
2060
|
return result;
|
|
1241
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
|
+
}
|
|
1242
2078
|
//#endregion
|
|
1243
|
-
//#region src/config
|
|
2079
|
+
//#region src/config.ts
|
|
1244
2080
|
/**
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
2081
|
+
* 定义 uni-pages 配置
|
|
2082
|
+
* 在 pages.config.ts 配置文件中使用,提供类型提示与校验
|
|
1247
2083
|
*
|
|
1248
|
-
* @param config -
|
|
1249
|
-
* @returns
|
|
2084
|
+
* @param config - 页面配置对象
|
|
2085
|
+
* @returns 配置对象
|
|
1250
2086
|
*/
|
|
1251
2087
|
function defineUniPages(config) {
|
|
1252
2088
|
return config;
|
|
1253
2089
|
}
|
|
1254
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
|
|
1255
2123
|
//#region src/index.ts
|
|
1256
2124
|
/**
|
|
1257
|
-
* vite-plugin-uni-pages
|
|
2125
|
+
* vite-plugin-uni-pages 插件主入口
|
|
1258
2126
|
*
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
2127
|
+
* 自动扫描页面目录并生成 pages.json 配置文件
|
|
2128
|
+
* 支持 definePage 宏定义页面配置
|
|
2129
|
+
* 支持多平台条件编译
|
|
2130
|
+
* 支持分包配置
|
|
2131
|
+
* 支持 TypeScript 声明文件生成
|
|
1264
2132
|
*
|
|
1265
|
-
* @param userOptions -
|
|
1266
|
-
* @returns Vite
|
|
2133
|
+
* @param userOptions - 用户配置项
|
|
2134
|
+
* @returns Vite 插件实例
|
|
1267
2135
|
*/
|
|
1268
2136
|
function VitePluginUniPages(userOptions = {}) {
|
|
1269
2137
|
let ctx;
|
|
1270
|
-
checkPagesJsonFileSync(
|
|
2138
|
+
checkPagesJsonFileSync(resolvePagesJsonPath(process.env.VITE_ROOT_DIR || process.cwd(), userOptions.outDir ?? "src"));
|
|
1271
2139
|
return {
|
|
1272
2140
|
name: "vite-plugin-uni-pages",
|
|
1273
2141
|
enforce: "pre",
|
|
1274
2142
|
/**
|
|
1275
|
-
* Vite configResolved
|
|
1276
|
-
*
|
|
2143
|
+
* Vite configResolved 钩子
|
|
2144
|
+
* 初始化 PageContext,设置 logger,生成初始 pages.json
|
|
1277
2145
|
*/
|
|
1278
2146
|
async configResolved(config) {
|
|
1279
2147
|
ctx = new PageContext(userOptions, config.root);
|
|
@@ -1282,19 +2150,18 @@ function VitePluginUniPages(userOptions = {}) {
|
|
|
1282
2150
|
ctx.setLogger(logger);
|
|
1283
2151
|
await ctx.updatePagesJSON();
|
|
1284
2152
|
if (config.command === "build") {
|
|
1285
|
-
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)))));
|
|
1286
2154
|
}
|
|
1287
2155
|
},
|
|
1288
2156
|
/**
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
2157
|
+
* 代码转换钩子
|
|
2158
|
+
* 从 Vue SFC 中移除 definePage 宏调用,避免运行时报错
|
|
1291
2159
|
*/
|
|
1292
2160
|
async transform(code, id) {
|
|
1293
2161
|
if (!FILE_EXTENSIONS.some((ext) => id.endsWith(ext))) return null;
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
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
|
+
} });
|
|
1298
2165
|
if (!macro) return null;
|
|
1299
2166
|
const s = new MagicString(code);
|
|
1300
2167
|
s.remove(macro.start, macro.end);
|
|
@@ -1304,26 +2171,26 @@ function VitePluginUniPages(userOptions = {}) {
|
|
|
1304
2171
|
source: id,
|
|
1305
2172
|
includeContent: true,
|
|
1306
2173
|
file: `${id}.map`
|
|
1307
|
-
})
|
|
2174
|
+
}).toString()
|
|
1308
2175
|
};
|
|
1309
2176
|
},
|
|
1310
2177
|
/**
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
2178
|
+
* 配置开发服务器钩子
|
|
2179
|
+
* 设置文件监听与 HMR 支持
|
|
1313
2180
|
*/
|
|
1314
2181
|
configureServer(server) {
|
|
1315
2182
|
ctx.setupViteServer(server);
|
|
1316
2183
|
},
|
|
1317
2184
|
/**
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
2185
|
+
* 模块解析钩子
|
|
2186
|
+
* 将虚拟模块标识符解析为内部路径
|
|
1320
2187
|
*/
|
|
1321
2188
|
resolveId(id) {
|
|
1322
2189
|
if (id === "virtual:uni-pages") return RESOLVED_MODULE_ID_VIRTUAL;
|
|
1323
2190
|
},
|
|
1324
2191
|
/**
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
2192
|
+
* 模块加载钩子
|
|
2193
|
+
* 返回虚拟模块的代码内容
|
|
1327
2194
|
*/
|
|
1328
2195
|
load(id) {
|
|
1329
2196
|
if (id === RESOLVED_MODULE_ID_VIRTUAL) return ctx.virtualModule();
|
|
@@ -1331,4 +2198,4 @@ function VitePluginUniPages(userOptions = {}) {
|
|
|
1331
2198
|
};
|
|
1332
2199
|
}
|
|
1333
2200
|
//#endregion
|
|
1334
|
-
export { FILE_EXTENSIONS, MODULE_ID_VIRTUAL, OUTPUT_NAME, Page, PageContext, RESOLVED_MODULE_ID_VIRTUAL, VitePluginUniPages, VitePluginUniPages as default,
|
|
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 };
|