vite-plugin-uni-inject 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,12 +24,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  let _babel_parser = require("@babel/parser");
25
25
  let _vue_compiler_sfc = require("@vue/compiler-sfc");
26
26
  let fs = require("fs");
27
- fs = __toESM(fs);
27
+ fs = __toESM(fs, 1);
28
28
  let path = require("path");
29
- path = __toESM(path);
30
- let magic_string = require("magic-string");
31
- magic_string = __toESM(magic_string);
32
- //#region src/inject.ts
29
+ path = __toESM(path, 1);
30
+ //#region src/modules/inject/index.ts
33
31
  /**
34
32
  * 注入代码插件
35
33
  */
@@ -84,25 +82,39 @@ function uniInject(opts) {
84
82
  const start = newDescriptor.scriptSetup.loc.start.offset;
85
83
  const end = newDescriptor.scriptSetup.loc.end.offset;
86
84
  const scriptCode = newDescriptor.scriptSetup.content;
87
- const s = new magic_string.default(scriptCode);
88
85
  const ast = (0, _babel_parser.parse)(scriptCode, {
89
86
  sourceType: "module",
90
87
  plugins: ["typescript"]
91
88
  });
92
89
  const imports = [];
93
- for (const node of ast.program.body) if (node.type === "ImportDeclaration") {
94
- imports.push(scriptCode.slice(node.start, node.end));
95
- s.remove(node.start, node.end + 1);
90
+ for (const node of ast.program.body) if (node.type === "ImportDeclaration" && typeof node.start === "number" && typeof node.end === "number") imports.push({
91
+ code: scriptCode.slice(node.start, node.end),
92
+ start: node.start,
93
+ end: node.end
94
+ });
95
+ let nextScriptCode = scriptCode;
96
+ if (imports.length) {
97
+ const ranges = [...imports].sort((a, b) => b.start - a.start);
98
+ for (const { start: rangeStart, end: rangeEnd } of ranges) {
99
+ let removeEnd = rangeEnd;
100
+ if (nextScriptCode.startsWith("\r\n", removeEnd)) removeEnd += 2;
101
+ else if (nextScriptCode[removeEnd] === "\n") removeEnd += 1;
102
+ nextScriptCode = nextScriptCode.slice(0, rangeStart) + nextScriptCode.slice(removeEnd);
103
+ }
104
+ nextScriptCode = `\n${imports.map((item) => item.code).join("\n")}${nextScriptCode}`;
96
105
  }
97
- if (imports.length) s.prepend("\n" + imports.join("\n"));
98
- newCode = newCode.slice(0, start) + s.toString() + newCode.slice(end);
106
+ newCode = newCode.slice(0, start) + nextScriptCode + newCode.slice(end);
99
107
  }
100
108
  return { code: newCode };
101
109
  }
102
110
  };
103
111
  }
104
112
  //#endregion
105
- //#region src/auto-pages.ts
113
+ //#region src/modules/auto-pages/index.ts
114
+ function readPagesJson(pagesJsonPath) {
115
+ if (!fs.default.existsSync(pagesJsonPath)) return null;
116
+ return JSON.parse(fs.default.readFileSync(pagesJsonPath, "utf-8"));
117
+ }
106
118
  function toPosixPath(value) {
107
119
  return value.replace(/\\/g, "/");
108
120
  }
@@ -122,115 +134,152 @@ function walkFiles(dirPath, filePaths = []) {
122
134
  }
123
135
  return filePaths;
124
136
  }
