vite-plugin-uni-inject 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,41 +1,19 @@
1
- # vite-plugin-uni-inject
1
+ # Vite-plugin-uni-inject
2
2
 
3
- 利用 Vite 插件机制,实现自动注入代码,解放你的双手。
3
+ 一个轻量级的 uniapp 注入增强插件,旨在为 uniapp 项目提供更舒适的开发体验。
4
4
 
5
- ## 功能特点
5
+ ## 文档
6
6
 
7
- - 干净的注入任何代码,没有多余的结构。
8
- - 支持注入 `<page-meta/>` 这种只能放在页面第一个位置的标签节点。
9
- - 支持基于文件路由的 `page.json` 自动生成并注入,无需手动维护页面表。
7
+ 请阅读 [文档](https://kriac.github.io/vite-plugin-uni-inject) 了解更多使用细节。
10
8
 
11
- ## 如何使用
9
+ ## 使用
12
10
 
13
- ### 安装依赖
11
+ 如果你在开发过程中遇到了什么问题,或者有更好的建议,欢迎提交 issue 与我们讨论。
14
12
 
15
- ```bash
16
- pnpm i -D vite-plugin-uni-inject
17
- ```
13
+ ## 贡献
18
14
 
19
- ### vite.config.ts
15
+ 首先感谢你考虑为本项目做出贡献!我们欢迎社区成员的贡献,以帮助改进和扩展本项目。
20
16
 
21
- ```ts
22
- import { defineConfig } from "vite";
23
- import { uniAutoPages, uniInject } from "vite-plugin-uni-inject";
24
- import uni from "@dcloudio/vite-plugin-uni";
17
+ ## 执照
25
18
 
26
- // 如果有重写 page.json 文件的插件,请确保写在 uniInject 之前
27
- export default defineConfig(() => {
28
- return {
29
- plugins: [uniAutoPages(), uniInject(), uni()],
30
- };
31
- });
32
- ```
33
-
34
- ### 独立插件说明
35
-
36
- - `uniAutoPages(options)`:负责扫描文件路由并补全 `src/pages.json` 。
37
- - `uniInject(options)`:负责注入 `App.inject.vue`(或自定义文件)到页面文件。
38
-
39
- ## 报告错误
40
-
41
- 欢迎提交 issue 与我们讨论。
19
+ 本项目采用 MIT 许可证,详细内容请见 [LICENSE](LICENSE) 文件。
package/dist/index.cjs CHANGED
@@ -94,14 +94,18 @@ function uniInject(opts) {
94
94
  });
95
95
  let nextScriptCode = scriptCode;
96
96
  if (imports.length) {
97
- const ranges = [...imports].sort((a, b) => b.start - a.start);
97
+ const ranges = [...imports].sort((a, b) => {
98
+ return b.start - a.start;
99
+ });
98
100
  for (const { start: rangeStart, end: rangeEnd } of ranges) {
99
101
  let removeEnd = rangeEnd;
100
102
  if (nextScriptCode.startsWith("\r\n", removeEnd)) removeEnd += 2;
101
103
  else if (nextScriptCode[removeEnd] === "\n") removeEnd += 1;
102
104
  nextScriptCode = nextScriptCode.slice(0, rangeStart) + nextScriptCode.slice(removeEnd);
103
105
  }
104
- nextScriptCode = `\n${imports.map((item) => item.code).join("\n")}${nextScriptCode}`;
106
+ nextScriptCode = `\n${imports.map((item) => {
107
+ return item.code;
108
+ }).join("\n")}${nextScriptCode}`;
105
109
  }
106
110
  newCode = newCode.slice(0, start) + nextScriptCode + newCode.slice(end);
107
111
  }
@@ -111,6 +115,10 @@ function uniInject(opts) {
111
115
  }
112
116
  //#endregion
113
117
  //#region src/modules/auto-pages/index.ts
118
+ function readPagesJson(pagesJsonPath) {
119
+ if (!fs.default.existsSync(pagesJsonPath)) return null;
120
+ return JSON.parse(fs.default.readFileSync(pagesJsonPath, "utf-8"));
121
+ }
114
122
  function toPosixPath(value) {
115
123
  return value.replace(/\\/g, "/");
116
124
  }
@@ -130,55 +138,97 @@ function walkFiles(dirPath, filePaths = []) {
130
138
  }
131
139
  return filePaths;
132
140
  }