137
+ function analyzeVueContent(content, filePath) {
138
+ if (!content.includes("definePage")) return null;
139
+ const stripRanges = [];
140
+ const { descriptor } = (0, _vue_compiler_sfc.parse)(content);
141
+ const blocks = [descriptor.scriptSetup, descriptor.script].filter((b) => Boolean(b));
142
+ let config = null;
143
+ for (const block of blocks) {
144
+ const src = block.content;
145
+ if (!src.includes("definePage")) continue;
146
+ let ast;
147
+ try {
148
+ ast = (0, _babel_parser.parse)(src, {
149
+ sourceType: "module",
150
+ plugins: ["typescript"]
151
+ });
152
+ } catch {
153
+ continue;
154
+ }
155
+ const base = block.loc.start.offset;
156
+ for (const node of ast.program.body) {
157
+ if (typeof node.start !== "number" || typeof node.end !== "number") continue;
158
+ if (node.type === "ImportDeclaration") {
159
+ if (node.specifiers.some((s) => s.type === "ImportSpecifier" && s.imported.type === "Identifier" && s.imported.name === "definePage")) stripRanges.push({
160
+ start: base + node.start,
161
+ end: base + node.end
162
+ });
163
+ continue;
164
+ }
165
+ if (node.type !== "ExpressionStatement" || node.expression.type !== "CallExpression" || node.expression.callee.type !== "Identifier" || node.expression.callee.name !== "definePage") continue;
166
+ stripRanges.push({
167
+ start: base + node.start,
168
+ end: base + node.end
169
+ });
170
+ const arg = node.expression.arguments[0];
171
+ if (config || !arg || typeof arg.start !== "number" || typeof arg.end !== "number") continue;
172
+ const argSrc = src.slice(arg.start, arg.end);
173
+ try {
174
+ config = new Function(`return (${argSrc})`)();
175
+ } catch (err) {
176
+ console.warn(`解析失败(${filePath}): ${err.message}`);
177
+ }
178
+ }
179
+ }
180
+ if (!stripRanges.length && !config) return null;
181
+ return {
182
+ config,
183
+ stripRanges
184
+ };
185
+ }
125
186
  function collectFileRoutes(srcRoot, routeDirs) {
126
187
  const routeSet = /* @__PURE__ */ new Set();
127
- routeDirs.forEach((dir) => {
128
- walkFiles(path.default.resolve(srcRoot, dir)).forEach((filePath) => {
129
- if (!filePath.endsWith(".vue")) return;
130
- const noExt = path.default.relative(srcRoot, filePath).slice(0, -4);
131
- routeSet.add(toPosixPath(noExt));
132
- });
133
- });
134
- return Array.from(routeSet).sort();
188
+ const analysisMap = /* @__PURE__ */ new Map();
189
+ for (const dir of routeDirs) for (const filePath of walkFiles(path.default.resolve(srcRoot, dir))) {
190
+ if (!filePath.endsWith(".vue")) continue;
191
+ const route = toPosixPath(path.default.relative(srcRoot, filePath).slice(0, -4));
192
+ routeSet.add(route);
193
+ const result = analyzeVueContent(fs.default.readFileSync(filePath, "utf-8"), filePath);
194
+ if (result) analysisMap.set(route, result);
195
+ }
196
+ return {
197
+ routes: Array.from(routeSet).sort(),
198
+ analysisMap
199
+ };
135
200
  }
136
- function getScanDirs(dir, subPackages) {
201
+ function getScanDirs(mainPackage, subPackages) {
137
202
  const dirSet = /* @__PURE__ */ new Set();
138
- const normalizedMainDir = normalizeDir(dir);
139
- if (normalizedMainDir) dirSet.add(normalizedMainDir);
140
- subPackages.forEach((subRoot) => {
141
- const normalizedSubRoot = normalizeDir(subRoot);
142
- if (!normalizedSubRoot) return;
143
- if (normalizedMainDir) {
144
- dirSet.add(`${normalizedSubRoot}/${normalizedMainDir}`);
145
- return;
146
- }
147
- dirSet.add(normalizedSubRoot);
148
- });
203
+ const mainDir = normalizeDir(mainPackage);
204
+ if (mainDir) dirSet.add(mainDir);
205
+ for (const subRoot of subPackages) {
206
+ const sub = normalizeDir(subRoot);
207
+ if (!sub) continue;
208
+ dirSet.add(mainDir ? `${sub}/${mainDir}` : sub);
209
+ }
149
210
  return Array.from(dirSet);
150
211
  }
151
- function readPagesJson(pagesJsonPath) {
152
- if (!fs.default.existsSync(pagesJsonPath)) return null;
153
- const content = fs.default.readFileSync(pagesJsonPath, "utf-8");
154
- return JSON.parse(content);
155
- }
156
- function mergePages(routes, existingPages) {
157
- const existingMap = new Map(existingPages.map((p) => [p.path, p]));
212
+ function buildPages(routes, analysisMap, toFullRoute) {
158
213
  return routes.map((route) => {
159
- return existingMap.get(route) ?? { path: route };
160
- });
161
- }
162
- function hasPagesJsonChanged(current, next) {
163
- return JSON.stringify({
164
- pages: next.pages,
165
- subPackages: next.subPackages ?? []
166
- }) !== JSON.stringify({
167
- pages: current.pages ?? [],
168
- subPackages: current.subPackages ?? []
214
+ const cfg = analysisMap.get(toFullRoute(route))?.config;
215
+ if (!cfg) return { path: route };
216
+ return {
217
+ path: route,
218
+ ...cfg,
219
+ home: void 0
220
+ };
169
221
  });
170
222
  }
171
- function getPagesByFileRoute(routes, pagesJson, subPackageRoots) {
172
- const originPages = pagesJson.pages ?? [];
173
- const originSubPackages = pagesJson.subPackages ?? [];
223
+ function getPagesByFileRoute(routes, analysisMap, pagesJson, subPackageRoots) {
174
224
  const mainRoutes = [];
175
225
  const subRouteMap = /* @__PURE__ */ new Map();
176
226
  const normalizedSubRoots = subPackageRoots.map(normalizeDir);
177
227
  for (const route of routes) {
178
- const matchedRoot = normalizedSubRoots.find((root) => {
179
- return route.startsWith(root + "/");
180
- });
228
+ const matchedRoot = normalizedSubRoots.find((root) => route.startsWith(root + "/"));
181
229
  if (matchedRoot) {
182
- const subPath = route.slice(matchedRoot.length + 1);
183
230
  const list = subRouteMap.get(matchedRoot) ?? [];
184
- list.push(subPath);
231
+ list.push(route.slice(matchedRoot.length + 1));
185
232
  subRouteMap.set(matchedRoot, list);
186
233
  } else mainRoutes.push(route);
187
234
  }
188
- const homePage = originPages[0]?.path;
189
- if (mainRoutes.includes(homePage)) {
190
- const idx = mainRoutes.indexOf(homePage);
191
- mainRoutes.splice(idx, 1);
192
- mainRoutes.unshift(homePage);
193
- }
194
- const pages = mergePages(mainRoutes, originPages);
195
- const subPackages = normalizedSubRoots.map((root) => {
196
- return {
197
- root,
198
- pages: mergePages(subRouteMap.get(root) ?? [], originSubPackages.find((s) => {
199
- return normalizeDir(s.root) === root;
200
- })?.pages ?? [])
201
- };
235
+ const homeRoute = mainRoutes.find((r) => {
236
+ return analysisMap.get(r)?.config?.home === true;
202
237
  });
203
- const merged = {
204
- ...pagesJson,
205
- pages,
206
- subPackages
207
- };
238
+ if (homeRoute) {
239
+ mainRoutes.splice(mainRoutes.indexOf(homeRoute), 1);
240
+ mainRoutes.unshift(homeRoute);
241
+ }
208
242
  return {
209
- merged,
210
- changed: hasPagesJsonChanged(pagesJson, merged)
243
+ ...pagesJson,
244
+ pages: buildPages(mainRoutes, analysisMap, (r) => r),
245
+ subPackages: normalizedSubRoots.map((root) => ({
246
+ root,
247
+ pages: buildPages(subRouteMap.get(root) ?? [], analysisMap, (r) => `${root}/${r}`)
248
+ }))
211
249
  };
212
250
  }
213
- function resolveDtsFilePath(srcRoot, dts) {
214
- return path.default.isAbsolute(dts) ? dts : path.default.resolve(srcRoot, dts);
215
- }
216
251
  function buildRouteDts(routes) {
217
- return [
218
- "// Auto-generated by vite-plugin-uni-inject. Do not edit.",
219
- "",
220
- "/** 提取路径 */",
221
- "export type ExtractPath<T extends string, Prefix extends string> = T extends `${Prefix}${infer P}` ? P : never;",
222
- "",
223
- "/** 路由路径 */",
224
- "export type RoutePath =",
225
- routes.map((route) => ` | "/${route}"`).join("\n") + ";",
226
- ""
227
- ].join("\n");
252
+ return `import type { DefinePageConfig } from "vite-plugin-uni-inject";
253
+
254
+ // Auto-generated by vite-plugin-uni-inject. Do not edit.
255
+
256
+ /** 提取路径 */
257
+ export type ExtractPath<T extends string, Prefix extends string> = T extends \`\${Prefix}\${infer P}\` ? P : never;
258
+
259
+ /** 路由路径 */
260
+ export type RoutePath = ${routes.map((r) => `"/${r}"`).join(` |\n`)};
261
+
262
+ declare global {
263
+ /**
264
+ * Vue \`<script setup>\` 宏定义 uniapp 页面级配置。
265
+ */
266
+ function definePage(config: DefinePageConfig): void;
267
+ }
268
+ `;
269
+ }
270
+ function writeIfTextChanged(targetPath, nextContent) {
271
+ const normalize = (s) => {
272
+ return s.replace(/\s+/g, "").replace(/,(?=[)\]}>])/g, "").replace(/([=(<,|&?:])\|/g, "$1");
273
+ };
274
+ if (fs.default.existsSync(targetPath) && normalize(fs.default.readFileSync(targetPath, "utf-8")) === normalize(nextContent)) return;
275
+ fs.default.mkdirSync(path.default.dirname(targetPath), { recursive: true });
276
+ fs.default.writeFileSync(targetPath, nextContent);
228
277
  }
229
278
  /**
230
279
  * 自动补全 pages.json 插件
231
280
  */
232
281
  function uniAutoPages(opts) {
233
- const { dir = "pages", subPackages = [], dts = "uni-pages.d.ts" } = opts || {};
282
+ const { mainPackage = "pages", subPackages = [], dts = "uni-pages.d.ts" } = opts || {};
234
283
  return {
235
284
  name: "vite-plugin-uni-auto-pages",
236
285
  enforce: "pre",
@@ -239,14 +288,31 @@ function uniAutoPages(opts) {
239
288
  const pagesJsonPath = path.default.join(srcRoot, "pages.json");
240
289
  const pagesJson = readPagesJson(pagesJsonPath);
241
290
  if (!pagesJson) return;
242
- const routes = collectFileRoutes(srcRoot, getScanDirs(dir, subPackages));
243
- const { merged, changed } = getPagesByFileRoute(routes, pagesJson, subPackages);
244
- if (changed) fs.default.writeFileSync(pagesJsonPath, JSON.stringify(merged, null, 2) + "\n");
291
+ const { routes, analysisMap } = collectFileRoutes(srcRoot, getScanDirs(mainPackage, subPackages));
292
+ const merged = getPagesByFileRoute(routes, analysisMap, pagesJson, subPackages);
293
+ writeIfTextChanged(pagesJsonPath, JSON.stringify(merged, null, 2) + "\n");
245
294
  if (dts) {
246
- const dtsFilePath = resolveDtsFilePath(srcRoot, dts);
247
- const dtsContent = buildRouteDts(routes);
248
- fs.default.writeFileSync(dtsFilePath, dtsContent);
295
+ const newDts = buildRouteDts(routes);
296
+ writeIfTextChanged(path.default.join(srcRoot, dts), newDts);
297
+ }
298
+ },
299
+ transform(code, id) {
300
+ const pure = id.split("?")[0];
301
+ if (!pure.endsWith(".vue")) return;
302
+ const analysis = analyzeVueContent(code, pure);
303
+ if (!analysis?.stripRanges.length) return;
304
+ let newCode = code;
305
+ const ranges = [...analysis.stripRanges].sort((a, b) => b.start - a.start);
306
+ for (const { start, end } of ranges) {
307
+ let removeEnd = end;
308
+ if (newCode.startsWith("\r\n", removeEnd)) removeEnd += 2;
309
+ else if (newCode[removeEnd] === "\n") removeEnd += 1;
310
+ newCode = newCode.slice(0, start) + newCode.slice(removeEnd);
249
311
  }
312
+ return {
313
+ code: newCode,
314
+ map: null
315
+ };
250
316
  }
251
317
  };
252
318
  }
package/dist/index.d.cts CHANGED
@@ -7,6 +7,35 @@ 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
41
  /**
@@ -15,18 +44,18 @@ interface AutoPagesPluginOptions {
15
44
  */
16
45
  dts?: string;
17
46
  /**
18
- * 扫描目录
47
+ * 主包目录
19
48
  * @default 'pages'
20
49
  */
21
- dir?: string;
50
+ mainPackage?: string;
22
51
  /**
23
- * 分包目录
52
+ * 分包列表
24
53
  * @default []
25
54
  */
26
55
  subPackages?: string[];
27
56
  }
28
57
  //#endregion
29
- //#region src/inject.d.ts
58
+ //#region src/modules/inject/index.d.ts
30
59
  /**
31
60
  * 注入代码插件
32
61
  */
@@ -41,7 +70,7 @@ declare function uniInject(opts?: InjectPluginOptions): {
41
70
  } | undefined;
42
71
  };
43
72
  //#endregion
44
- //#region src/auto-pages.d.ts
73
+ //#region src/modules/auto-pages/index.d.ts
45
74
  /**
46
75
  * 自动补全 pages.json 插件
47
76
  */
@@ -51,6 +80,10 @@ declare function uniAutoPages(opts?: AutoPagesPluginOptions): {
51
80
  configResolved(config: {
52
81
  root: string;
53
82
  }): void;
83
+ transform(code: string, id: string): {
84
+ code: string;
85
+ map: null;
86
+ } | undefined;
54
87
  };
55
88
  //#endregion
56
- export { uniAutoPages, uniInject };
89
+ export { type DefinePageConfig, uniAutoPages, uniInject };
package/dist/index.d.mts CHANGED
@@ -7,6 +7,35 @@ 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
41
  /**
@@ -15,18 +44,18 @@ interface AutoPagesPluginOptions {
15
44
  */
16
45
  dts?: string;
17
46
  /**
18
- * 扫描目录
47
+ * 主包目录
19
48
  * @default 'pages'
20
49
  */
21
- dir?: string;
50
+ mainPackage?: string;
22
51
  /**
23
- * 分包目录
52
+ * 分包列表
24
53
  * @default []
25
54
  */
26
55
  subPackages?: string[];
27
56
  }