141
+ function analyzeVueContent(content, filePath) {
142
+ if (!content.includes("definePage")) return null;
143
+ const stripRanges = [];
144
+ const { descriptor } = (0, _vue_compiler_sfc.parse)(content);
145
+ const blocks = [descriptor.scriptSetup, descriptor.script].filter((b) => {
146
+ return Boolean(b);
147
+ });
148
+ let config = null;
149
+ for (const block of blocks) {
150
+ const src = block.content;
151
+ if (!src.includes("definePage")) continue;
152
+ let ast;
153
+ try {
154
+ ast = (0, _babel_parser.parse)(src, {
155
+ sourceType: "module",
156
+ plugins: ["typescript"]
157
+ });
158
+ } catch {
159
+ continue;
160
+ }
161
+ const base = block.loc.start.offset;
162
+ for (const node of ast.program.body) {
163
+ if (typeof node.start !== "number" || typeof node.end !== "number") continue;
164
+ if (node.type === "ImportDeclaration") {
165
+ if (node.specifiers.some((s) => {
166
+ return s.type === "ImportSpecifier" && s.imported.type === "Identifier" && s.imported.name === "definePage";
167
+ })) stripRanges.push({
168
+ start: base + node.start,
169
+ end: base + node.end
170
+ });
171
+ continue;
172
+ }
173
+ if (node.type !== "ExpressionStatement" || node.expression.type !== "CallExpression" || node.expression.callee.type !== "Identifier" || node.expression.callee.name !== "definePage") continue;
174
+ stripRanges.push({
175
+ start: base + node.start,
176
+ end: base + node.end
177
+ });
178
+ const arg = node.expression.arguments[0];
179
+ if (config || !arg || typeof arg.start !== "number" || typeof arg.end !== "number") continue;
180
+ const argSrc = src.slice(arg.start, arg.end);
181
+ try {
182
+ config = new Function(`return (${argSrc})`)();
183
+ } catch (err) {
184
+ console.warn(`解析失败(${filePath}): ${err.message}`);
185
+ }
186
+ }
187
+ }
188
+ if (!stripRanges.length && !config) return null;
189
+ return {
190
+ config,
191
+ stripRanges
192
+ };
193
+ }
133
194
  function collectFileRoutes(srcRoot, routeDirs) {
134
195
  const routeSet = /* @__PURE__ */ new Set();
135
- routeDirs.forEach((dir) => {
136
- walkFiles(path.default.resolve(srcRoot, dir)).forEach((filePath) => {
137
- if (!filePath.endsWith(".vue")) return;
138
- const noExt = path.default.relative(srcRoot, filePath).slice(0, -4);
139
- routeSet.add(toPosixPath(noExt));
140
- });
141
- });
142
- return Array.from(routeSet).sort();
196
+ const analysisMap = /* @__PURE__ */ new Map();
197
+ for (const dir of routeDirs) for (const filePath of walkFiles(path.default.resolve(srcRoot, dir))) {
198
+ if (!filePath.endsWith(".vue")) continue;
199
+ const route = toPosixPath(path.default.relative(srcRoot, filePath).slice(0, -4));
200
+ routeSet.add(route);
201
+ const result = analyzeVueContent(fs.default.readFileSync(filePath, "utf-8"), filePath);
202
+ if (result) analysisMap.set(route, result);
203
+ }
204
+ return {
205
+ routes: Array.from(routeSet).sort(),
206
+ analysisMap
207
+ };
143
208
  }