28
57
  //#endregion
29
- //#region src/inject.d.ts
58
+ //#region src/modules/inject/index.d.ts
30
59
  /**
31
60
  * 注入代码插件
32
61
  */
@@ -41,7 +70,7 @@ declare function uniInject(opts?: InjectPluginOptions): {
41
70
  } | undefined;
42
71
  };
43
72
  //#endregion
44
- //#region src/auto-pages.d.ts
73
+ //#region src/modules/auto-pages/index.d.ts
45
74
  /**
46
75
  * 自动补全 pages.json 插件
47
76
  */
@@ -51,6 +80,10 @@ declare function uniAutoPages(opts?: AutoPagesPluginOptions): {
51
80
  configResolved(config: {
52
81
  root: string;
53
82
  }): void;
83
+ transform(code: string, id: string): {
84
+ code: string;
85
+ map: null;
86
+ } | undefined;
54
87
  };
55
88
  //#endregion
56
- export { uniAutoPages, uniInject };
89
+ export { type DefinePageConfig, uniAutoPages, uniInject };
package/dist/index.mjs CHANGED
@@ -2,8 +2,7 @@ import { parse } from "@babel/parser";
2
2
  import { parse as parse$1 } from "@vue/compiler-sfc";
3
3
  import fs from "fs";
4
4
  import path from "path";
5
- import MagicString from "magic-string";
6
- //#region src/inject.ts
5
+ //#region src/modules/inject/index.ts
7
6
  /**
8
7
  * 注入代码插件
9
8
  */
@@ -58,25 +57,39 @@ function uniInject(opts) {
58
57
  const start = newDescriptor.scriptSetup.loc.start.offset;
59
58
  const end = newDescriptor.scriptSetup.loc.end.offset;
60
59
  const scriptCode = newDescriptor.scriptSetup.content;
61
- const s = new MagicString(scriptCode);
62
60
  const ast = parse(scriptCode, {
63
61
  sourceType: "module",
64
62
  plugins: ["typescript"]
65
63
  });
66
64
  const imports = [];
67
- for (const node of ast.program.body) if (node.type === "ImportDeclaration") {
68
- imports.push(scriptCode.slice(node.start, node.end));
69
- s.remove(node.start, node.end + 1);
65
+ for (const node of ast.program.body) if (node.type === "ImportDeclaration" && typeof node.start === "number" && typeof node.end === "number") imports.push({
66
+ code: scriptCode.slice(node.start, node.end),
67
+ start: node.start,
68
+ end: node.end
69
+ });
70
+ let nextScriptCode = scriptCode;
71
+ if (imports.length) {
72
+ const ranges = [...imports].sort((a, b) => b.start - a.start);
73
+ for (const { start: rangeStart, end: rangeEnd } of ranges) {
74
+ let removeEnd = rangeEnd;
75
+ if (nextScriptCode.startsWith("\r\n", removeEnd)) removeEnd += 2;
76
+ else if (nextScriptCode[removeEnd] === "\n") removeEnd += 1;
77
+ nextScriptCode = nextScriptCode.slice(0, rangeStart) + nextScriptCode.slice(removeEnd);
78
+ }
79
+ nextScriptCode = `\n${imports.map((item) => item.code).join("\n")}${nextScriptCode}`;
70
80
  }
71
- if (imports.length) s.prepend("\n" + imports.join("\n"));
72
- newCode = newCode.slice(0, start) + s.toString() + newCode.slice(end);
81
+ newCode = newCode.slice(0, start) + nextScriptCode + newCode.slice(end);
73
82
  }
74
83
  return { code: newCode };
75
84
  }
76
85
  };
77
86
  }
78
87
  //#endregion
79
- //#region src/auto-pages.ts
88
+ //#region src/modules/auto-pages/index.ts
89
+ function readPagesJson(pagesJsonPath) {
90
+ if (!fs.existsSync(pagesJsonPath)) return null;
91
+ return JSON.parse(fs.readFileSync(pagesJsonPath, "utf-8"));
92
+ }
80
93
  function toPosixPath(value) {
81
94
  return value.replace(/\\/g, "/");
82
95
  }