144
- function getScanDirs(dir, subPackages) {
209
+ function getScanDirs(mainPackage, subPackages) {
145
210
  const dirSet = /* @__PURE__ */ new Set();
146
- const normalizedMainDir = normalizeDir(dir);
147
- if (normalizedMainDir) dirSet.add(normalizedMainDir);
148
- subPackages.forEach((subRoot) => {
149
- const normalizedSubRoot = normalizeDir(subRoot);
150
- if (!normalizedSubRoot) return;
151
- if (normalizedMainDir) {
152
- dirSet.add(`${normalizedSubRoot}/${normalizedMainDir}`);
153
- return;
154
- }
155
- dirSet.add(normalizedSubRoot);
156
- });
211
+ const mainDir = normalizeDir(mainPackage);
212
+ if (mainDir) dirSet.add(mainDir);
213
+ for (const subRoot of subPackages) {
214
+ const sub = normalizeDir(subRoot);
215
+ if (!sub) continue;
216
+ dirSet.add(mainDir ? `${sub}/${mainDir}` : sub);
217
+ }
157
218
  return Array.from(dirSet);
158
219
  }
159
- function readPagesJson(pagesJsonPath) {
160
- if (!fs.default.existsSync(pagesJsonPath)) return null;
161
- const content = fs.default.readFileSync(pagesJsonPath, "utf-8");
162
- return JSON.parse(content);
163
- }
164
- function mergePages(routes, existingPages) {
165
- const existingMap = new Map(existingPages.map((p) => [p.path, p]));
220
+ function buildPages(routes, analysisMap, toFullRoute) {
166
221
  return routes.map((route) => {
167
- return existingMap.get(route) ?? { path: route };
168
- });
169
- }
170
- function hasPagesJsonChanged(current, next) {
171
- return JSON.stringify({
172
- pages: next.pages,
173
- subPackages: next.subPackages ?? []
174
- }) !== JSON.stringify({
175
- pages: current.pages ?? [],
176
- subPackages: current.subPackages ?? []
222
+ const cfg = analysisMap.get(toFullRoute(route))?.config;
223
+ if (!cfg) return { path: route };
224
+ return {
225
+ path: route,
226
+ ...cfg,
227
+ home: void 0
228
+ };
177
229
  });
178
230
  }
179
- function getPagesByFileRoute(routes, pagesJson, subPackageRoots) {
180
- const originPages = pagesJson.pages ?? [];
181
- const originSubPackages = pagesJson.subPackages ?? [];
231
+ function getPagesByFileRoute(routes, analysisMap, pagesJson, subPackageRoots) {
182
232
  const mainRoutes = [];
183
233
  const subRouteMap = /* @__PURE__ */ new Map();
184
234
  const normalizedSubRoots = subPackageRoots.map(normalizeDir);
@@ -187,58 +237,71 @@ function getPagesByFileRoute(routes, pagesJson, subPackageRoots) {
187
237
  return route.startsWith(root + "/");
188
238
  });
189
239
  if (matchedRoot) {
190
- const subPath = route.slice(matchedRoot.length + 1);
191
240
  const list = subRouteMap.get(matchedRoot) ?? [];
192
- list.push(subPath);
241
+ list.push(route.slice(matchedRoot.length + 1));
193
242
  subRouteMap.set(matchedRoot, list);
194
243
  } else mainRoutes.push(route);
195
244
  }
196
- const homePage = originPages[0]?.path;
197
- if (mainRoutes.includes(homePage)) {
198
- const idx = mainRoutes.indexOf(homePage);
199
- mainRoutes.splice(idx, 1);
200
- mainRoutes.unshift(homePage);
201
- }
202
- const pages = mergePages(mainRoutes, originPages);
203
- const subPackages = normalizedSubRoots.map((root) => {
204
- return {
205
- root,
206
- pages: mergePages(subRouteMap.get(root) ?? [], originSubPackages.find((s) => {
207
- return normalizeDir(s.root) === root;
208
- })?.pages ?? [])
209
- };
245
+ const homeRoute = mainRoutes.find((r) => {
246
+ return analysisMap.get(r)?.config?.home === true;
210
247
  });
211
- const merged = {
212
- ...pagesJson,
213
- pages,
214
- subPackages
215
- };
248
+ if (homeRoute) {
249
+ mainRoutes.splice(mainRoutes.indexOf(homeRoute), 1);
250
+ mainRoutes.unshift(homeRoute);
251
+ }
216
252
  return {
217
- merged,
218
- changed: hasPagesJsonChanged(pagesJson, merged)
253
+ ...pagesJson,
254
+ pages: buildPages(mainRoutes, analysisMap, (r) => {
255
+ return r;
256
+ }),
257
+ subPackages: normalizedSubRoots.map((root) => {
258
+ return {
259
+ root,
260
+ pages: buildPages(subRouteMap.get(root) ?? [], analysisMap, (r) => {
261
+ return `${root}/${r}`;
262
+ })
263
+ };
264
+ })
219
265
  };
220
266
  }
221
- function resolveDtsFilePath(srcRoot, dts) {
222
- return path.default.isAbsolute(dts) ? dts : path.default.resolve(srcRoot, dts);
223
- }
224
267
  function buildRouteDts(routes) {
225
- return [
226
- "// Auto-generated by vite-plugin-uni-inject. Do not edit.",
227
- "",
228
- "/** 提取路径 */",
229
- "export type ExtractPath<T extends string, Prefix extends string> = T extends `${Prefix}${infer P}` ? P : never;",
230
- "",
231
- "/** 路由路径 */",
232
- "export type RoutePath =",
233
- routes.map((route) => ` | "/${route}"`).join("\n") + ";",
234
- ""
235
- ].join("\n");
268
+ return `import type { DefinePageConfig } from "vite-plugin-uni-inject";
269
+
270
+ // Auto-generated by vite-plugin-uni-inject. Do not edit.
271
+
272
+ /** 提取路径 */
273
+ export type ExtractPath<T extends string, Prefix extends string> = T extends \`\${Prefix}\${infer P}\` ? P : never;
274
+
275
+ /** 路由路径 */
276
+ export type RoutePath = ${routes.map((r) => {
277
+ return `"/${r}"`;
278
+ }).join(` |\n`)};
279
+
280
+ declare global {
281
+ /**
282
+ * Vue \`<script setup>\` 宏定义 uniapp 页面级配置。
283
+ */
284
+ function definePage(config: DefinePageConfig): void;
285
+ }
286
+ `;
287
+ }
288
+ function getCachePath(cacheDir, targetPath) {
289
+ const cacheName = Buffer.from(path.default.resolve(targetPath)).toString("base64url");
290
+ return path.default.join(cacheDir, "vite-plugin-uni-inject", "auto-pages", cacheName);
291
+ }
292
+ function writeFileWithCache(targetPath, nextContent, cacheDir, forceWrite = false) {
293
+ const cachePath = getCachePath(cacheDir, targetPath);
294
+ if (!forceWrite && fs.default.existsSync(targetPath) && fs.default.existsSync(cachePath) && fs.default.readFileSync(cachePath, "utf-8") === nextContent) return;
295
+ fs.default.mkdirSync(path.default.dirname(targetPath), { recursive: true });
296
+ fs.default.writeFileSync(targetPath, nextContent);
297
+ fs.default.mkdirSync(path.default.dirname(cachePath), { recursive: true });
298
+ fs.default.writeFileSync(cachePath, nextContent);
236
299
  }
237
300
  /**
238
301
  * 自动补全 pages.json 插件
239
302
  */
240
303
  function uniAutoPages(opts) {
241
- const { dir = "pages", subPackages = [], dts = "uni-pages.d.ts" } = opts || {};
304
+ const { mainPackage = "pages", subPackages = [], dts = "uni-pages.d.ts" } = opts || {};
242
305
  return {
243
306
  name: "vite-plugin-uni-auto-pages",
244
307
  enforce: "pre",
@@ -247,16 +310,33 @@ function uniAutoPages(opts) {
247
310
  const pagesJsonPath = path.default.join(srcRoot, "pages.json");
248
311
  const pagesJson = readPagesJson(pagesJsonPath);
249
312
  if (!pagesJson) return;
250
- const routes = collectFileRoutes(srcRoot, getScanDirs(dir, subPackages));
251
- const { merged, changed } = getPagesByFileRoute(routes, pagesJson, subPackages);
252
- if (changed) {
253
- if (dts) {
254
- const dtsFilePath = resolveDtsFilePath(srcRoot, dts);
255
- const dtsContent = buildRouteDts(routes);
256
- fs.default.writeFileSync(dtsFilePath, dtsContent);
257
- }
258
- fs.default.writeFileSync(pagesJsonPath, JSON.stringify(merged, null, 2) + "\n");
313
+ const { routes, analysisMap } = collectFileRoutes(srcRoot, getScanDirs(mainPackage, subPackages));
314
+ const merged = getPagesByFileRoute(routes, analysisMap, pagesJson, subPackages);
315
+ const newPagesJson = JSON.stringify(merged, null, 2) + "\n";
316
+ const dtsPath = dts ? path.default.join(srcRoot, dts) : null;
317
+ const shouldRefreshPagesJson = dtsPath ? !fs.default.existsSync(dtsPath) : false;
318
+ writeFileWithCache(pagesJsonPath, newPagesJson, config.cacheDir, shouldRefreshPagesJson);
319
+ if (dtsPath) writeFileWithCache(dtsPath, buildRouteDts(routes), config.cacheDir);
320
+ },
321
+ transform(code, id) {
322
+ const pure = id.split("?")[0];
323
+ if (!pure.endsWith(".vue")) return;
324
+ const analysis = analyzeVueContent(code, pure);
325
+ if (!analysis?.stripRanges.length) return;
326
+ let newCode = code;
327
+ const ranges = [...analysis.stripRanges].sort((a, b) => {
328
+ return b.start - a.start;
329
+ });
330
+ for (const { start, end } of ranges) {
331
+ let removeEnd = end;
332
+ if (newCode.startsWith("\r\n", removeEnd)) removeEnd += 2;
333
+ else if (newCode[removeEnd] === "\n") removeEnd += 1;
334
+ newCode = newCode.slice(0, start) + newCode.slice(removeEnd);
259
335
  }
336
+ return {
337
+ code: newCode,
338
+ map: null
339
+ };
260
340
  }
261
341
  };
262
342
  }
package/dist/index.d.cts CHANGED
@@ -7,20 +7,49 @@ interface InjectPluginOptions {
7
7
  */
8
8
  path?: string;
9
9
  }
10
+ /** 页面 style 配置 */
11
+ interface UniPageStyle {
12
+ [key: string]: unknown;
13
+ /** 窗口背景色 */
14
+ backgroundColor?: string;
15
+ /** 下拉加载样式 */
16
+ backgroundTextStyle?: "dark" | "light";
17
+ /** 是否禁止页面滚动 */
18
+ disableScroll?: boolean;
19
+ /** 是否开启下拉刷新 */
20
+ enablePullDownRefresh?: boolean;
21
+ /** 导航栏背景颜色 */
22
+ navigationBarBackgroundColor?: string;
23
+ /** 导航栏标题颜色 */
24
+ navigationBarTextStyle?: "black" | "white";
25
+ /** 导航栏标题文字 */
26
+ navigationBarTitleText?: string;
27
+ /** 触底距离 */
28
+ onReachBottomDistance?: number;
29
+ /** 自定义组件配置 */
30
+ usingComponents?: Record<string, string>;
31
+ }
32
+ /** definePage 宏接收的页面配置 */
33
+ interface DefinePageConfig {
34
+ /** 标记为首页,仅主包页面生效。 */
35
+ home?: boolean;
36
+ /** 页面样式 */
37
+ style?: UniPageStyle;
38
+ }
10
39
  /** 自动补全 pages 插件配置 */
11
40
  interface AutoPagesPluginOptions {
12
- /**
13
- * 扫描目录
14
- * @default 'pages'
15
- */
16
- dir?: string;
17
41
  /**
18
42
  * 输出类型
19
43
  * @default './uni-pages.d.ts'
20
44
  */
21
45
  dts?: string;
22
46
  /**
23
- * 分包目录
47
+ * 主包目录
48
+ * @default 'pages'
49
+ */
50
+ mainPackage?: string;
51
+ /**
52
+ * 分包列表
24
53
  * @default []
25
54
  */
26
55
  subPackages?: string[];
@@ -49,8 +78,13 @@ declare function uniAutoPages(opts?: AutoPagesPluginOptions): {
49
78
  name: string;
50
79
  enforce: "pre";
51
80
  configResolved(config: {
81
+ cacheDir: string;
52
82
  root: string;
53
83
  }): void;
84
+ transform(code: string, id: string): {
85
+ code: string;
86
+ map: null;
87
+ } | undefined;
54
88
  };
55
89
  //#endregion
56
- export { uniAutoPages, uniInject };
90
+ export { type DefinePageConfig, uniAutoPages, uniInject };
package/dist/index.d.mts CHANGED
@@ -7,20 +7,49 @@ interface InjectPluginOptions {
7
7
  */
8
8
  path?: string;
9
9
  }
10
+ /** 页面 style 配置 */
11
+ interface UniPageStyle {
12
+ [key: string]: unknown;
13
+ /** 窗口背景色 */
14
+ backgroundColor?: string;
15
+ /** 下拉加载样式 */
16
+ backgroundTextStyle?: "dark" | "light";
17
+ /** 是否禁止页面滚动 */
18
+ disableScroll?: boolean;
19
+ /** 是否开启下拉刷新 */
20
+ enablePullDownRefresh?: boolean;
21
+ /** 导航栏背景颜色 */
22
+ navigationBarBackgroundColor?: string;
23
+ /** 导航栏标题颜色 */
24
+ navigationBarTextStyle?: "black" | "white";
25
+ /** 导航栏标题文字 */
26
+ navigationBarTitleText?: string;
27
+ /** 触底距离 */
28
+ onReachBottomDistance?: number;
29
+ /** 自定义组件配置 */
30
+ usingComponents?: Record<string, string>;
31
+ }
32
+ /** definePage 宏接收的页面配置 */
33
+ interface DefinePageConfig {
34
+ /** 标记为首页,仅主包页面生效。 */
35
+ home?: boolean;
36
+ /** 页面样式 */
37
+ style?: UniPageStyle;
38
+ }
10
39
  /** 自动补全 pages 插件配置 */
11
40
  interface AutoPagesPluginOptions {
12
- /**
13
- * 扫描目录
14
- * @default 'pages'
15
- */
16
- dir?: string;
17
41
  /**
18
42
  * 输出类型
19
43
  * @default './uni-pages.d.ts'
20
44
  */
21
45
  dts?: string;
22
46
  /**
23
- * 分包目录
47
+ * 主包目录
48
+ * @default 'pages'
49
+ */
50
+ mainPackage?: string;
51
+ /**
52
+ * 分包列表
24
53
  * @default []
25
54
  */
26
55
  subPackages?: string[];
@@ -49,8 +78,13 @@ declare function uniAutoPages(opts?: AutoPagesPluginOptions): {
49
78
  name: string;
50
79
  enforce: "pre";
51
80
  configResolved(config: {
81
+ cacheDir: string;
52
82
  root: string;
53
83
  }): void;
84
+ transform(code: string, id: string): {
85
+ code: string;
86
+ map: null;
87
+ } | undefined;
54
88
  };
55
89
  //#endregion
56
- export { uniAutoPages, uniInject };
90
+ export { type DefinePageConfig, uniAutoPages, uniInject };
package/dist/index.mjs CHANGED
@@ -69,14 +69,18 @@ function uniInject(opts) {
69
69
  });
70
70
  let nextScriptCode = scriptCode;
71
71
  if (imports.length) {
72
- const ranges = [...imports].sort((a, b) => b.start - a.start);
72
+ const ranges = [...imports].sort((a, b) => {
73
+ return b.start - a.start;
74
+ });
73
75
  for (const { start: rangeStart, end: rangeEnd } of ranges) {
74
76
  let removeEnd = rangeEnd;
75
77
  if (nextScriptCode.startsWith("\r\n", removeEnd)) removeEnd += 2;
76
78
  else if (nextScriptCode[removeEnd] === "\n") removeEnd += 1;
77
79
  nextScriptCode = nextScriptCode.slice(0, rangeStart) + nextScriptCode.slice(removeEnd);
78
80
  }
79
- nextScriptCode = `\n${imports.map((item) => item.code).join("\n")}${nextScriptCode}`;
81
+ nextScriptCode = `\n${imports.map((item) => {
82
+ return item.code;
83
+ }).join("\n")}${nextScriptCode}`;
80
84
  }
81
85
  newCode = newCode.slice(0, start) + nextScriptCode + newCode.slice(end);
82
86
  }
@@ -86,6 +90,10 @@ function uniInject(opts) {
86
90
  }
87
91
  //#endregion
88
92
  //#region src/modules/auto-pages/index.ts
93
+ function readPagesJson(pagesJsonPath) {
94
+ if (!fs.existsSync(pagesJsonPath)) return null;
95
+ return JSON.parse(fs.readFileSync(pagesJsonPath, "utf-8"));
96
+ }
89
97
  function toPosixPath(value) {
90
98
  return value.replace(/\\/g, "/");
91
99
  }
@@ -105,55 +113,97 @@ function walkFiles(dirPath, filePaths = []) {
105
113
  }
106
114
  return filePaths;
107
115
  }
116
+ function analyzeVueContent(content, filePath) {
117
+ if (!content.includes("definePage")) return null;
118
+ const stripRanges = [];
119
+ const { descriptor } = parse$1(content);
120
+ const blocks = [descriptor.scriptSetup, descriptor.script].filter((b) => {
121
+ return Boolean(b);
122
+ });
123
+ let config = null;
124
+ for (const block of blocks) {
125
+ const src = block.content;
126
+ if (!src.includes("definePage")) continue;
127
+ let ast;
128
+ try {
129
+ ast = parse(src, {
130
+ sourceType: "module",
131
+ plugins: ["typescript"]
132
+ });
133
+ } catch {
134
+ continue;
135
+ }
136
+ const base = block.loc.start.offset;
137
+ for (const node of ast.program.body) {
138
+ if (typeof node.start !== "number" || typeof node.end !== "number") continue;
139
+ if (node.type === "ImportDeclaration") {
140
+ if (node.specifiers.some((s) => {
141
+ return s.type === "ImportSpecifier" && s.imported.type === "Identifier" && s.imported.name === "definePage";
142
+ })) stripRanges.push({
143
+ start: base + node.start,
144
+ end: base + node.end
145
+ });
146
+ continue;
147
+ }
148
+ if (node.type !== "ExpressionStatement" || node.expression.type !== "CallExpression" || node.expression.callee.type !== "Identifier" || node.expression.callee.name !== "definePage") continue;
149
+ stripRanges.push({
150
+ start: base + node.start,
151
+ end: base + node.end
152
+ });
153
+ const arg = node.expression.arguments[0];
154
+ if (config || !arg || typeof arg.start !== "number" || typeof arg.end !== "number") continue;
155
+ const argSrc = src.slice(arg.start, arg.end);
156
+ try {
157
+ config = new Function(`return (${argSrc})`)();
158
+ } catch (err) {
159
+ console.warn(`解析失败(${filePath}): ${err.message}`);
160
+ }
161
+ }
162
+ }
163
+ if (!stripRanges.length && !config) return null;
164
+ return {
165
+ config,
166
+ stripRanges
167
+ };
168
+ }
108
169
  function collectFileRoutes(srcRoot, routeDirs) {
109
170
  const routeSet = /* @__PURE__ */ new Set();
110
- routeDirs.forEach((dir) => {
111
- walkFiles(path.resolve(srcRoot, dir)).forEach((filePath) => {
112
- if (!filePath.endsWith(".vue")) return;
113
- const noExt = path.relative(srcRoot, filePath).slice(0, -4);
114
- routeSet.add(toPosixPath(noExt));
115
- });
116
- });
117
- return Array.from(routeSet).sort();
171
+ const analysisMap = /* @__PURE__ */ new Map();
172
+ for (const dir of routeDirs) for (const filePath of walkFiles(path.resolve(srcRoot, dir))) {
173
+ if (!filePath.endsWith(".vue")) continue;
174
+ const route = toPosixPath(path.relative(srcRoot, filePath).slice(0, -4));
175
+ routeSet.add(route);
176
+ const result = analyzeVueContent(fs.readFileSync(filePath, "utf-8"), filePath);
177
+ if (result) analysisMap.set(route, result);
178
+ }
179
+ return {
180
+ routes: Array.from(routeSet).sort(),
181
+ analysisMap
182
+ };
118
183
  }
119
- function getScanDirs(dir, subPackages) {
184
+ function getScanDirs(mainPackage, subPackages) {
120
185
  const dirSet = /* @__PURE__ */ new Set();
121
- const normalizedMainDir = normalizeDir(dir);
122
- if (normalizedMainDir) dirSet.add(normalizedMainDir);
123
- subPackages.forEach((subRoot) => {
124
- const normalizedSubRoot = normalizeDir(subRoot);
125
- if (!normalizedSubRoot) return;
126
- if (normalizedMainDir) {
127
- dirSet.add(`${normalizedSubRoot}/${normalizedMainDir}`);
128
- return;
129
- }
130
- dirSet.add(normalizedSubRoot);
131
- });
186
+ const mainDir = normalizeDir(mainPackage);
187
+ if (mainDir) dirSet.add(mainDir);
188
+ for (const subRoot of subPackages) {
189
+ const sub = normalizeDir(subRoot);
190
+ if (!sub) continue;
191
+ dirSet.add(mainDir ? `${sub}/${mainDir}` : sub);
192
+ }
132
193
  return Array.from(dirSet);
133
194
  }
134
- function readPagesJson(pagesJsonPath) {
135
- if (!fs.existsSync(pagesJsonPath)) return null;
136
- const content = fs.readFileSync(pagesJsonPath, "utf-8");
137
- return JSON.parse(content);
138
- }
139
- function mergePages(routes, existingPages) {
140
- const existingMap = new Map(existingPages.map((p) => [p.path, p]));
195
+ function buildPages(routes, analysisMap, toFullRoute) {
141
196
  return routes.map((route) => {
142
- return existingMap.get(route) ?? { path: route };
143
- });
144
- }
145
- function hasPagesJsonChanged(current, next) {
146
- return JSON.stringify({
147
- pages: next.pages,
148
- subPackages: next.subPackages ?? []
149
- }) !== JSON.stringify({
150
- pages: current.pages ?? [],
151
- subPackages: current.subPackages ?? []
197
+ const cfg = analysisMap.get(toFullRoute(route))?.config;
198
+ if (!cfg) return { path: route };
199
+ return {
200
+ path: route,
201
+ ...cfg,
202
+ home: void 0
203
+ };
152
204
  });
153
205
  }
154
- function getPagesByFileRoute(routes, pagesJson, subPackageRoots) {
155
- const originPages = pagesJson.pages ?? [];
156
- const originSubPackages = pagesJson.subPackages ?? [];
206
+ function getPagesByFileRoute(routes, analysisMap, pagesJson, subPackageRoots) {
157
207
  const mainRoutes = [];
158
208
  const subRouteMap = /* @__PURE__ */ new Map();
159
209
  const normalizedSubRoots = subPackageRoots.map(normalizeDir);
@@ -162,58 +212,71 @@ function getPagesByFileRoute(routes, pagesJson, subPackageRoots) {
162
212
  return route.startsWith(root + "/");
163
213
  });
164
214
  if (matchedRoot) {
165
- const subPath = route.slice(matchedRoot.length + 1);
166
215
  const list = subRouteMap.get(matchedRoot) ?? [];
167
- list.push(subPath);
216
+ list.push(route.slice(matchedRoot.length + 1));
168
217
  subRouteMap.set(matchedRoot, list);
169
218
  } else mainRoutes.push(route);
170
219
  }
171
- const homePage = originPages[0]?.path;
172
- if (mainRoutes.includes(homePage)) {
173
- const idx = mainRoutes.indexOf(homePage);
174
- mainRoutes.splice(idx, 1);
175
- mainRoutes.unshift(homePage);
176
- }
177
- const pages = mergePages(mainRoutes, originPages);
178
- const subPackages = normalizedSubRoots.map((root) => {
179
- return {
180
- root,
181
- pages: mergePages(subRouteMap.get(root) ?? [], originSubPackages.find((s) => {
182
- return normalizeDir(s.root) === root;
183
- })?.pages ?? [])
184
- };
220
+ const homeRoute = mainRoutes.find((r) => {
221
+ return analysisMap.get(r)?.config?.home === true;
185
222
  });
186
- const merged = {
187
- ...pagesJson,
188
- pages,
189
- subPackages
190
- };
223
+ if (homeRoute) {
224
+ mainRoutes.splice(mainRoutes.indexOf(homeRoute), 1);
225
+ mainRoutes.unshift(homeRoute);
226
+ }
191
227
  return {
192
- merged,
193
- changed: hasPagesJsonChanged(pagesJson, merged)
228
+ ...pagesJson,
229
+ pages: buildPages(mainRoutes, analysisMap, (r) => {
230
+ return r;
231
+ }),
232
+ subPackages: normalizedSubRoots.map((root) => {
233
+ return {
234
+ root,
235
+ pages: buildPages(subRouteMap.get(root) ?? [], analysisMap, (r) => {
236
+ return `${root}/${r}`;
237
+ })
238
+ };
239
+ })
194
240
  };
195
241
  }
196
- function resolveDtsFilePath(srcRoot, dts) {
197
- return path.isAbsolute(dts) ? dts : path.resolve(srcRoot, dts);
198
- }
199
242
  function buildRouteDts(routes) {
200
- return [
201
- "// Auto-generated by vite-plugin-uni-inject. Do not edit.",
202
- "",
203
- "/** 提取路径 */",
204
- "export type ExtractPath<T extends string, Prefix extends string> = T extends `${Prefix}${infer P}` ? P : never;",
205
- "",
206
- "/** 路由路径 */",
207
- "export type RoutePath =",
208
- routes.map((route) => ` | "/${route}"`).join("\n") + ";",
209
- ""
210
- ].join("\n");
243
+ return `import type { DefinePageConfig } from "vite-plugin-uni-inject";
244
+
245
+ // Auto-generated by vite-plugin-uni-inject. Do not edit.
246
+
247
+ /** 提取路径 */
248
+ export type ExtractPath<T extends string, Prefix extends string> = T extends \`\${Prefix}\${infer P}\` ? P : never;
249
+
250
+ /** 路由路径 */
251
+ export type RoutePath = ${routes.map((r) => {
252
+ return `"/${r}"`;
253
+ }).join(` |\n`)};
254
+
255
+ declare global {
256
+ /**
257
+ * Vue \`<script setup>\` 宏定义 uniapp 页面级配置。
258
+ */
259
+ function definePage(config: DefinePageConfig): void;
260
+ }
261
+ `;
262
+ }
263
+ function getCachePath(cacheDir, targetPath) {
264
+ const cacheName = Buffer.from(path.resolve(targetPath)).toString("base64url");
265
+ return path.join(cacheDir, "vite-plugin-uni-inject", "auto-pages", cacheName);
266
+ }
267
+ function writeFileWithCache(targetPath, nextContent, cacheDir, forceWrite = false) {
268
+ const cachePath = getCachePath(cacheDir, targetPath);
269
+ if (!forceWrite && fs.existsSync(targetPath) && fs.existsSync(cachePath) && fs.readFileSync(cachePath, "utf-8") === nextContent) return;
270
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
271
+ fs.writeFileSync(targetPath, nextContent);
272
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
273
+ fs.writeFileSync(cachePath, nextContent);
211
274
  }
212
275
  /**
213
276
  * 自动补全 pages.json 插件
214
277
  */
215
278
  function uniAutoPages(opts) {
216
- const { dir = "pages", subPackages = [], dts = "uni-pages.d.ts" } = opts || {};
279
+ const { mainPackage = "pages", subPackages = [], dts = "uni-pages.d.ts" } = opts || {};
217
280
  return {
218
281
  name: "vite-plugin-uni-auto-pages",
219
282
  enforce: "pre",
@@ -222,16 +285,33 @@ function uniAutoPages(opts) {
222
285
  const pagesJsonPath = path.join(srcRoot, "pages.json");
223
286
  const pagesJson = readPagesJson(pagesJsonPath);
224
287
  if (!pagesJson) return;
225
- const routes = collectFileRoutes(srcRoot, getScanDirs(dir, subPackages));
226
- const { merged, changed } = getPagesByFileRoute(routes, pagesJson, subPackages);
227
- if (changed) {
228
- if (dts) {
229
- const dtsFilePath = resolveDtsFilePath(srcRoot, dts);
230
- const dtsContent = buildRouteDts(routes);
231
- fs.writeFileSync(dtsFilePath, dtsContent);
232
- }
233
- fs.writeFileSync(pagesJsonPath, JSON.stringify(merged, null, 2) + "\n");
288
+ const { routes, analysisMap } = collectFileRoutes(srcRoot, getScanDirs(mainPackage, subPackages));
289
+ const merged = getPagesByFileRoute(routes, analysisMap, pagesJson, subPackages);
290
+ const newPagesJson = JSON.stringify(merged, null, 2) + "\n";
291
+ const dtsPath = dts ? path.join(srcRoot, dts) : null;
292
+ const shouldRefreshPagesJson = dtsPath ? !fs.existsSync(dtsPath) : false;
293
+ writeFileWithCache(pagesJsonPath, newPagesJson, config.cacheDir, shouldRefreshPagesJson);
294
+ if (dtsPath) writeFileWithCache(dtsPath, buildRouteDts(routes), config.cacheDir);
295
+ },
296
+ transform(code, id) {
297
+ const pure = id.split("?")[0];
298
+ if (!pure.endsWith(".vue")) return;
299
+ const analysis = analyzeVueContent(code, pure);
300
+ if (!analysis?.stripRanges.length) return;
301
+ let newCode = code;
302
+ const ranges = [...analysis.stripRanges].sort((a, b) => {
303
+ return b.start - a.start;
304
+ });
305
+ for (const { start, end } of ranges) {
306
+ let removeEnd = end;
307
+ if (newCode.startsWith("\r\n", removeEnd)) removeEnd += 2;
308
+ else if (newCode[removeEnd] === "\n") removeEnd += 1;
309
+ newCode = newCode.slice(0, start) + newCode.slice(removeEnd);
234
310
  }
311
+ return {
312
+ code: newCode,
313
+ map: null
314
+ };
235
315
  }
236
316
  };
237
317
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Kriac",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "version": "0.4.0",
7
+ "version": "0.6.0",
8
8
  "homepage": "https://github.com/Kriac/vite-plugin-uni-inject",
9
9
  "repository": {
10
10
  "type": "git",