@@ -96,115 +109,152 @@ function walkFiles(dirPath, filePaths = []) {
96
109
  }
97
110
  return filePaths;
98
111
  }
112
+ function analyzeVueContent(content, filePath) {
113
+ if (!content.includes("definePage")) return null;
114
+ const stripRanges = [];
115
+ const { descriptor } = parse$1(content);
116
+ const blocks = [descriptor.scriptSetup, descriptor.script].filter((b) => Boolean(b));
117
+ let config = null;
118
+ for (const block of blocks) {
119
+ const src = block.content;
120
+ if (!src.includes("definePage")) continue;
121
+ let ast;
122
+ try {
123
+ ast = parse(src, {
124
+ sourceType: "module",
125
+ plugins: ["typescript"]
126
+ });
127
+ } catch {
128
+ continue;
129
+ }
130
+ const base = block.loc.start.offset;
131
+ for (const node of ast.program.body) {
132
+ if (typeof node.start !== "number" || typeof node.end !== "number") continue;
133
+ if (node.type === "ImportDeclaration") {
134
+ if (node.specifiers.some((s) => s.type === "ImportSpecifier" && s.imported.type === "Identifier" && s.imported.name === "definePage")) stripRanges.push({
135
+ start: base + node.start,
136
+ end: base + node.end
137
+ });
138
+ continue;
139
+ }
140
+ if (node.type !== "ExpressionStatement" || node.expression.type !== "CallExpression" || node.expression.callee.type !== "Identifier" || node.expression.callee.name !== "definePage") continue;
141
+ stripRanges.push({
142
+ start: base + node.start,
143
+ end: base + node.end
144
+ });
145
+ const arg = node.expression.arguments[0];
146
+ if (config || !arg || typeof arg.start !== "number" || typeof arg.end !== "number") continue;
147
+ const argSrc = src.slice(arg.start, arg.end);
148
+ try {
149
+ config = new Function(`return (${argSrc})`)();
150
+ } catch (err) {
151
+ console.warn(`解析失败(${filePath}): ${err.message}`);
152
+ }
153
+ }
154
+ }
155
+ if (!stripRanges.length && !config) return null;
156
+ return {
157
+ config,
158
+ stripRanges
159
+ };
160
+ }
99
161
  function collectFileRoutes(srcRoot, routeDirs) {
100
162
  const routeSet = /* @__PURE__ */ new Set();
101
- routeDirs.forEach((dir) => {
102
- walkFiles(path.resolve(srcRoot, dir)).forEach((filePath) => {
103
- if (!filePath.endsWith(".vue")) return;
104
- const noExt = path.relative(srcRoot, filePath).slice(0, -4);
105
- routeSet.add(toPosixPath(noExt));
106
- });
107
- });
108
- return Array.from(routeSet).sort();
163
+ const analysisMap = /* @__PURE__ */ new Map();
164
+ for (const dir of routeDirs) for (const filePath of walkFiles(path.resolve(srcRoot, dir))) {
165
+ if (!filePath.endsWith(".vue")) continue;
166
+ const route = toPosixPath(path.relative(srcRoot, filePath).slice(0, -4));
167
+ routeSet.add(route);
168
+ const result = analyzeVueContent(fs.readFileSync(filePath, "utf-8"), filePath);
169
+ if (result) analysisMap.set(route, result);
170
+ }
171
+ return {
172
+ routes: Array.from(routeSet).sort(),
173
+ analysisMap
174
+ };
109
175
  }
110
- function getScanDirs(dir, subPackages) {
176
+ function getScanDirs(mainPackage, subPackages) {
111
177
  const dirSet = /* @__PURE__ */ new Set();
112
- const normalizedMainDir = normalizeDir(dir);
113
- if (normalizedMainDir) dirSet.add(normalizedMainDir);
114
- subPackages.forEach((subRoot) => {
115
- const normalizedSubRoot = normalizeDir(subRoot);
116
- if (!normalizedSubRoot) return;
117
- if (normalizedMainDir) {
118
- dirSet.add(`${normalizedSubRoot}/${normalizedMainDir}`);
119
- return;
120
- }
121
- dirSet.add(normalizedSubRoot);
122
- });
178
+ const mainDir = normalizeDir(mainPackage);
179
+ if (mainDir) dirSet.add(mainDir);
180
+ for (const subRoot of subPackages) {
181
+ const sub = normalizeDir(subRoot);
182
+ if (!sub) continue;
183
+ dirSet.add(mainDir ? `${sub}/${mainDir}` : sub);
184
+ }
123
185
  return Array.from(dirSet);
124
186
  }
125
- function readPagesJson(pagesJsonPath) {
126
- if (!fs.existsSync(pagesJsonPath)) return null;
127
- const content = fs.readFileSync(pagesJsonPath, "utf-8");
128
- return JSON.parse(content);
129
- }
130
- function mergePages(routes, existingPages) {
131
- const existingMap = new Map(existingPages.map((p) => [p.path, p]));
187
+ function buildPages(routes, analysisMap, toFullRoute) {
132
188
  return routes.map((route) => {
133
- return existingMap.get(route) ?? { path: route };
134
- });
135
- }
136
- function hasPagesJsonChanged(current, next) {
137
- return JSON.stringify({
138
- pages: next.pages,
139
- subPackages: next.subPackages ?? []
140
- }) !== JSON.stringify({
141
- pages: current.pages ?? [],
142
- subPackages: current.subPackages ?? []
189
+ const cfg = analysisMap.get(toFullRoute(route))?.config;
190
+ if (!cfg) return { path: route };
191
+ return {
192
+ path: route,
193
+ ...cfg,
194
+ home: void 0
195
+ };
143
196
  });
144
197
  }
145
- function getPagesByFileRoute(routes, pagesJson, subPackageRoots) {
146
- const originPages = pagesJson.pages ?? [];
147
- const originSubPackages = pagesJson.subPackages ?? [];
198
+ function getPagesByFileRoute(routes, analysisMap, pagesJson, subPackageRoots) {
148
199
  const mainRoutes = [];
149
200
  const subRouteMap = /* @__PURE__ */ new Map();
150
201
  const normalizedSubRoots = subPackageRoots.map(normalizeDir);
151
202
  for (const route of routes) {
152
- const matchedRoot = normalizedSubRoots.find((root) => {
153
- return route.startsWith(root + "/");
154
- });
203
+ const matchedRoot = normalizedSubRoots.find((root) => route.startsWith(root + "/"));
155
204
  if (matchedRoot) {
156
- const subPath = route.slice(matchedRoot.length + 1);
157
205
  const list = subRouteMap.get(matchedRoot) ?? [];
158
- list.push(subPath);
206
+ list.push(route.slice(matchedRoot.length + 1));
159
207
  subRouteMap.set(matchedRoot, list);
160
208
  } else mainRoutes.push(route);
161
209
  }
162
- const homePage = originPages[0]?.path;
163
- if (mainRoutes.includes(homePage)) {
164
- const idx = mainRoutes.indexOf(homePage);
165
- mainRoutes.splice(idx, 1);
166
- mainRoutes.unshift(homePage);
167
- }
168
- const pages = mergePages(mainRoutes, originPages);
169
- const subPackages = normalizedSubRoots.map((root) => {
170
- return {
171
- root,
172
- pages: mergePages(subRouteMap.get(root) ?? [], originSubPackages.find((s) => {
173
- return normalizeDir(s.root) === root;
174
- })?.pages ?? [])
175
- };
210
+ const homeRoute = mainRoutes.find((r) => {
211
+ return analysisMap.get(r)?.config?.home === true;
176
212
  });
177
- const merged = {
178
- ...pagesJson,
179
- pages,
180
- subPackages
181
- };
213
+ if (homeRoute) {
214
+ mainRoutes.splice(mainRoutes.indexOf(homeRoute), 1);
215
+ mainRoutes.unshift(homeRoute);
216
+ }
182
217
  return {
183
- merged,
184
- changed: hasPagesJsonChanged(pagesJson, merged)
218
+ ...pagesJson,
219
+ pages: buildPages(mainRoutes, analysisMap, (r) => r),
220
+ subPackages: normalizedSubRoots.map((root) => ({
221
+ root,
222
+ pages: buildPages(subRouteMap.get(root) ?? [], analysisMap, (r) => `${root}/${r}`)
223
+ }))
185
224
  };
186
225
  }
187
- function resolveDtsFilePath(srcRoot, dts) {
188
- return path.isAbsolute(dts) ? dts : path.resolve(srcRoot, dts);
189
- }
190
226
  function buildRouteDts(routes) {
191
- return [
192
- "// Auto-generated by vite-plugin-uni-inject. Do not edit.",
193
- "",
194
- "/** 提取路径 */",
195
- "export type ExtractPath<T extends string, Prefix extends string> = T extends `${Prefix}${infer P}` ? P : never;",
196
- "",
197
- "/** 路由路径 */",
198
- "export type RoutePath =",
199
- routes.map((route) => ` | "/${route}"`).join("\n") + ";",
200
- ""
201
- ].join("\n");
227
+ return `import type { DefinePageConfig } from "vite-plugin-uni-inject";
228
+
229
+ // Auto-generated by vite-plugin-uni-inject. Do not edit.
230
+
231
+ /** 提取路径 */
232
+ export type ExtractPath<T extends string, Prefix extends string> = T extends \`\${Prefix}\${infer P}\` ? P : never;
233
+
234
+ /** 路由路径 */
235
+ export type RoutePath = ${routes.map((r) => `"/${r}"`).join(` |\n`)};
236
+
237
+ declare global {
238
+ /**
239
+ * Vue \`<script setup>\` 宏定义 uniapp 页面级配置。
240
+ */
241
+ function definePage(config: DefinePageConfig): void;
242
+ }
243
+ `;
244
+ }
245
+ function writeIfTextChanged(targetPath, nextContent) {
246
+ const normalize = (s) => {
247
+ return s.replace(/\s+/g, "").replace(/,(?=[)\]}>])/g, "").replace(/([=(<,|&?:])\|/g, "$1");
248
+ };
249
+ if (fs.existsSync(targetPath) && normalize(fs.readFileSync(targetPath, "utf-8")) === normalize(nextContent)) return;
250
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
251
+ fs.writeFileSync(targetPath, nextContent);
202
252
  }
203
253
  /**
204
254
  * 自动补全 pages.json 插件
205
255
  */
206
256
  function uniAutoPages(opts) {
207
- const { dir = "pages", subPackages = [], dts = "uni-pages.d.ts" } = opts || {};
257
+ const { mainPackage = "pages", subPackages = [], dts = "uni-pages.d.ts" } = opts || {};
208
258
  return {
209
259
  name: "vite-plugin-uni-auto-pages",
210
260
  enforce: "pre",
@@ -213,14 +263,31 @@ function uniAutoPages(opts) {
213
263
  const pagesJsonPath = path.join(srcRoot, "pages.json");
214
264
  const pagesJson = readPagesJson(pagesJsonPath);
215
265
  if (!pagesJson) return;
216
- const routes = collectFileRoutes(srcRoot, getScanDirs(dir, subPackages));
217
- const { merged, changed } = getPagesByFileRoute(routes, pagesJson, subPackages);
218
- if (changed) fs.writeFileSync(pagesJsonPath, JSON.stringify(merged, null, 2) + "\n");
266
+ const { routes, analysisMap } = collectFileRoutes(srcRoot, getScanDirs(mainPackage, subPackages));
267
+ const merged = getPagesByFileRoute(routes, analysisMap, pagesJson, subPackages);
268
+ writeIfTextChanged(pagesJsonPath, JSON.stringify(merged, null, 2) + "\n");
219
269
  if (dts) {
220
- const dtsFilePath = resolveDtsFilePath(srcRoot, dts);
221
- const dtsContent = buildRouteDts(routes);
222
- fs.writeFileSync(dtsFilePath, dtsContent);
270
+ const newDts = buildRouteDts(routes);
271
+ writeIfTextChanged(path.join(srcRoot, dts), newDts);
272
+ }
273
+ },
274
+ transform(code, id) {
275
+ const pure = id.split("?")[0];
276
+ if (!pure.endsWith(".vue")) return;
277
+ const analysis = analyzeVueContent(code, pure);
278
+ if (!analysis?.stripRanges.length) return;
279
+ let newCode = code;
280
+ const ranges = [...analysis.stripRanges].sort((a, b) => b.start - a.start);
281
+ for (const { start, end } of ranges) {
282
+ let removeEnd = end;
283
+ if (newCode.startsWith("\r\n", removeEnd)) removeEnd += 2;
284
+ else if (newCode[removeEnd] === "\n") removeEnd += 1;
285
+ newCode = newCode.slice(0, start) + newCode.slice(removeEnd);
223
286
  }
287
+ return {
288
+ code: newCode,
289
+ map: null
290
+ };
224
291
  }
225
292
  };
226
293
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Kriac",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "version": "0.3.1",
7
+ "version": "0.5.0",
8
8
  "homepage": "https://github.com/Kriac/vite-plugin-uni-inject",
9
9
  "repository": {
10
10
  "type": "git",
@@ -20,12 +20,11 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@vue/compiler-sfc": "3.5.27",
23
- "@babel/parser": "7.29.0",
24
- "magic-string": "0.30.21"
23
+ "@babel/parser": "7.29.0"
25
24
  },
26
25
  "devDependencies": {
27
26
  "@types/node": "25.0.10",
28
- "tsdown": "0.21.0",
27
+ "tsdown": "0.22.0",
29
28
  "vue-tsc": "3.2.1"
30
29
  },
31
30
  "files": [